package particle_os import ( "fmt" "os" "path/filepath" "gopkg.in/yaml.v3" ) // Recipe represents a particle-os recipe configuration type Recipe struct { Name string `yaml:"name"` Description string `yaml:"description"` BaseImage string `yaml:"base-image"` ImageVersion string `yaml:"image-version"` Stages []Stage `yaml:"stages"` Output OutputConfig `yaml:"output"` Metadata map[string]interface{} `yaml:"metadata,omitempty"` } // Stage represents a build stage in the recipe type Stage struct { Type string `yaml:"type"` Options map[string]interface{} `yaml:"options,omitempty"` Inputs map[string]interface{} `yaml:"inputs,omitempty"` Devices map[string]interface{} `yaml:"devices,omitempty"` Mounts []interface{} `yaml:"mounts,omitempty"` } // User represents a user account configuration type User struct { Password string `yaml:"password,omitempty"` Shell string `yaml:"shell,omitempty"` Groups []string `yaml:"groups,omitempty"` UID int `yaml:"uid"` GID int `yaml:"gid"` Home string `yaml:"home,omitempty"` Comment string `yaml:"comment,omitempty"` } // UsersOptions represents the options for the users stage type UsersOptions struct { Users map[string]User `yaml:"users"` DefaultShell string `yaml:"default_shell,omitempty"` DefaultHome string `yaml:"default_home,omitempty"` } // OutputConfig defines the output image configuration type OutputConfig struct { Formats []string `yaml:"formats"` Size string `yaml:"size"` Path string `yaml:"path,omitempty"` } // LoadRecipe loads a recipe from a YAML file func LoadRecipe(recipePath string) (*Recipe, error) { data, err := os.ReadFile(recipePath) if err != nil { return nil, fmt.Errorf("failed to read recipe file: %w", err) } var recipe Recipe if err := yaml.Unmarshal(data, &recipe); err != nil { return nil, fmt.Errorf("failed to parse recipe YAML: %w", err) } // Validate required fields if recipe.Name == "" { return nil, fmt.Errorf("recipe must have a name") } if recipe.BaseImage == "" { return nil, fmt.Errorf("recipe must specify a base-image") } if len(recipe.Stages) == 0 { return nil, fmt.Errorf("recipe must have at least one stage") } return &recipe, nil } // SaveRecipe saves a recipe to a YAML file func (r *Recipe) SaveRecipe(outputPath string) error { data, err := yaml.Marshal(r) if err != nil { return fmt.Errorf("failed to marshal recipe: %w", err) } // Ensure output directory exists outputDir := filepath.Dir(outputPath) if err := os.MkdirAll(outputDir, 0755); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } if err := os.WriteFile(outputPath, data, 0644); err != nil { return fmt.Errorf("failed to write recipe file: %w", err) } return nil } // Validate checks if the recipe is valid func (r *Recipe) Validate() error { // Check for required fields if r.Name == "" { return fmt.Errorf("recipe name is required") } if r.BaseImage == "" { return fmt.Errorf("base-image is required") } if len(r.Stages) == 0 { return fmt.Errorf("at least one stage is required") } // Validate stages for i, stage := range r.Stages { if stage.Type == "" { return fmt.Errorf("stage %d: type is required", i) } } // Validate output if len(r.Output.Formats) == 0 { return fmt.Errorf("at least one output format is required") } return nil } // GetStageByType returns all stages of a specific type func (r *Recipe) GetStageByType(stageType string) []Stage { var stages []Stage for _, stage := range r.Stages { if stage.Type == stageType { stages = append(stages, stage) } } return stages } // HasStage checks if the recipe has a stage of the specified type func (r *Recipe) HasStage(stageType string) bool { for _, stage := range r.Stages { if stage.Type == stageType { return true } } return false }