debian-forge-cli/cmd/image-builder/blueprint.go
2025-08-26 10:33:28 -07:00

260 lines
7.2 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"gopkg.in/yaml.v3"
)
type DebianBlueprint struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
Version string `yaml:"version" json:"version"`
Variant string `yaml:"variant" json:"variant"`
Architecture string `yaml:"architecture" json:"architecture"`
Packages BlueprintPackages `yaml:"packages" json:"packages"`
Users []BlueprintUser `yaml:"users" json:"users"`
Groups []BlueprintGroup `yaml:"groups" json:"groups"`
Services []BlueprintService `yaml:"services" json:"services"`
Files []BlueprintFile `yaml:"files" json:"files"`
Customizations BlueprintCustomizations `yaml:"customizations" json:"customizations"`
Created time.Time `yaml:"created" json:"created"`
Modified time.Time `yaml:"modified" json:"modified"`
}
type BlueprintPackages struct {
Include []string `yaml:"include" json:"include"`
Exclude []string `yaml:"exclude" json:"exclude"`
Groups []string `yaml:"groups" json:"groups"`
}
type BlueprintUser struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
Password string `yaml:"password" json:"password"`
Key string `yaml:"key" json:"key"`
Home string `yaml:"home" json:"home"`
Shell string `yaml:"shell" json:"shell"`
Groups []string `yaml:"groups" json:"groups"`
UID int `yaml:"uid" json:"uid"`
GID int `yaml:"gid" json:"gid"`
}
type BlueprintGroup struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
GID int `yaml:"gid" json:"gid"`
}
type BlueprintService struct {
Name string `yaml:"name" json:"name"`
Enabled bool `yaml:"enabled" json:"enabled"`
Masked bool `yaml:"masked" json:"masked"`
}
type BlueprintFile struct {
Path string `yaml:"path" json:"path"`
User string `yaml:"user" json:"user"`
Group string `yaml:"group" json:"group"`
Mode string `yaml:"mode" json:"mode"`
Data string `yaml:"data" json:"data"`
EnsureParents bool `yaml:"ensure_parents" json:"ensure_parents"`
}
type BlueprintCustomizations struct {
Hostname string `yaml:"hostname" json:"hostname"`
Kernel BlueprintKernel `yaml:"kernel" json:"kernel"`
Timezone string `yaml:"timezone" json:"timezone"`
Locale string `yaml:"locale" json:"locale"`
Firewall BlueprintFirewall `yaml:"firewall" json:"firewall"`
SSH BlueprintSSH `yaml:"ssh" json:"ssh"`
}
type BlueprintKernel struct {
Name string `yaml:"name" json:"name"`
Append string `yaml:"append" json:"append"`
Remove string `yaml:"remove" json:"remove"`
}
type BlueprintFirewall struct {
Services []string `yaml:"services" json:"services"`
Ports []string `yaml:"ports" json:"ports"`
}
type BlueprintSSH struct {
KeyFile string `yaml:"key_file" json:"key_file"`
User string `yaml:"user" json:"user"`
}
func loadBlueprint(path string) (*DebianBlueprint, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("cannot read blueprint file: %w", err)
}
var blueprint DebianBlueprint
// Try YAML first
if err := yaml.Unmarshal(data, &blueprint); err != nil {
// Try JSON if YAML fails
if err := json.Unmarshal(data, &blueprint); err != nil {
return nil, fmt.Errorf("cannot parse blueprint file (neither YAML nor JSON): %w", err)
}
}
// Set timestamps if not present
if blueprint.Created.IsZero() {
blueprint.Created = time.Now()
}
blueprint.Modified = time.Now()
return &blueprint, nil
}
func saveBlueprint(blueprint *DebianBlueprint, path string, format string) error {
var data []byte
var err error
switch strings.ToLower(format) {
case "yaml", "yml":
data, err = yaml.Marshal(blueprint)
case "json":
data, err = json.MarshalIndent(blueprint, "", " ")
default:
return fmt.Errorf("unsupported format: %s", format)
}
if err != nil {
return fmt.Errorf("cannot marshal blueprint: %w", err)
}
// Ensure directory exists
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("cannot create directory: %w", err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
return fmt.Errorf("cannot write blueprint file: %w", err)
}
return nil
}
func validateBlueprintStructure(blueprint *DebianBlueprint) error {
var errors []string
if blueprint.Name == "" {
errors = append(errors, "name is required")
}
if blueprint.Variant == "" {
errors = append(errors, "variant is required")
}
if blueprint.Architecture == "" {
errors = append(errors, "architecture is required")
}
// Validate variant
validVariants := []string{"bookworm", "sid", "testing", "backports"}
validVariant := false
for _, v := range validVariants {
if blueprint.Variant == v {
validVariant = true
break
}
}
if !validVariant {
errors = append(errors, fmt.Sprintf("invalid variant: %s (valid: %v)", blueprint.Variant, validVariants))
}
// Validate architecture
validArchs := []string{"amd64", "arm64", "armel", "armhf", "i386", "mips64el", "mipsel", "ppc64el", "s390x"}
validArch := false
for _, a := range validArchs {
if blueprint.Architecture == a {
validArch = true
break
}
}
if !validArch {
errors = append(errors, fmt.Sprintf("invalid architecture: %s (valid: %v)", blueprint.Architecture, validArchs))
}
if len(errors) > 0 {
return fmt.Errorf("blueprint validation failed: %s", strings.Join(errors, "; "))
}
return nil
}
func createDefaultBlueprint(name, variant, architecture string) *DebianBlueprint {
return &DebianBlueprint{
Name: name,
Description: fmt.Sprintf("Debian %s %s blueprint", variant, architecture),
Version: "1.0.0",
Variant: variant,
Architecture: architecture,
Packages: BlueprintPackages{
Include: []string{"task-minimal"},
Exclude: []string{},
Groups: []string{},
},
Users: []BlueprintUser{
{
Name: "debian",
Description: "Default user",
Home: "/home/debian",
Shell: "/bin/bash",
Groups: []string{"users"},
},
},
Groups: []BlueprintGroup{
{
Name: "users",
Description: "Default users group",
},
},
Services: []BlueprintService{
{
Name: "ssh",
Enabled: true,
},
},
Customizations: BlueprintCustomizations{
Hostname: fmt.Sprintf("%s-%s", name, variant),
Timezone: "UTC",
Locale: "en_US.UTF-8",
Kernel: BlueprintKernel{
Name: "linux-image-amd64",
},
},
Created: time.Now(),
Modified: time.Now(),
}
}
func listBlueprints(directory string) ([]string, error) {
files, err := os.ReadDir(directory)
if err != nil {
return nil, fmt.Errorf("cannot read directory: %w", err)
}
var blueprints []string
for _, file := range files {
if !file.IsDir() {
ext := strings.ToLower(filepath.Ext(file.Name()))
if ext == ".yaml" || ext == ".yml" || ext == ".json" {
blueprints = append(blueprints, file.Name())
}
}
}
return blueprints, nil
}