Go Coding Style Guide for OS Image Composer#

Table of Contents#

  1. General Principles

  2. Code Formatting

  3. Naming Conventions

  4. Error Handling

  5. Logging Guidelines

  6. Modularization

  7. Package Design

  8. Function Design

  9. Testing Guidelines

  10. Performance Best Practices

  11. Security Considerations

  12. Project-Specific Guidelines

1. General Principles#

1.1 Follow Go Idioms#

  • Write idiomatic Go code that follows community standards

  • Use gofmt to format all code consistently

  • Run go vet to catch common mistakes

  • Use golint for style suggestions

1.2 Code Clarity Over Cleverness#

// Good: Clear and readable
func isValidImageFormat(format string) bool {
    validFormats := []string{"iso", "qcow2", "raw", "vmdk"}
    for _, valid := range validFormats {
        if format == valid {
            return true
        }
    }
    return false
}

// Avoid: Too clever, hard to understand
func isValidImageFormat(format string) bool {
    return map[string]bool{"iso": true, "qcow2": true, "raw": true, "vmdk": true}[format]
}

1.3 Consistency#

  • Maintain consistent patterns across the codebase

  • Use the same error handling patterns

  • Follow established naming conventions

2. Code Formatting#

2.1 Use gofmt#

All code must be formatted with gofmt. Set up your editor to run it automatically on save.

2.2 Line Length#

  • Keep lines under 120 characters when possible

  • Break long function signatures and calls appropriately

// Good: Proper line breaking
func BuildImage(
    targetOS string,
    targetDist string,
    targetArch string,
    outputPath string,
    options *BuildOptions,
) error {
    // implementation
}

// Good: Method chaining
result, err := builder.
    SetTargetOS(targetOS).
    SetArchitecture(targetArch).
    SetOutputPath(outputPath).
    Build()

2.3 Imports#

  • Group imports in three sections: standard library, third-party, local

  • Use blank lines between groups

import (
    "fmt"
    "os"
    "path/filepath"

    "github.com/spf13/cobra"
    "gopkg.in/yaml.v3"

    "github.com/open-edge-platform/os-image-composer/internal/config"
    "github.com/open-edge-platform/os-image-composer/internal/utils/logger"
)

3. Naming Conventions#

3.1 Variables and Functions#

  • Use camelCase for variables and functions

  • Use descriptive names that explain purpose

// Good
var chrootBuildDir string
var packageCacheDir string
func buildChrootEnvironment() error

// Avoid
var dir string
var cache string
func build() error

3.2 Constants#

  • Use PascalCase for exported constants and camelCase for unexported constants

  • Group related constants

const (
    DefaultImageSize     = "10GB"
    MaxRetryAttempts    = 3
    configFileExtension  = ".yml" // unexported constant
)

3.3 Types and Interfaces#

  • Use PascalCase for exported types

  • Interface names should end with ‘er’ when possible

// Good
type ImageBuilder struct {}
type PackageInstaller interface {}
type ConfigReader interface {}

// Specific to OS Image Composer
type ChrootBuilder struct {}
type RpmInstaller interface {}
type DebInstaller interface {}

3.4 Package Names#

  • Use lowercase, single word package names

  • Package names should be descriptive but concise

// Good package structure for OS Image Composer
package chroot    // for chroot environment building
package rpm       // for RPM package handling
package deb       // for DEB package handling
package config    // for configuration management
package logger    // for logging utilities

4. Error Handling#

4.1 Error Wrapping#

Always wrap errors with context using fmt.Errorf with %w verb:

// Good: Provides context and preserves original error
func downloadPackage(url, destination string) error {
    resp, err := http.Get(url)
    if err != nil {
        return fmt.Errorf("failed to download package from %s: %w", url, err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("download failed with status %d for URL %s", resp.StatusCode, url)
    }

    // ... rest of implementation
    return nil
}

4.2 Named Return Parameters#

Use named returns for complex functions with multiple exit points:

// Good: Clear what's being returned
func createEfiFatImage(isoEfiPath, isoImagesPath string) (efiFatImgPath string, err error) {
    efiFatImgPath = filepath.Join(isoImagesPath, "efiboot.img")

    if err = imagedisc.CreateRawFile(efiFatImgPath, "18MiB"); err != nil {
        err = fmt.Errorf("failed to create EFI FAT image: %w", err)
        return // Bare return uses named parameters
    }

    // ... more implementation
    return // Success case
}

4.3 Resource Cleanup Patterns#

Use defer with named returns instead of “goto fail”:

// Good: Use defer for cleanup
func processImageWithCleanup(imagePath string) (err error) {
    tempDir, err := os.MkdirTemp("", "image-process")
    if err != nil {
        return fmt.Errorf("failed to create temp directory: %w", err)
    }

    // Setup cleanup with proper error handling
    defer func() {
        if err == nil {
            // Success cleanup - fail if cleanup fails
            if cleanupErr := os.RemoveAll(tempDir); cleanupErr != nil {
                log.Errorf("Failed to cleanup temp directory: %v", cleanupErr)
                // Update the return error
                err = fmt.Errorf("cleanup failed: %w", cleanupErr)
            }
        } else {
            // Error cleanup - don't override original error
            if cleanupErr := os.RemoveAll(tempDir); cleanupErr != nil {
                log.Errorf("Failed to cleanup temp directory: %v", cleanupErr)
                // Update the return error with warpped original error and cleanup error
                err = fmt.Errorf("operation failed: %w, cleanup errors: %v", err, cleanupErr)
            }
        }
    }()

    // Main processing logic
    if err = processImage(imagePath, tempDir); err != nil {
        return fmt.Errorf("image processing failed: %w", err)
    }

    return nil
}

4.4 Error Types#

Define custom error types for specific error conditions:

// Define custom errors
type ValidationError struct {
    Field   string
    Value   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed for field '%s' with value '%s': %s",
        e.Field, e.Value, e.Message)
}

// Usage
func validateImageConfig(config *ImageConfig) error {
    if config.Size <= 0 {
        return &ValidationError{
            Field:   "size",
            Value:   fmt.Sprintf("%d", config.Size),
            Message: "size must be positive",
        }
    }
    return nil
}

4.5 Error Handling Patterns#

// Pattern 1: Early return on error
func buildImage() error {
    if err := validateConfig(); err != nil {
        return fmt.Errorf("config validation failed: %w", err)
    }

    if err := setupEnvironment(); err != nil {
        return fmt.Errorf("environment setup failed: %w", err)
    }

    return nil
}

// Pattern 2: Cleanup on error
func processWithCleanup() (err error) {
    tempDir, err := os.MkdirTemp("", "image-builder")
    if err != nil {
        return fmt.Errorf("failed to create temp directory: %w", err)
    }
    defer func() {
        if cleanupErr := os.RemoveAll(tempDir); cleanupErr != nil && err == nil {
            err = fmt.Errorf("cleanup failed: %w", cleanupErr)
        }
    }()

    // ... process implementation
    return nil
}

4.6 Error and Logging Strategy by Component Type#

Rule 1: Internal Utilities (internal/utils)#

Internal utilities should primarily return errors, not log them:

// internal/utils/file/operations.go
package file

// Good: Utility function returns error, doesn't log
func CopyFile(src, dst string) error {
    srcFile, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("failed to open source file %s: %w", src, err)
    }
    defer srcFile.Close()

    // Only use debug logging for utilities when absolutely necessary
    log.Debugf("Copying file from %s to %s", src, dst)

    dstFile, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("failed to create destination file %s: %w", dst, err)
    }
    defer dstFile.Close()

    if _, err := io.Copy(dstFile, srcFile); err != nil {
        return fmt.Errorf("failed to copy file content: %w", err)
    }

    return nil
}

Rule 2: Business Logic Functions#

Functions that call libraries should both log and return errors:

// internal/chroot/chrootbuild.go
func (cb *ChrootBuilder) installPackages(packages []string) error {
    log.Infof("Installing %d packages in chroot environment", len(packages))

    for _, pkg := range packages {
        if err := cb.packageManager.InstallPackage(pkg); err != nil {
            // Log immediately with business context
            log.Errorf("Failed to install package %s in chroot %s: %v", pkg, cb.chrootPath, err)
            // Return error with context for caller
            return fmt.Errorf("failed to install package %s: %w", pkg, err)
        }
        log.Debugf("Successfully installed package: %s", pkg)
    }

    return nil
}

Rule 3: High-Level Orchestration Functions#

High-level functions should only return errors to avoid duplicate logging:

// cmd/build.go or high-level orchestrators
func buildImageWorkflow(config *BuildConfig) error {
    // Only return errors - logging happens at lower levels
    if err := validateConfig(config); err != nil {
        return fmt.Errorf("configuration validation failed: %w", err)
    }

    if err := setupEnvironment(config); err != nil {
        return fmt.Errorf("environment setup failed: %w", err)
    }

    if err := buildImage(config); err != nil {
        return fmt.Errorf("image build failed: %w", err)
    }

    return nil
}

5. Logging Guidelines#

5.1 Global Logger Declaration#

Use package-level logger instead of declaring in each function:

// Good: Declare at package level
package chroot

import (
    "github.com/open-edge-platform/os-image-composer/internal/utils/logger"
)

var log = logger.Logger()

func (cb *ChrootBuilder) BuildChrootEnv() error {
    log.Infof("Starting chroot environment build")
    // ... implementation
}

// Avoid: Declaring in each function
func (cb *ChrootBuilder) BuildChrootEnv() error {
    log := logger.Logger() // Remove this pattern
    log.Infof("Starting chroot environment build")
    // ... implementation
}

5.2 Logging Levels#

// Info: Major workflow steps
log.Infof("Starting chroot environment build for %s/%s", targetOS, targetArch)

// Error: Business logic errors with context
log.Errorf("Failed to install package %s in chroot %s: %v", packageName, chrootPath, err)

// Debug: Detailed operation info (utilities can use this sparingly)
log.Debugf("Mounting %s at %s with options: %s", device, mountpoint, options)

// Warn: Recoverable issues
log.Warnf("Package %s already installed, skipping", packageName)

5.3 Error Context in Messages#

Add business context to error logs, technical context to error returns:

func (cb *ChrootBuilder) downloadPackage(url, destination string) error {
    if err := downloader.Download(url, destination); err != nil {
        // Log with business context
        log.Errorf("Failed to download package for chroot build, URL: %s, destination: %s: %v",
                   url, destination, err)
        // Return with technical context
        return fmt.Errorf("failed to download package from %s: %w", url, err)
    }
    return nil
}

6. Modularization#

6.1 Package Structure#

Organize code into logical packages based on functionality:

internal/
├── chroot/           # Chroot environment building
│   ├── chrootbuild/
│   ├── rpm/
│   ├── deb/
│   └── chrootenv.go
├── config/          # Configuration management
│   ├── manifest/
│   ├── schema/
│   ├── testdata/
│   ├── validate/
│   ├── version/
│   ├── config.go
│   ├── global.go
│   └── merge.go
├── image/           # Image building logic
│   ├── imageboot/
│   ├── imageconvert/
│   ├── imagedisc/
│   ├── imageos/
│   ├── imagesecure/
│   ├── imagesign/
│   ├── initrdmaker/
│   ├── isomaker/
│   └── rawmaker/
├── ospackage/       # OS package management
│   ├── debutils/
│   ├── pkgfetcher/
│   ├── pkgsorter/
│   ├── rpmutils/
│   └── ospackage.go
├── provider/        # OSV provider management
│   ├── azl/
│   ├── elxr/
│   ├── emt/
│   └── provider.go
└── utils/           # Shared utilities
    ├── compression/ # Compression utilities
    ├── file/        # File operations
    ├── logger/      # Logging
    ├── mount/       # mount operations
    ├── shell/       # Shell command execution
    ├── slice/       # data structure slice operations
    └── system/      # system command execution

6.2 Struct-Based Component Design with Interface-Based Dependency Injection#

Prefer struct-based approach over global variables:

Global State vs Struct-Based Comparison#

Aspect

Global State

Struct-based

Thread Safety

❌ Race conditions

✅ Instance isolation

Testability

❌ Shared state pollution

✅ Clean, isolated tests

Multiple Instances

❌ Only one configuration

✅ Multiple concurrent instances

Ownership

❌ Unclear lifecycle

✅ Clear ownership

Encapsulation

❌ Public access

✅ Controlled access

Debugging

❌ Hard to trace changes

✅ Built-in debugging support

Mocking

❌ Difficult to mock

✅ Easy dependency injection

Struct-Based Component Implementation Guidelines#

// Good: Struct-based with dependency injection
type ChrootBuilder struct {
    config        *Config
    chrootPath    string
    cacheDir      string
    packageMgr    PackageManager
    fileSystem    FileSystemInterface
    mountManager  MountManager
}

func NewChrootBuilder(
    config *Config,
    packageMgr PackageManager,
    fs FileSystemInterface,
    mountMgr MountManager,
) *ChrootBuilder {
    return &ChrootBuilder{
        config:       config,
        chrootPath:   filepath.Join(config.WorkDir, "chroot"),
        cacheDir:     config.CacheDir,
        packageMgr:   packageMgr,
        fileSystem:   fs,
        mountManager: mountMgr,
    }
}

// Avoid: Global variables
var (
    globalChrootPath string
    globalCacheDir   string
    globalPackageMgr PackageManager
)

6.3 Function Wrapping Guidelines#

Rules for Wrapping Functions in Structs#

Should be wrapped (methods):

  • Functions that maintain state between calls

  • Functions that are part of the component workflow

  • Functions that use struct state/fields

  • Functions that need to be mocked for testing

Should NOT be wrapped (standalone functions):

  • Functions that are stateless utilities

  • Functions that don’t use struct state

  • Functions that could be used independently

  • Pure computational functions

// Should be methods (use struct state)
func (cb *ChrootBuilder) buildChrootEnv() error {
    // Uses cb.chrootPath, cb.config, etc.
}

func (cb *ChrootBuilder) installPackages(packages []string) error {
    // Uses cb.packageMgr, cb.chrootPath
}

// Should be standalone functions (stateless utilities)
func validateTargetOS(targetOS string) error {
    supportedOS := []string{"ubuntu", "centos", "fedora"}
    // ... validation logic
}

func parseVersion(versionStr string) (*Version, error) {
    // Pure parsing function
}

6.4 Interface-Based Dependency Injection Design#

Design interfaces that are focused and composable:

Rule 1: Define Comprehensive Interfaces#

Create interfaces that define all operations a component needs, enabling complete mockability.

// Good: Comprehensive interface defining all chroot operations
type ChrootEnvInterface interface {
    // Configuration access
    GetChrootEnvRoot() string
    GetTargetOsPkgType() string
    GetTargetOsConfigDir() string

    // Path operations
    GetChrootEnvHostPath(chrootPath string) (string, error)
    GetChrootEnvPath(ChrootEnvHostPath string) (string, error)

    // Mount operations
    MountChrootSysfs(chrootPath string) error
    UmountChrootSysfs(chrootPath string) error
    MountChrootPath(hostFullPath, chrootPath, mountFlags string) error
    UmountChrootPath(chrootPath string) error

    // File operations
    CopyFileFromHostToChroot(hostFilePath, chrootPath string) error
    CopyFileFromChrootToHost(hostFilePath, chrootPath string) error

    // Environment lifecycle
    InitChrootEnv(targetOs, targetDist, targetArch string) error
    CleanupChrootEnv(targetOs, targetDist, targetArch string) error

    // Package management
    TdnfInstallPackage(packageName, installRoot string, repositoryIDList []string) error
    AptInstallPackage(packageName, installRoot string, repoSrcList []string) error
}

Rule 2: Constructor-Based Dependency Injection#

Inject dependencies through constructors, not as method parameters:

// Good: Dependencies injected at construction time
type ChrootEnv struct {
    ChrootEnvRoot       string
    ChrootImageBuildDir string
    ChrootBuilder       chrootbuild.ChrootBuilderInterface  // Injected dependency
}

func NewChrootEnv(targetOs, targetDist, targetArch string) (*ChrootEnv, error) {
    // Create dependency
    chrootBuilder, err := chrootbuild.NewChrootBuilder(targetOs, targetDist, targetArch)
    if err != nil {
        return nil, fmt.Errorf("failed to create chroot builder: %w", err)
    }

    return &ChrootEnv{
        ChrootEnvRoot: chrootEnvRoot,
        ChrootBuilder: chrootBuilder,  // Inject dependency
    }, nil
}

// Avoid: Passing dependencies as method parameters repeatedly
func (ce *ChrootEnv) SomeOperation(builder chrootbuild.ChrootBuilderInterface) error {
    // Don't do this - inject at construction time instead
}

Rule 3: Delegate to Injected Dependencies for Sub Components#

If the sub component’s function will be used outside, use the struct to orchestrate calls to injected dependencies:

// Good: Orchestration through delegation
func (chrootEnv *ChrootEnv) GetTargetOsPkgType() string {
    return chrootEnv.ChrootBuilder.GetTargetOsPkgType()
}

func (chrootEnv *ChrootEnv) GetChrootEnvEssentialPackageList() ([]string, error) {
    return chrootEnv.ChrootBuilder.GetChrootEnvEssentialPackageList()
}

func (chrootEnv *ChrootEnv) GetTargetOsReleaseVersion() string {
    targetOsConfig := chrootEnv.ChrootBuilder.GetTargetOsConfig()
    releaseVersion, ok := targetOsConfig["releaseVersion"]
    if !ok {
        log.Errorf("releaseVersion not found in target OS config")
        return "unknown"
    }
    return releaseVersion.(string)
}

Rule 4: Interface Composition for Complex Dependencies#

When a component needs multiple interfaces, compose them rather than creating monolithic interfaces

Rule 5: Factory Pattern for Complex Construction#

Use factory functions when dependency creation is complex

7. Package Design#

7.1 Package Responsibility#

Each package should have a single, well-defined responsibility:

// package config - handles all configuration operations
package config

type Reader interface {
    ReadConfig(path string) (*Config, error)
}

type Validator interface {
    ValidateConfig(config *Config) error
}

type Writer interface {
    WriteConfig(config *Config, path string) error
}

7.2 Public API Design#

Keep public APIs minimal and stable:

// Good: Clean public API
package chroot

// Public interface
type Builder interface {
    BuildChrootEnv(targetOS, targetDist, targetArch string) error
    GetChrootPath() string
    Cleanup() error
}

// Public constructor
func NewBuilder(configDir string, options ...BuilderOption) (Builder, error) {
    // implementation
}

// Internal implementation
type chrootBuilder struct {
    // private fields
}

func (cb *chrootBuilder) BuildChrootEnv(targetOS, targetDist, targetArch string) error {
    // implementation
}

7.3 Configuration Patterns#

Use functional options for flexible configuration:

type BuilderOption func(*chrootBuilder) error

func WithCacheDir(dir string) BuilderOption {
    return func(cb *chrootBuilder) error {
        if dir == "" {
            return errors.New("cache directory cannot be empty")
        }
        cb.cacheDir = dir
        return nil
    }
}

func WithTimeout(timeout time.Duration) BuilderOption {
    return func(cb *chrootBuilder) error {
        cb.timeout = timeout
        return nil
    }
}

// Usage
builder, err := chroot.NewBuilder(
    configDir,
    chroot.WithCacheDir("/tmp/cache"),
    chroot.WithTimeout(30*time.Minute),
)

8. Function Design#

8.1 Function Length#

Keep functions short and focused (typically under 50 lines):

// Good: Single responsibility, easy to test
func validateTargetOS(targetOS string) error {
    supportedOS := []string{"ubuntu", "centos", "fedora", "debian"}
    for _, os := range supportedOS {
        if targetOS == os {
            return nil
        }
    }
    return fmt.Errorf("unsupported target OS: %s", targetOS)
}

func validateTargetArch(targetArch string) error {
    supportedArch := []string{"amd64", "arm64", "armhf"}
    for _, arch := range supportedArch {
        if targetArch == arch {
            return nil
        }
    }
    return fmt.Errorf("unsupported target architecture: %s", targetArch)
}

8.2 Parameter Lists#

Limit the number of parameters (max 4-5), use structs for complex parameter sets:

// Avoid: Too many parameters
func buildImage(targetOS, targetDist, targetArch, outputPath, tempDir, cacheDir string, timeout int, verbose bool) error

// Good: Use a config struct
type BuildConfig struct {
    TargetOS    string
    TargetDist  string
    TargetArch  string
    OutputPath  string
    TempDir     string
    CacheDir    string
    Timeout     time.Duration
    Verbose     bool
}

func buildImage(config *BuildConfig) error {
    // implementation
}

8.3 Return Values#

Be consistent with return patterns:

// Good: Consistent error handling
func createDirectory(path string) error {
    if err := os.MkdirAll(path, 0700); err != nil {
        return fmt.Errorf("failed to create directory %s: %w", path, err)
    }
    return nil
}

// Good: Multiple returns with clear meaning
func findPackage(name string) (path string, found bool, err error) {
    // implementation
}

9. Testing Guidelines#

9.1 Test Structure#

Follow the AAA pattern (Arrange, Act, Assert):

func TestChrootBuilder_BuildChrootEnv(t *testing.T) {
    tests := []struct {
        name          string
        config        *BuildConfig
        setupFunc     func(*testing.T) string // returns temp dir
        expectedError string
    }{
        {
            name: "successful_build",
            config: &BuildConfig{
                TargetOS:   "ubuntu",
                TargetDist: "20.04",
                TargetArch: "amd64",
            },
            setupFunc:     setupValidEnvironment,
            expectedError: "",
        },
        // ... more test cases
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Arrange
            tempDir := tt.setupFunc(t)
            defer os.RemoveAll(tempDir)

            builder := NewChrootBuilder(tempDir)

            // Act
            err := builder.BuildChrootEnv(tt.config.TargetOS, tt.config.TargetDist, tt.config.TargetArch)

            // Assert
            if tt.expectedError == "" {
                assert.NoError(t, err)
            } else {
                assert.Error(t, err)
                assert.Contains(t, err.Error(), tt.expectedError)
            }
        })
    }
}

9.2 Mocking#

Use interfaces for dependencies to enable easy mocking:

// Mock implementation
type mockPackageInstaller struct {
    shouldFail bool
    installedPackages []string
}

func (m *mockPackageInstaller) InstallPackage(packagePath string) error {
    if m.shouldFail {
        return errors.New("mock installation failure")
    }
    m.installedPackages = append(m.installedPackages, packagePath)
    return nil
}

9.3 Test Utilities#

Create test utilities for common setup:

// testutils/setup.go
func CreateTempDir(t *testing.T) string {
    dir, err := os.MkdirTemp("", "test")
    require.NoError(t, err)
    t.Cleanup(func() {
        os.RemoveAll(dir)
    })
    return dir
}

func CreateMockConfig(targetOS, targetDist, targetArch string) *BuildConfig {
    return &BuildConfig{
        TargetOS:   targetOS,
        TargetDist: targetDist,
        TargetArch: targetArch,
        Timeout:    30 * time.Second,
    }
}

10. Performance Best Practices#

10.1 Memory Management#

// Good: Reuse slices when possible
func processPackages(packages []string) error {
    results := make([]ProcessResult, 0, len(packages)) // Pre-allocate capacity

    for _, pkg := range packages {
        result, err := processPackage(pkg)
        if err != nil {
            return fmt.Errorf("failed to process package %s: %w", pkg, err)
        }
        results = append(results, result)
    }
    return nil
}

// Good: Use string builder for concatenation
func buildPackageList(packages []string) string {
    var builder strings.Builder
    builder.Grow(len(packages) * 20) // Estimate capacity

    for i, pkg := range packages {
        if i > 0 {
            builder.WriteString(", ")
        }
        builder.WriteString(pkg)
    }
    return builder.String()
}

10.2 Concurrency#

// Good: Use worker pools for concurrent processing
func downloadPackagesConcurrently(urls []string, workers int) error {
    urlChan := make(chan string, len(urls))
    errChan := make(chan error, len(urls))

    // Start workers
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for url := range urlChan {
                if err := downloadPackage(url); err != nil {
                    errChan <- fmt.Errorf("failed to download %s: %w", url, err)
                    return
                }
            }
        }()
    }

    // Send work
    for _, url := range urls {
        urlChan <- url
    }
    close(urlChan)

    // Wait for completion
    go func() {
        wg.Wait()
        close(errChan)
    }()

    // Check for errors
    for err := range errChan {
        if err != nil {
            return err
        }
    }

    return nil
}

11. Security Considerations#

11.1 Input Validation#

Always validate inputs, especially from external sources:

func validateImageConfig(config *ImageConfig) error {
    if config.TargetOS == "" {
        return errors.New("target OS cannot be empty")
    }

    // Validate path traversal
    if strings.Contains(config.OutputPath, "..") {
        return errors.New("output path cannot contain path traversal")
    }

    // Validate allowed values
    allowedFormats := map[string]bool{"iso": true, "qcow2": true, "raw": true}
    if !allowedFormats[config.Format] {
        return fmt.Errorf("unsupported image format: %s", config.Format)
    }

    return nil
}

11.2 Command Execution#

Be careful when executing shell commands:

// Good: Use proper escaping and validation
func createFileSystem(devicePath, fsType string) error {
    // Validate inputs
    if !isValidDevicePath(devicePath) {
        return fmt.Errorf("invalid device path: %s", devicePath)
    }

    allowedFS := map[string]bool{"ext4": true, "xfs": true, "vfat": true}
    if !allowedFS[fsType] {
        return fmt.Errorf("unsupported filesystem type: %s", fsType)
    }

    // Use exec.Command instead of shell
    cmd := exec.Command("mkfs", "-t", fsType, devicePath)
    if err := cmd.Run(); err != nil {
        return fmt.Errorf("failed to create filesystem: %w", err)
    }

    return nil
}

11.3 File Permissions#

Set appropriate file permissions:

const (
    FilePermission     = 0644
    DirPermission      = 0755
    ExecutablePermission = 0755
    SecretPermission   = 0600
)

func writeConfigFile(path string, data []byte) error {
    return os.WriteFile(path, data, FilePermission)
}

func writeSecretFile(path string, data []byte) error {
    return os.WriteFile(path, data, SecretPermission)
}

12. Project-Specific Guidelines#

12.1 Image Builder Patterns#

// Use builder pattern for complex image construction
type ImageBuilder struct {
    config *ImageConfig
    steps  []BuildStep
}

func (ib *ImageBuilder) AddStep(step BuildStep) *ImageBuilder {
    ib.steps = append(ib.steps, step)
    return ib
}

func (ib *ImageBuilder) Build() error {
    for i, step := range ib.steps {
        log.Infof("Executing step %d: %s", i+1, step.Name())
        if err := step.Execute(ib.config); err != nil {
            return fmt.Errorf("build step %d failed: %w", i+1, err)
        }
    }
    return nil
}

12.2 Package Management#

// Consistent interface for different package managers
type PackageManager interface {
    InstallPackages(packages []string) error
    UpdateRepository() error
    CleanCache() error
}

type RpmManager struct {
    chrootPath string
    cacheDir   string
}

func (rm *RpmManager) InstallPackages(packages []string) error {
    for _, pkg := range packages {
        if err := rm.installSinglePackage(pkg); err != nil {
            return fmt.Errorf("failed to install package %s: %w", pkg, err)
        }
    }
    return nil
}