Some checks failed
particle-os CI / Test particle-os (push) Failing after 1s
particle-os CI / Integration Test (push) Has been skipped
particle-os CI / Security & Quality (push) Failing after 1s
Test particle-os Basic Functionality / test-basic (push) Failing after 1s
particle-os CI / Build and Release (push) Has been skipped
149 lines
4 KiB
Go
149 lines
4 KiB
Go
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
|
|
}
|