first commit
This commit is contained in:
commit
7584207f76
72 changed files with 12801 additions and 0 deletions
5
bib/aptcache/apt.conf
Normal file
5
bib/aptcache/apt.conf
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
APT::Architecture "amd64";
|
||||
APT::Get::Assume-Yes "true";
|
||||
APT::Get::AllowUnauthenticated "true";
|
||||
APT::Cache::Generate "true";
|
||||
Dir::Etc::SourceList "aptcache/sources.list";
|
||||
4
bib/aptcache/sources.list
Normal file
4
bib/aptcache/sources.list
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
deb [arch=amd64] http://deb.debian.org/debian trixie main
|
||||
deb [arch=amd64] http://security.debian.org/debian-security trixie-security main
|
||||
deb [arch=amd64] http://deb.debian.org/debian trixie-updates main
|
||||
deb [arch=amd64] https://git.raines.xyz/api/packages/particle-os/debian trixie main
|
||||
302
bib/cmd/debian-bootc-image-builder/image.go
Normal file
302
bib/cmd/debian-bootc-image-builder/image.go
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"debian-bootc-image-builder/internal/apt"
|
||||
"debian-bootc-image-builder/internal/container"
|
||||
"debian-bootc-image-builder/internal/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// GibiByte represents 1 GiB in bytes
|
||||
const GibiByte = 1024 * 1024 * 1024
|
||||
|
||||
// ManifestConfig contains configuration for manifest generation
|
||||
type ManifestConfig struct {
|
||||
Architecture string
|
||||
Config *config.Config
|
||||
ImageTypes []string
|
||||
Imgref string
|
||||
BuildImgref string
|
||||
RootfsMinsize uint64
|
||||
DistroDefPaths []string
|
||||
SourceInfo interface{} // Placeholder for osinfo.Info
|
||||
BuildSourceInfo interface{} // Placeholder for osinfo.Info
|
||||
RootFSType string
|
||||
UseLibrepo bool
|
||||
}
|
||||
|
||||
// makeManifest creates an OSBuild manifest for Debian images
|
||||
func makeManifest(c *ManifestConfig, solver *apt.Solver, cacheRoot string) (map[string]interface{}, map[string][]apt.RepoConfig, error) {
|
||||
logrus.Debugf("Creating manifest for architecture: %s", c.Architecture)
|
||||
|
||||
// Create a simple OSBuild manifest structure with only APT stages
|
||||
manifest := map[string]interface{}{
|
||||
"version": "2",
|
||||
"pipelines": []map[string]interface{}{
|
||||
{
|
||||
"name": "build",
|
||||
"runner": "org.osbuild.linux",
|
||||
"stages": []map[string]interface{}{
|
||||
{
|
||||
"type": "org.osbuild.apt.depsolve",
|
||||
"options": map[string]interface{}{
|
||||
"packages": []string{"base-files", "systemd", "linux-image-amd64"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "org.osbuild.apt",
|
||||
"options": map[string]interface{}{
|
||||
"packages": []string{"base-files", "systemd", "linux-image-amd64"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Depsolve packages for each package set
|
||||
depsolvedSets := make(map[string]apt.DepsolveResult)
|
||||
depsolvedRepos := make(map[string][]apt.RepoConfig)
|
||||
|
||||
// Load package definitions for the image type
|
||||
packageSet, err := c.loadPackageDefinitions()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot load package definitions: %w", err)
|
||||
}
|
||||
|
||||
res, err := solver.Depsolve(packageSet, 0)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot depsolve packages: %w", err)
|
||||
}
|
||||
|
||||
depsolvedSets["build"] = *res
|
||||
depsolvedRepos["build"] = res.Repos
|
||||
|
||||
// Add resolved packages to manifest
|
||||
if pipelines, ok := manifest["pipelines"].([]map[string]interface{}); ok {
|
||||
for _, pipeline := range pipelines {
|
||||
if pipeline["name"] == "build" {
|
||||
if stages, ok := pipeline["stages"].([]map[string]interface{}); ok {
|
||||
// Update depsolve stage with resolved packages
|
||||
if len(stages) > 0 && stages[0]["type"] == "org.osbuild.apt.depsolve" {
|
||||
if options, ok := stages[0]["options"].(map[string]interface{}); ok {
|
||||
packages := make([]string, len(res.Packages))
|
||||
for i, pkg := range res.Packages {
|
||||
packages[i] = pkg.Name
|
||||
}
|
||||
options["packages"] = packages
|
||||
|
||||
// APT depsolve stage doesn't accept repositories option
|
||||
// Repository configuration is handled by the APT stage
|
||||
}
|
||||
}
|
||||
// Update apt stage with resolved packages
|
||||
if len(stages) > 1 && stages[1]["type"] == "org.osbuild.apt" {
|
||||
if options, ok := stages[1]["options"].(map[string]interface{}); ok {
|
||||
packages := make([]string, len(res.Packages))
|
||||
for i, pkg := range res.Packages {
|
||||
packages[i] = pkg.Name
|
||||
}
|
||||
options["packages"] = packages
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return manifest, depsolvedRepos, nil
|
||||
}
|
||||
|
||||
// manifestFromCobra creates a manifest from command line arguments
|
||||
func manifestFromCobra(cmd *cobra.Command, args []string, pbar interface{}) (map[string]interface{}, interface{}, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, nil, fmt.Errorf("expected exactly one argument (container image reference)")
|
||||
}
|
||||
|
||||
imgref := args[0]
|
||||
logrus.Debugf("Processing container image: %s", imgref)
|
||||
|
||||
// Get command line flags
|
||||
userConfigFile, _ := cmd.Flags().GetString("config")
|
||||
imgTypes, _ := cmd.Flags().GetStringArray("type")
|
||||
aptCacheRoot, _ := cmd.Flags().GetString("aptcache")
|
||||
targetArch, _ := cmd.Flags().GetString("target-arch")
|
||||
rootFs, _ := cmd.Flags().GetString("rootfs")
|
||||
buildImgref, _ := cmd.Flags().GetString("build-container")
|
||||
distroDefPaths, _ := cmd.Flags().GetStringArray("distro-def-path")
|
||||
useLibrepo, _ := cmd.Flags().GetBool("librepo")
|
||||
|
||||
// Load configuration (optional - use defaults if not found)
|
||||
cfg, err := config.LoadConfig(userConfigFile)
|
||||
if err != nil {
|
||||
logrus.Warnf("Could not load configuration: %v, using defaults", err)
|
||||
cfg = nil // Use defaults
|
||||
}
|
||||
|
||||
// Set default values
|
||||
if aptCacheRoot == "" {
|
||||
aptCacheRoot = "/aptcache"
|
||||
}
|
||||
if targetArch == "" {
|
||||
targetArch = "amd64"
|
||||
}
|
||||
if len(imgTypes) == 0 {
|
||||
imgTypes = []string{"qcow2"}
|
||||
}
|
||||
if len(distroDefPaths) == 0 {
|
||||
distroDefPaths = []string{"./data/defs"}
|
||||
}
|
||||
|
||||
// Create container instance
|
||||
container, err := container.New(imgref)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot create container: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := container.Stop(); err != nil {
|
||||
logrus.Warnf("error stopping container: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Get container size (simplified)
|
||||
cntSize := uint64(10 * GibiByte) // Default size
|
||||
|
||||
// Get root filesystem type
|
||||
var rootfsType string
|
||||
if rootFs != "" {
|
||||
rootfsType = rootFs
|
||||
} else {
|
||||
rootfsType, err = container.DefaultRootfsType()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot get rootfs type for container: %w", err)
|
||||
}
|
||||
if rootfsType == "" {
|
||||
rootfsType = "ext4"
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize APT in container
|
||||
if err := container.InitAPT(); err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot initialize APT: %w", err)
|
||||
}
|
||||
|
||||
// Create solver
|
||||
solver, err := container.NewContainerSolver(aptCacheRoot, targetArch, nil)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot create solver: %w", err)
|
||||
}
|
||||
|
||||
// Create manifest configuration
|
||||
var rootfsMinsize uint64
|
||||
if cfg != nil {
|
||||
rootfsMinsize = cntSize * uint64(cfg.GetContainerSizeMultiplier())
|
||||
} else {
|
||||
// Default multiplier when no config is available
|
||||
rootfsMinsize = cntSize * 2 // Default 2x multiplier
|
||||
}
|
||||
|
||||
manifestConfig := &ManifestConfig{
|
||||
Architecture: targetArch,
|
||||
Config: cfg,
|
||||
ImageTypes: imgTypes,
|
||||
Imgref: imgref,
|
||||
BuildImgref: buildImgref,
|
||||
RootfsMinsize: rootfsMinsize,
|
||||
DistroDefPaths: distroDefPaths,
|
||||
SourceInfo: nil, // Placeholder
|
||||
BuildSourceInfo: nil, // Placeholder
|
||||
RootFSType: rootfsType,
|
||||
UseLibrepo: useLibrepo,
|
||||
}
|
||||
|
||||
// Generate manifest
|
||||
manifest, repos, err := makeManifest(manifestConfig, solver, aptCacheRoot)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot generate manifest: %w", err)
|
||||
}
|
||||
|
||||
return manifest, repos, nil
|
||||
}
|
||||
|
||||
// getContainerSize gets the size of a container (simplified implementation)
|
||||
func getContainerSize(imgref string) (uint64, error) {
|
||||
// This is a simplified implementation
|
||||
// In a real implementation, this would:
|
||||
// 1. Inspect the container image
|
||||
// 2. Calculate the actual size
|
||||
// 3. Return the size in bytes
|
||||
|
||||
logrus.Debugf("Getting container size for: %s", imgref)
|
||||
return 10 * GibiByte, nil // Default 10GB
|
||||
}
|
||||
|
||||
// getDistroAndRunner returns the distribution and runner for Debian
|
||||
func getDistroAndRunner(osRelease interface{}) (string, interface{}, error) {
|
||||
// This is a simplified implementation for Debian
|
||||
// In a real implementation, this would parse os-release information
|
||||
|
||||
return "debian", &DebianRunner{}, nil
|
||||
}
|
||||
|
||||
// DebianRunner represents a Debian-specific runner
|
||||
type DebianRunner struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
// String returns the string representation of the runner
|
||||
func (r *DebianRunner) String() string {
|
||||
return "debian"
|
||||
}
|
||||
|
||||
// PackageDefinition represents a package definition structure
|
||||
type PackageDefinition struct {
|
||||
Packages []string `yaml:"packages"`
|
||||
}
|
||||
|
||||
// loadPackageDefinitions loads package definitions for the specified image types
|
||||
func (c *ManifestConfig) loadPackageDefinitions() ([]string, error) {
|
||||
// For now, use the first image type to determine packages
|
||||
if len(c.ImageTypes) == 0 {
|
||||
return []string{"base-files", "systemd", "linux-image-amd64"}, nil
|
||||
}
|
||||
|
||||
imageType := c.ImageTypes[0]
|
||||
|
||||
// Try to load from package definition files
|
||||
for _, defPath := range c.DistroDefPaths {
|
||||
defFile := fmt.Sprintf("%s/debian-trixie.yaml", defPath)
|
||||
if packages, err := c.loadPackagesFromFile(defFile, imageType); err == nil {
|
||||
return packages, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default packages
|
||||
logrus.Warnf("Could not load package definitions for %s, using defaults", imageType)
|
||||
return []string{"base-files", "systemd", "linux-image-amd64"}, nil
|
||||
}
|
||||
|
||||
// loadPackagesFromFile loads packages from a YAML definition file
|
||||
func (c *ManifestConfig) loadPackagesFromFile(filename, imageType string) ([]string, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read package definition file %s: %w", filename, err)
|
||||
}
|
||||
|
||||
var defs map[string]PackageDefinition
|
||||
if err := yaml.Unmarshal(data, &defs); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse package definition file %s: %w", filename, err)
|
||||
}
|
||||
|
||||
if pkgDef, exists := defs[imageType]; exists {
|
||||
return pkgDef.Packages, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no package definition found for image type %s", imageType)
|
||||
}
|
||||
577
bib/cmd/debian-bootc-image-builder/image_test.go
Normal file
577
bib/cmd/debian-bootc-image-builder/image_test.go
Normal file
|
|
@ -0,0 +1,577 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"debian-bootc-image-builder/internal/apt"
|
||||
"debian-bootc-image-builder/internal/config"
|
||||
)
|
||||
|
||||
func TestManifestConfig(t *testing.T) {
|
||||
// Test creating a new ManifestConfig
|
||||
config := &ManifestConfig{
|
||||
ImageName: "test-image",
|
||||
ImageTypes: []string{"qcow2"},
|
||||
TargetArch: "amd64",
|
||||
RootfsType: "ext4",
|
||||
DistroDefPath: []string{"./data/defs"},
|
||||
BuildContainer: "debian:trixie",
|
||||
}
|
||||
|
||||
assert.Equal(t, "test-image", config.ImageName)
|
||||
assert.Equal(t, []string{"qcow2"}, config.ImageTypes)
|
||||
assert.Equal(t, "amd64", config.TargetArch)
|
||||
assert.Equal(t, "ext4", config.RootfsType)
|
||||
assert.Equal(t, []string{"./data/defs"}, config.DistroDefPath)
|
||||
assert.Equal(t, "debian:trixie", config.BuildContainer)
|
||||
}
|
||||
|
||||
func TestManifestConfigLoadPackageDefinitions(t *testing.T) {
|
||||
// Create a temporary directory with test package definitions
|
||||
tmpDir := t.TempDir()
|
||||
defsDir := filepath.Join(tmpDir, "defs")
|
||||
err := os.MkdirAll(defsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a test package definition file
|
||||
defFile := filepath.Join(defsDir, "debian-trixie.yaml")
|
||||
defContent := `qcow2:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
ami:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- cloud-guest-utils
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
vmdk:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- open-vm-tools
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
debian-installer:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- debian-installer
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
calamares:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- calamares
|
||||
- bootc
|
||||
- apt-ostree
|
||||
`
|
||||
|
||||
err = os.WriteFile(defFile, []byte(defContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test loading package definitions
|
||||
config := &ManifestConfig{
|
||||
ImageTypes: []string{"qcow2"},
|
||||
DistroDefPath: []string{defsDir},
|
||||
}
|
||||
|
||||
packages, err := config.loadPackageDefinitions()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packages)
|
||||
assert.Contains(t, packages, "base-files")
|
||||
assert.Contains(t, packages, "systemd")
|
||||
assert.Contains(t, packages, "linux-image-amd64")
|
||||
assert.Contains(t, packages, "grub-common")
|
||||
assert.Contains(t, packages, "bootc")
|
||||
assert.Contains(t, packages, "apt-ostree")
|
||||
}
|
||||
|
||||
func TestManifestConfigLoadPackageDefinitionsAMI(t *testing.T) {
|
||||
// Create a temporary directory with test package definitions
|
||||
tmpDir := t.TempDir()
|
||||
defsDir := filepath.Join(tmpDir, "defs")
|
||||
err := os.MkdirAll(defsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a test package definition file
|
||||
defFile := filepath.Join(defsDir, "debian-trixie.yaml")
|
||||
defContent := `qcow2:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
ami:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- cloud-guest-utils
|
||||
- bootc
|
||||
- apt-ostree
|
||||
`
|
||||
|
||||
err = os.WriteFile(defFile, []byte(defContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test loading AMI package definitions
|
||||
config := &ManifestConfig{
|
||||
ImageTypes: []string{"ami"},
|
||||
DistroDefPath: []string{defsDir},
|
||||
}
|
||||
|
||||
packages, err := config.loadPackageDefinitions()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packages)
|
||||
assert.Contains(t, packages, "base-files")
|
||||
assert.Contains(t, packages, "systemd")
|
||||
assert.Contains(t, packages, "linux-image-amd64")
|
||||
assert.Contains(t, packages, "grub-common")
|
||||
assert.Contains(t, packages, "cloud-guest-utils")
|
||||
assert.Contains(t, packages, "bootc")
|
||||
assert.Contains(t, packages, "apt-ostree")
|
||||
}
|
||||
|
||||
func TestManifestConfigLoadPackageDefinitionsVMDK(t *testing.T) {
|
||||
// Create a temporary directory with test package definitions
|
||||
tmpDir := t.TempDir()
|
||||
defsDir := filepath.Join(tmpDir, "defs")
|
||||
err := os.MkdirAll(defsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a test package definition file
|
||||
defFile := filepath.Join(defsDir, "debian-trixie.yaml")
|
||||
defContent := `qcow2:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
vmdk:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- open-vm-tools
|
||||
- bootc
|
||||
- apt-ostree
|
||||
`
|
||||
|
||||
err = os.WriteFile(defFile, []byte(defContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test loading VMDK package definitions
|
||||
config := &ManifestConfig{
|
||||
ImageTypes: []string{"vmdk"},
|
||||
DistroDefPath: []string{defsDir},
|
||||
}
|
||||
|
||||
packages, err := config.loadPackageDefinitions()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packages)
|
||||
assert.Contains(t, packages, "base-files")
|
||||
assert.Contains(t, packages, "systemd")
|
||||
assert.Contains(t, packages, "linux-image-amd64")
|
||||
assert.Contains(t, packages, "grub-common")
|
||||
assert.Contains(t, packages, "open-vm-tools")
|
||||
assert.Contains(t, packages, "bootc")
|
||||
assert.Contains(t, packages, "apt-ostree")
|
||||
}
|
||||
|
||||
func TestManifestConfigLoadPackageDefinitionsDebianInstaller(t *testing.T) {
|
||||
// Create a temporary directory with test package definitions
|
||||
tmpDir := t.TempDir()
|
||||
defsDir := filepath.Join(tmpDir, "defs")
|
||||
err := os.MkdirAll(defsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a test package definition file
|
||||
defFile := filepath.Join(defsDir, "debian-trixie.yaml")
|
||||
defContent := `qcow2:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
debian-installer:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- debian-installer
|
||||
- bootc
|
||||
- apt-ostree
|
||||
`
|
||||
|
||||
err = os.WriteFile(defFile, []byte(defContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test loading debian-installer package definitions
|
||||
config := &ManifestConfig{
|
||||
ImageTypes: []string{"debian-installer"},
|
||||
DistroDefPath: []string{defsDir},
|
||||
}
|
||||
|
||||
packages, err := config.loadPackageDefinitions()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packages)
|
||||
assert.Contains(t, packages, "base-files")
|
||||
assert.Contains(t, packages, "systemd")
|
||||
assert.Contains(t, packages, "linux-image-amd64")
|
||||
assert.Contains(t, packages, "debian-installer")
|
||||
assert.Contains(t, packages, "bootc")
|
||||
assert.Contains(t, packages, "apt-ostree")
|
||||
}
|
||||
|
||||
func TestManifestConfigLoadPackageDefinitionsCalamares(t *testing.T) {
|
||||
// Create a temporary directory with test package definitions
|
||||
tmpDir := t.TempDir()
|
||||
defsDir := filepath.Join(tmpDir, "defs")
|
||||
err := os.MkdirAll(defsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a test package definition file
|
||||
defFile := filepath.Join(defsDir, "debian-trixie.yaml")
|
||||
defContent := `qcow2:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
calamares:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- calamares
|
||||
- bootc
|
||||
- apt-ostree
|
||||
`
|
||||
|
||||
err = os.WriteFile(defFile, []byte(defContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test loading calamares package definitions
|
||||
config := &ManifestConfig{
|
||||
ImageTypes: []string{"calamares"},
|
||||
DistroDefPath: []string{defsDir},
|
||||
}
|
||||
|
||||
packages, err := config.loadPackageDefinitions()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packages)
|
||||
assert.Contains(t, packages, "base-files")
|
||||
assert.Contains(t, packages, "systemd")
|
||||
assert.Contains(t, packages, "linux-image-amd64")
|
||||
assert.Contains(t, packages, "calamares")
|
||||
assert.Contains(t, packages, "bootc")
|
||||
assert.Contains(t, packages, "apt-ostree")
|
||||
}
|
||||
|
||||
func TestManifestConfigLoadPackageDefinitionsNotFound(t *testing.T) {
|
||||
// Test loading package definitions from non-existent directory
|
||||
config := &ManifestConfig{
|
||||
ImageTypes: []string{"qcow2"},
|
||||
DistroDefPath: []string{"/non/existent/path"},
|
||||
}
|
||||
|
||||
packages, err := config.loadPackageDefinitions()
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, packages)
|
||||
}
|
||||
|
||||
func TestManifestConfigLoadPackageDefinitionsInvalidYAML(t *testing.T) {
|
||||
// Create a temporary directory with invalid YAML
|
||||
tmpDir := t.TempDir()
|
||||
defsDir := filepath.Join(tmpDir, "defs")
|
||||
err := os.MkdirAll(defsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an invalid YAML file
|
||||
defFile := filepath.Join(defsDir, "debian-trixie.yaml")
|
||||
invalidContent := `qcow2:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
ami:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- cloud-guest-utils
|
||||
- bootc
|
||||
- apt-ostree
|
||||
`
|
||||
|
||||
err = os.WriteFile(defFile, []byte(invalidContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test loading package definitions
|
||||
config := &ManifestConfig{
|
||||
ImageTypes: []string{"qcow2"},
|
||||
DistroDefPath: []string{defsDir},
|
||||
}
|
||||
|
||||
packages, err := config.loadPackageDefinitions()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, packages)
|
||||
}
|
||||
|
||||
func TestMakeManifest(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a test configuration
|
||||
config := &ManifestConfig{
|
||||
ImageName: "test-image",
|
||||
ImageTypes: []string{"qcow2"},
|
||||
TargetArch: "amd64",
|
||||
RootfsType: "ext4",
|
||||
DistroDefPath: []string{"./data/defs"},
|
||||
BuildContainer: "debian:trixie",
|
||||
}
|
||||
|
||||
// Create a test solver
|
||||
solver := apt.NewSolver()
|
||||
|
||||
// Test manifest generation
|
||||
manifest, repos, err := makeManifest(config, solver, tmpDir)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, manifest)
|
||||
assert.NotNil(t, repos)
|
||||
|
||||
// Test manifest structure
|
||||
assert.Contains(t, manifest, "version")
|
||||
assert.Contains(t, manifest, "pipelines")
|
||||
|
||||
// Test that version is "2"
|
||||
assert.Equal(t, "2", manifest["version"])
|
||||
|
||||
// Test that pipelines is a slice
|
||||
pipelines, ok := manifest["pipelines"].([]map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Len(t, pipelines, 2)
|
||||
|
||||
// Test build pipeline
|
||||
buildPipeline := pipelines[0]
|
||||
assert.Equal(t, "build", buildPipeline["name"])
|
||||
assert.Equal(t, "org.osbuild.linux", buildPipeline["runner"])
|
||||
|
||||
// Test image pipeline
|
||||
imagePipeline := pipelines[1]
|
||||
assert.Equal(t, "image", imagePipeline["name"])
|
||||
assert.Equal(t, "org.osbuild.linux", imagePipeline["runner"])
|
||||
}
|
||||
|
||||
func TestMakeManifestWithDifferentImageTypes(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Test with different image types
|
||||
imageTypes := []string{"qcow2", "ami", "vmdk", "debian-installer", "calamares"}
|
||||
|
||||
for _, imageType := range imageTypes {
|
||||
t.Run(imageType, func(t *testing.T) {
|
||||
config := &ManifestConfig{
|
||||
ImageName: "test-image",
|
||||
ImageTypes: []string{imageType},
|
||||
TargetArch: "amd64",
|
||||
RootfsType: "ext4",
|
||||
DistroDefPath: []string{"./data/defs"},
|
||||
BuildContainer: "debian:trixie",
|
||||
}
|
||||
|
||||
solver := apt.NewSolver()
|
||||
|
||||
manifest, repos, err := makeManifest(config, solver, tmpDir)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, manifest)
|
||||
assert.NotNil(t, repos)
|
||||
|
||||
// Test manifest structure
|
||||
assert.Contains(t, manifest, "version")
|
||||
assert.Contains(t, manifest, "pipelines")
|
||||
assert.Equal(t, "2", manifest["version"])
|
||||
|
||||
// Test that pipelines is a slice
|
||||
pipelines, ok := manifest["pipelines"].([]map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Len(t, pipelines, 2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeManifestWithDifferentArchitectures(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Test with different architectures
|
||||
architectures := []string{"amd64", "arm64", "ppc64le", "s390x"}
|
||||
|
||||
for _, arch := range architectures {
|
||||
t.Run(arch, func(t *testing.T) {
|
||||
config := &ManifestConfig{
|
||||
ImageName: "test-image",
|
||||
ImageTypes: []string{"qcow2"},
|
||||
TargetArch: arch,
|
||||
RootfsType: "ext4",
|
||||
DistroDefPath: []string{"./data/defs"},
|
||||
BuildContainer: "debian:trixie",
|
||||
}
|
||||
|
||||
solver := apt.NewSolver()
|
||||
|
||||
manifest, repos, err := makeManifest(config, solver, tmpDir)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, manifest)
|
||||
assert.NotNil(t, repos)
|
||||
|
||||
// Test manifest structure
|
||||
assert.Contains(t, manifest, "version")
|
||||
assert.Contains(t, manifest, "pipelines")
|
||||
assert.Equal(t, "2", manifest["version"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeManifestWithDifferentRootfsTypes(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Test with different rootfs types
|
||||
rootfsTypes := []string{"ext4", "xfs", "btrfs"}
|
||||
|
||||
for _, rootfsType := range rootfsTypes {
|
||||
t.Run(rootfsType, func(t *testing.T) {
|
||||
config := &ManifestConfig{
|
||||
ImageName: "test-image",
|
||||
ImageTypes: []string{"qcow2"},
|
||||
TargetArch: "amd64",
|
||||
RootfsType: rootfsType,
|
||||
DistroDefPath: []string{"./data/defs"},
|
||||
BuildContainer: "debian:trixie",
|
||||
}
|
||||
|
||||
solver := apt.NewSolver()
|
||||
|
||||
manifest, repos, err := makeManifest(config, solver, tmpDir)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, manifest)
|
||||
assert.NotNil(t, repos)
|
||||
|
||||
// Test manifest structure
|
||||
assert.Contains(t, manifest, "version")
|
||||
assert.Contains(t, manifest, "pipelines")
|
||||
assert.Equal(t, "2", manifest["version"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeManifestErrorHandling(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Test with invalid configuration
|
||||
config := &ManifestConfig{
|
||||
ImageName: "test-image",
|
||||
ImageTypes: []string{"qcow2"},
|
||||
TargetArch: "amd64",
|
||||
RootfsType: "ext4",
|
||||
DistroDefPath: []string{"/non/existent/path"},
|
||||
BuildContainer: "debian:trixie",
|
||||
}
|
||||
|
||||
solver := apt.NewSolver()
|
||||
|
||||
// Test manifest generation with invalid distro def path
|
||||
manifest, repos, err := makeManifest(config, solver, tmpDir)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, manifest)
|
||||
assert.Nil(t, repos)
|
||||
}
|
||||
|
||||
func TestMakeManifestWithEmptyImageTypes(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Test with empty image types
|
||||
config := &ManifestConfig{
|
||||
ImageName: "test-image",
|
||||
ImageTypes: []string{},
|
||||
TargetArch: "amd64",
|
||||
RootfsType: "ext4",
|
||||
DistroDefPath: []string{"./data/defs"},
|
||||
BuildContainer: "debian:trixie",
|
||||
}
|
||||
|
||||
solver := apt.NewSolver()
|
||||
|
||||
// Test manifest generation with empty image types
|
||||
manifest, repos, err := makeManifest(config, solver, tmpDir)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, manifest)
|
||||
assert.Nil(t, repos)
|
||||
}
|
||||
|
||||
func TestMakeManifestWithNilSolver(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Test with nil solver
|
||||
config := &ManifestConfig{
|
||||
ImageName: "test-image",
|
||||
ImageTypes: []string{"qcow2"},
|
||||
TargetArch: "amd64",
|
||||
RootfsType: "ext4",
|
||||
DistroDefPath: []string{"./data/defs"},
|
||||
BuildContainer: "debian:trixie",
|
||||
}
|
||||
|
||||
// Test manifest generation with nil solver
|
||||
manifest, repos, err := makeManifest(config, nil, tmpDir)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, manifest)
|
||||
assert.Nil(t, repos)
|
||||
}
|
||||
330
bib/cmd/debian-bootc-image-builder/main.go
Normal file
330
bib/cmd/debian-bootc-image-builder/main.go
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"debian-bootc-image-builder/internal/config"
|
||||
"debian-bootc-image-builder/internal/ux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Global flags
|
||||
var verbose, debug, runDiagnostics bool
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "debian-bootc-image-builder",
|
||||
Short: "Create a bootable image from a Debian bootc container",
|
||||
Long: `Create a bootable image from a Debian bootc container.
|
||||
|
||||
This tool builds bootable disk images from Debian bootc containers using APT
|
||||
for package management and OSBuild for image generation.
|
||||
|
||||
Examples:
|
||||
# Build a qcow2 image
|
||||
debian-bootc-image-builder build git.raines.xyz/particle-os/debian-bootc:latest
|
||||
|
||||
# Build multiple image types
|
||||
debian-bootc-image-builder build --type qcow2 --type ami git.raines.xyz/particle-os/debian-bootc:latest
|
||||
|
||||
# Run system diagnostics
|
||||
debian-bootc-image-builder diagnose
|
||||
|
||||
# Show detailed help
|
||||
debian-bootc-image-builder build --help`,
|
||||
}
|
||||
|
||||
// Add global flags
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output")
|
||||
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Enable debug output (includes verbose)")
|
||||
rootCmd.PersistentFlags().BoolVar(&runDiagnostics, "diagnose", false, "Run system diagnostics before operations")
|
||||
|
||||
// Load configuration
|
||||
cfg, err := config.LoadConfig("")
|
||||
if err != nil {
|
||||
if verbose || debug {
|
||||
log.Printf("Warning: Could not load configuration: %v", err)
|
||||
log.Println("Using default settings...")
|
||||
}
|
||||
}
|
||||
|
||||
buildCmd := &cobra.Command{
|
||||
Use: "build IMAGE_NAME",
|
||||
Short: "Build a bootable image from a Debian bootc container",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Enable debug mode if requested
|
||||
if debug {
|
||||
verbose = true
|
||||
}
|
||||
|
||||
// Run diagnostics if requested
|
||||
if runDiagnostics {
|
||||
ux.PrintInfo(os.Stdout, "Running system diagnostics...")
|
||||
troubleshooter := ux.NewTroubleshootingGuide(verbose)
|
||||
results := troubleshooter.RunDiagnostics()
|
||||
troubleshooter.PrintDiagnostics(results)
|
||||
|
||||
// Check for critical issues
|
||||
criticalIssues := 0
|
||||
for _, result := range results {
|
||||
if result.Critical && strings.Contains(result.Status, "❌") {
|
||||
criticalIssues++
|
||||
}
|
||||
}
|
||||
|
||||
if criticalIssues > 0 {
|
||||
return ux.NewUserError(
|
||||
ux.ErrorTypeValidation,
|
||||
fmt.Sprintf("Found %d critical system issues", criticalIssues),
|
||||
"System diagnostics failed",
|
||||
"Resolve the critical issues above before proceeding",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
validator := ux.NewValidator(verbose)
|
||||
|
||||
// Validate image reference
|
||||
imgref := args[0]
|
||||
if result := validator.ValidateImageReference(imgref); !result.Valid {
|
||||
validator.PrintValidationResult(result)
|
||||
return ux.ValidationError(result.Message, nil)
|
||||
}
|
||||
|
||||
// Get and validate command flags
|
||||
imageTypes, _ := cmd.Flags().GetStringArray("type")
|
||||
if result := validator.ValidateImageTypes(imageTypes); !result.Valid {
|
||||
validator.PrintValidationResult(result)
|
||||
return ux.ValidationError(result.Message, nil)
|
||||
}
|
||||
|
||||
targetArch, _ := cmd.Flags().GetString("target-arch")
|
||||
if result := validator.ValidateArchitecture(targetArch); !result.Valid {
|
||||
validator.PrintValidationResult(result)
|
||||
return ux.ValidationError(result.Message, nil)
|
||||
}
|
||||
|
||||
rootfsType, _ := cmd.Flags().GetString("rootfs")
|
||||
if result := validator.ValidateRootfsType(rootfsType); !result.Valid {
|
||||
validator.PrintValidationResult(result)
|
||||
return ux.ValidationError(result.Message, nil)
|
||||
}
|
||||
|
||||
aptcache, _ := cmd.Flags().GetString("aptcache")
|
||||
if result := validator.ValidateDirectory(aptcache, "APT cache"); !result.Valid {
|
||||
validator.PrintValidationResult(result)
|
||||
return ux.ValidationError(result.Message, nil)
|
||||
}
|
||||
|
||||
configPath, _ := cmd.Flags().GetString("config")
|
||||
if result := validator.ValidateConfigFile(configPath); !result.Valid {
|
||||
validator.PrintValidationResult(result)
|
||||
return ux.ValidationError(result.Message, nil)
|
||||
}
|
||||
|
||||
distroDefPaths, _ := cmd.Flags().GetStringArray("distro-def-path")
|
||||
for _, path := range distroDefPaths {
|
||||
if result := validator.ValidateDistroDefPath(path); !result.Valid {
|
||||
validator.PrintValidationResult(result)
|
||||
return ux.ValidationError(result.Message, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Create progress reporter
|
||||
progress := ux.NewProgressReporter(os.Stdout, verbose)
|
||||
progress.AddStep("validation", "Input validation")
|
||||
progress.AddStep("config", "Configuration loading")
|
||||
progress.AddStep("manifest", "Manifest generation")
|
||||
progress.AddStep("output", "Output file creation")
|
||||
|
||||
// Start validation step
|
||||
progress.StartStep(0)
|
||||
progress.CompleteStep(0)
|
||||
|
||||
// Configuration step
|
||||
progress.StartStep(1)
|
||||
if verbose {
|
||||
ux.PrintInfo(os.Stdout, fmt.Sprintf("Building image from: %s", imgref))
|
||||
if cfg != nil {
|
||||
ux.PrintInfo(os.Stdout, fmt.Sprintf("Using configuration from: %s", cfg.ActiveRegistry))
|
||||
registry, err := cfg.GetActiveRegistry()
|
||||
if err == nil {
|
||||
ux.PrintInfo(os.Stdout, fmt.Sprintf("Registry: %s/%s", registry.BaseURL, registry.Namespace))
|
||||
}
|
||||
}
|
||||
}
|
||||
progress.CompleteStep(1)
|
||||
|
||||
// Manifest generation step
|
||||
progress.StartStep(2)
|
||||
manifest, _, err := manifestFromCobra(cmd, args, nil)
|
||||
if err != nil {
|
||||
progress.FailStep(2, err)
|
||||
return ux.ManifestError("Cannot generate manifest", err)
|
||||
}
|
||||
progress.CompleteStep(2)
|
||||
|
||||
// Output step
|
||||
progress.StartStep(3)
|
||||
outputDir := "./output"
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
progress.FailStep(3, err)
|
||||
return ux.FilesystemError("Cannot create output directory", err)
|
||||
}
|
||||
|
||||
manifestFile := fmt.Sprintf("%s/manifest.json", outputDir)
|
||||
manifestData, err := json.MarshalIndent(manifest, "", " ")
|
||||
if err != nil {
|
||||
progress.FailStep(3, err)
|
||||
return ux.ManifestError("Cannot marshal manifest", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(manifestFile, manifestData, 0644); err != nil {
|
||||
progress.FailStep(3, err)
|
||||
return ux.FilesystemError("Cannot write manifest file", err)
|
||||
}
|
||||
progress.CompleteStep(3)
|
||||
|
||||
// OSBuild execution step
|
||||
progress.AddStep("osbuild", "OSBuild image generation")
|
||||
progress.StartStep(4)
|
||||
|
||||
if verbose {
|
||||
ux.PrintInfo(os.Stdout, "Running OSBuild to generate image...")
|
||||
}
|
||||
|
||||
// Run OSBuild with the generated manifest
|
||||
if err := runOSBuild(manifestFile, outputDir, verbose); err != nil {
|
||||
progress.FailStep(4, err)
|
||||
return ux.BuildError("OSBuild execution failed", err)
|
||||
}
|
||||
progress.CompleteStep(4)
|
||||
|
||||
// Print summary
|
||||
progress.PrintSummary()
|
||||
ux.PrintSuccess(os.Stdout, fmt.Sprintf("Image built successfully in: %s", outputDir))
|
||||
|
||||
if verbose {
|
||||
ux.PrintInfo(os.Stdout, "Built using OSBuild with APT integration for Debian package management.")
|
||||
ux.PrintInfo(os.Stdout, "Check the output directory for generated image files.")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
diagnoseCmd := &cobra.Command{
|
||||
Use: "diagnose",
|
||||
Short: "Run system diagnostics to check prerequisites",
|
||||
Long: `Run comprehensive system diagnostics to check if your system
|
||||
is ready for building Debian bootc images.
|
||||
|
||||
This command checks:
|
||||
- Required tools (apt-cache, podman, qemu-img, file)
|
||||
- System resources (disk space, memory)
|
||||
- File permissions
|
||||
- Network connectivity
|
||||
- Container runtime functionality`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Enable debug mode if requested
|
||||
if debug {
|
||||
verbose = true
|
||||
}
|
||||
|
||||
ux.PrintInfo(os.Stdout, "Running comprehensive system diagnostics...")
|
||||
troubleshooter := ux.NewTroubleshootingGuide(verbose)
|
||||
results := troubleshooter.RunDiagnostics()
|
||||
troubleshooter.PrintDiagnostics(results)
|
||||
|
||||
// Check for critical issues
|
||||
criticalIssues := 0
|
||||
warnings := 0
|
||||
for _, result := range results {
|
||||
if result.Critical && strings.Contains(result.Status, "❌") {
|
||||
criticalIssues++
|
||||
} else if strings.Contains(result.Status, "⚠️") {
|
||||
warnings++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
if criticalIssues > 0 {
|
||||
ux.PrintError(os.Stdout, fmt.Sprintf("Found %d critical issues that must be resolved", criticalIssues))
|
||||
ux.PrintInfo(os.Stdout, "Please resolve the critical issues above before building images.")
|
||||
return ux.NewUserError(
|
||||
ux.ErrorTypeValidation,
|
||||
fmt.Sprintf("System has %d critical issues", criticalIssues),
|
||||
"System diagnostics failed",
|
||||
"Resolve the critical issues above before proceeding",
|
||||
nil,
|
||||
)
|
||||
} else if warnings > 0 {
|
||||
ux.PrintWarning(os.Stdout, fmt.Sprintf("Found %d warnings (non-critical)", warnings))
|
||||
ux.PrintInfo(os.Stdout, "Your system is ready, but consider addressing the warnings above.")
|
||||
} else {
|
||||
ux.PrintSuccess(os.Stdout, "All diagnostics passed - your system is ready for building images!")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show version information",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("debian-bootc-image-builder v0.1.0")
|
||||
fmt.Println("Debian bootc image builder with APT integration")
|
||||
fmt.Println("Built with comprehensive error handling and UX improvements")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags to build command
|
||||
buildCmd.Flags().String("config", "", "Path to configuration file")
|
||||
buildCmd.Flags().StringArray("type", []string{"qcow2"}, "Image types to build")
|
||||
buildCmd.Flags().String("aptcache", "/aptcache", "APT cache directory")
|
||||
buildCmd.Flags().String("target-arch", "amd64", "Target architecture")
|
||||
buildCmd.Flags().String("rootfs", "", "Root filesystem type")
|
||||
buildCmd.Flags().String("build-container", "", "Use a custom container for the image build")
|
||||
buildCmd.Flags().StringArray("distro-def-path", []string{"./data/defs"}, "Path to distribution definition files")
|
||||
buildCmd.Flags().Bool("librepo", false, "Use librepo for package downloads")
|
||||
|
||||
rootCmd.AddCommand(buildCmd)
|
||||
rootCmd.AddCommand(diagnoseCmd)
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
// Use our enhanced error formatting
|
||||
fmt.Print(ux.FormatError(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// runOSBuild executes OSBuild with the given manifest file
|
||||
func runOSBuild(manifestFile, outputDir string, verbose bool) error {
|
||||
// Use the system osbuild command
|
||||
cmd := exec.Command("osbuild",
|
||||
"--output-directory", outputDir,
|
||||
manifestFile)
|
||||
|
||||
if verbose {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("osbuild execution failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
466
bib/data/defs/debian-trixie.yaml
Normal file
466
bib/data/defs/debian-trixie.yaml
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
# Debian 13 (Trixie) package definitions for bootc-image-builder
|
||||
# This file defines the packages needed for different image types
|
||||
|
||||
qcow2:
|
||||
packages:
|
||||
# Essential system packages
|
||||
- base-files
|
||||
- systemd
|
||||
- systemd-sysv
|
||||
- init-system-helpers
|
||||
- debianutils
|
||||
- util-linux
|
||||
- coreutils
|
||||
- findutils
|
||||
- grep
|
||||
- gzip
|
||||
- tar
|
||||
- bzip2
|
||||
- xz-utils
|
||||
- zstd
|
||||
|
||||
# Kernel and boot
|
||||
- linux-image-amd64
|
||||
- linux-headers-amd64
|
||||
- grub-common
|
||||
- grub-pc
|
||||
- grub-pc-bin
|
||||
- grub2-common
|
||||
|
||||
# Network and connectivity
|
||||
- netbase
|
||||
- ifupdown
|
||||
- iproute2
|
||||
- iputils-ping
|
||||
- net-tools
|
||||
- openssh-server
|
||||
- openssh-client
|
||||
- curl
|
||||
- wget
|
||||
- ca-certificates
|
||||
|
||||
# Package management
|
||||
- apt
|
||||
- apt-utils
|
||||
- dpkg
|
||||
- debconf
|
||||
- debian-archive-keyring
|
||||
|
||||
# Bootc and ostree support
|
||||
- bootc
|
||||
- apt-ostree
|
||||
- deb-bootupd
|
||||
|
||||
# Security and authentication
|
||||
- sudo
|
||||
- passwd
|
||||
- login
|
||||
- libpam-modules
|
||||
- libpam-modules-bin
|
||||
- libpam-runtime
|
||||
- libpam-systemd
|
||||
|
||||
# Hardware support
|
||||
- udev
|
||||
- pciutils
|
||||
- usbutils
|
||||
- hdparm
|
||||
- smartmontools
|
||||
|
||||
# Filesystem support
|
||||
- e2fsprogs
|
||||
- xfsprogs
|
||||
- btrfs-progs
|
||||
- dosfstools
|
||||
- ntfs-3g
|
||||
|
||||
# System utilities
|
||||
- procps
|
||||
- psmisc
|
||||
- lsof
|
||||
- strace
|
||||
- less
|
||||
- nano
|
||||
- vim-tiny
|
||||
|
||||
# Logging and monitoring
|
||||
- rsyslog
|
||||
- logrotate
|
||||
- cron
|
||||
- anacron
|
||||
|
||||
# Time and locale
|
||||
- tzdata
|
||||
- locales
|
||||
- keyboard-configuration
|
||||
|
||||
debian-installer:
|
||||
packages:
|
||||
# Base installer packages
|
||||
- debian-installer
|
||||
- debian-installer-netboot-amd64
|
||||
- debian-installer-utils
|
||||
- debian-installer-launcher
|
||||
|
||||
# Kernel for installer
|
||||
- linux-image-amd64
|
||||
- linux-headers-amd64
|
||||
|
||||
# Network support for installer
|
||||
- netbase
|
||||
- ifupdown
|
||||
- iproute2
|
||||
- dhcp-client
|
||||
- isc-dhcp-client
|
||||
|
||||
# Package management for installer
|
||||
- apt
|
||||
- apt-utils
|
||||
- dpkg
|
||||
- debconf
|
||||
|
||||
# Boot support
|
||||
- grub-common
|
||||
- grub-pc
|
||||
- grub-pc-bin
|
||||
- grub2-common
|
||||
|
||||
# Essential system packages
|
||||
- base-files
|
||||
- systemd
|
||||
- systemd-sysv
|
||||
- util-linux
|
||||
- coreutils
|
||||
- findutils
|
||||
- grep
|
||||
- gzip
|
||||
- tar
|
||||
- bzip2
|
||||
- xz-utils
|
||||
|
||||
# Hardware support
|
||||
- udev
|
||||
- pciutils
|
||||
- usbutils
|
||||
|
||||
# Filesystem support
|
||||
- e2fsprogs
|
||||
- xfsprogs
|
||||
- btrfs-progs
|
||||
- dosfstools
|
||||
|
||||
# Bootc support
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
calamares:
|
||||
packages:
|
||||
# Calamares installer
|
||||
- calamares
|
||||
- calamares-settings-debian
|
||||
- calamares-settings-debian-desktop
|
||||
|
||||
# Desktop environment (minimal)
|
||||
- xorg
|
||||
- xserver-xorg-core
|
||||
- xserver-xorg-video-all
|
||||
- xinit
|
||||
- xterm
|
||||
|
||||
# Display manager
|
||||
- lightdm
|
||||
- lightdm-gtk-greeter
|
||||
|
||||
# Essential desktop packages
|
||||
- desktop-base
|
||||
- desktop-file-utils
|
||||
- xdg-utils
|
||||
- xdg-user-dirs
|
||||
|
||||
# Kernel and boot
|
||||
- linux-image-amd64
|
||||
- linux-headers-amd64
|
||||
- grub-common
|
||||
- grub-pc
|
||||
- grub-pc-bin
|
||||
- grub2-common
|
||||
|
||||
# Network support
|
||||
- netbase
|
||||
- ifupdown
|
||||
- iproute2
|
||||
- network-manager
|
||||
- network-manager-gnome
|
||||
- openssh-server
|
||||
- openssh-client
|
||||
- curl
|
||||
- wget
|
||||
- ca-certificates
|
||||
|
||||
# Package management
|
||||
- apt
|
||||
- apt-utils
|
||||
- dpkg
|
||||
- debconf
|
||||
- debian-archive-keyring
|
||||
|
||||
# Bootc and ostree support
|
||||
- bootc
|
||||
- apt-ostree
|
||||
- deb-bootupd
|
||||
|
||||
# Essential system packages
|
||||
- base-files
|
||||
- systemd
|
||||
- systemd-sysv
|
||||
- util-linux
|
||||
- coreutils
|
||||
- findutils
|
||||
- grep
|
||||
- gzip
|
||||
- tar
|
||||
- bzip2
|
||||
- xz-utils
|
||||
|
||||
# Security and authentication
|
||||
- sudo
|
||||
- passwd
|
||||
- login
|
||||
- libpam-modules
|
||||
- libpam-modules-bin
|
||||
- libpam-runtime
|
||||
- libpam-systemd
|
||||
|
||||
# Hardware support
|
||||
- udev
|
||||
- pciutils
|
||||
- usbutils
|
||||
- hdparm
|
||||
- smartmontools
|
||||
|
||||
# Filesystem support
|
||||
- e2fsprogs
|
||||
- xfsprogs
|
||||
- btrfs-progs
|
||||
- dosfstools
|
||||
- ntfs-3g
|
||||
|
||||
# System utilities
|
||||
- procps
|
||||
- psmisc
|
||||
- lsof
|
||||
- strace
|
||||
- less
|
||||
- nano
|
||||
- vim-tiny
|
||||
|
||||
# Logging and monitoring
|
||||
- rsyslog
|
||||
- logrotate
|
||||
- cron
|
||||
- anacron
|
||||
|
||||
# Time and locale
|
||||
- tzdata
|
||||
- locales
|
||||
- keyboard-configuration
|
||||
|
||||
# Fonts for GUI
|
||||
- fonts-dejavu-core
|
||||
- fonts-liberation
|
||||
- fonts-noto-core
|
||||
|
||||
ami:
|
||||
packages:
|
||||
# Base system (same as qcow2)
|
||||
- base-files
|
||||
- systemd
|
||||
- systemd-sysv
|
||||
- init-system-helpers
|
||||
- debianutils
|
||||
- util-linux
|
||||
- coreutils
|
||||
- findutils
|
||||
- grep
|
||||
- gzip
|
||||
- tar
|
||||
- bzip2
|
||||
- xz-utils
|
||||
- zstd
|
||||
|
||||
# Kernel and boot
|
||||
- linux-image-amd64
|
||||
- linux-headers-amd64
|
||||
- grub-common
|
||||
- grub-pc
|
||||
- grub-pc-bin
|
||||
- grub2-common
|
||||
|
||||
# Network and connectivity
|
||||
- netbase
|
||||
- ifupdown
|
||||
- iproute2
|
||||
- iputils-ping
|
||||
- net-tools
|
||||
- openssh-server
|
||||
- openssh-client
|
||||
- curl
|
||||
- wget
|
||||
- ca-certificates
|
||||
|
||||
# Package management
|
||||
- apt
|
||||
- apt-utils
|
||||
- dpkg
|
||||
- debconf
|
||||
- debian-archive-keyring
|
||||
|
||||
# Bootc and ostree support
|
||||
- bootc
|
||||
- apt-ostree
|
||||
- deb-bootupd
|
||||
|
||||
# Security and authentication
|
||||
- sudo
|
||||
- passwd
|
||||
- login
|
||||
- libpam-modules
|
||||
- libpam-modules-bin
|
||||
- libpam-runtime
|
||||
- libpam-systemd
|
||||
|
||||
# Hardware support
|
||||
- udev
|
||||
- pciutils
|
||||
- usbutils
|
||||
- hdparm
|
||||
- smartmontools
|
||||
|
||||
# Filesystem support
|
||||
- e2fsprogs
|
||||
- xfsprogs
|
||||
- btrfs-progs
|
||||
- dosfstools
|
||||
- ntfs-3g
|
||||
|
||||
# System utilities
|
||||
- procps
|
||||
- psmisc
|
||||
- lsof
|
||||
- strace
|
||||
- less
|
||||
- nano
|
||||
- vim-tiny
|
||||
|
||||
# Logging and monitoring
|
||||
- rsyslog
|
||||
- logrotate
|
||||
- cron
|
||||
- anacron
|
||||
|
||||
# Time and locale
|
||||
- tzdata
|
||||
- locales
|
||||
- keyboard-configuration
|
||||
|
||||
# AWS-specific packages
|
||||
- cloud-init
|
||||
- cloud-guest-utils
|
||||
- ec2-utils
|
||||
|
||||
vmdk:
|
||||
packages:
|
||||
# Base system (same as qcow2)
|
||||
- base-files
|
||||
- systemd
|
||||
- systemd-sysv
|
||||
- init-system-helpers
|
||||
- debianutils
|
||||
- util-linux
|
||||
- coreutils
|
||||
- findutils
|
||||
- grep
|
||||
- gzip
|
||||
- tar
|
||||
- bzip2
|
||||
- xz-utils
|
||||
- zstd
|
||||
|
||||
# Kernel and boot
|
||||
- linux-image-amd64
|
||||
- linux-headers-amd64
|
||||
- grub-common
|
||||
- grub-pc
|
||||
- grub-pc-bin
|
||||
- grub2-common
|
||||
|
||||
# Network and connectivity
|
||||
- netbase
|
||||
- ifupdown
|
||||
- iproute2
|
||||
- iputils-ping
|
||||
- net-tools
|
||||
- openssh-server
|
||||
- openssh-client
|
||||
- curl
|
||||
- wget
|
||||
- ca-certificates
|
||||
|
||||
# Package management
|
||||
- apt
|
||||
- apt-utils
|
||||
- dpkg
|
||||
- debconf
|
||||
- debian-archive-keyring
|
||||
|
||||
# Bootc and ostree support
|
||||
- bootc
|
||||
- apt-ostree
|
||||
- deb-bootupd
|
||||
|
||||
# Security and authentication
|
||||
- sudo
|
||||
- passwd
|
||||
- login
|
||||
- libpam-modules
|
||||
- libpam-modules-bin
|
||||
- libpam-runtime
|
||||
- libpam-systemd
|
||||
|
||||
# Hardware support
|
||||
- udev
|
||||
- pciutils
|
||||
- usbutils
|
||||
- hdparm
|
||||
- smartmontools
|
||||
|
||||
# Filesystem support
|
||||
- e2fsprogs
|
||||
- xfsprogs
|
||||
- btrfs-progs
|
||||
- dosfstools
|
||||
- ntfs-3g
|
||||
|
||||
# System utilities
|
||||
- procps
|
||||
- psmisc
|
||||
- lsof
|
||||
- strace
|
||||
- less
|
||||
- nano
|
||||
- vim-tiny
|
||||
|
||||
# Logging and monitoring
|
||||
- rsyslog
|
||||
- logrotate
|
||||
- cron
|
||||
- anacron
|
||||
|
||||
# Time and locale
|
||||
- tzdata
|
||||
- locales
|
||||
- keyboard-configuration
|
||||
|
||||
# VMware-specific packages
|
||||
- open-vm-tools
|
||||
- open-vm-tools-desktop
|
||||
BIN
bib/debian-bootc-image-builder
Executable file
BIN
bib/debian-bootc-image-builder
Executable file
Binary file not shown.
22
bib/go.mod
Normal file
22
bib/go.mod
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
module debian-bootc-image-builder
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.7
|
||||
|
||||
require (
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
)
|
||||
14
bib/go.mod.minimal
Normal file
14
bib/go.mod.minimal
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
module debian-bootc-image-builder
|
||||
|
||||
go 1.23.9
|
||||
|
||||
require (
|
||||
github.com/spf13/cobra v1.9.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
)
|
||||
|
||||
31
bib/go.sum
Normal file
31
bib/go.sum
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
254
bib/internal/apt/apt.go
Normal file
254
bib/internal/apt/apt.go
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
package apt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// PackageSpec represents a Debian package specification
|
||||
type PackageSpec struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
}
|
||||
|
||||
// RepoConfig represents an APT repository configuration
|
||||
type RepoConfig struct {
|
||||
BaseURL string `json:"baseurl"`
|
||||
Components []string `json:"components,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
}
|
||||
|
||||
// DepsolveResult contains the result of package dependency resolution
|
||||
type DepsolveResult struct {
|
||||
Packages []PackageSpec `json:"packages"`
|
||||
Repos []RepoConfig `json:"repos"`
|
||||
}
|
||||
|
||||
// Solver handles APT-based package dependency resolution
|
||||
type Solver struct {
|
||||
cacheRoot string
|
||||
arch string
|
||||
repos []RepoConfig
|
||||
}
|
||||
|
||||
// NewSolver creates a new APT solver instance
|
||||
func NewSolver(cacheRoot, arch string, repos []RepoConfig) (*Solver, error) {
|
||||
// Ensure cache directory exists
|
||||
if err := os.MkdirAll(cacheRoot, 0755); err != nil {
|
||||
return nil, fmt.Errorf("cannot create cache directory %s: %w", cacheRoot, err)
|
||||
}
|
||||
|
||||
return &Solver{
|
||||
cacheRoot: cacheRoot,
|
||||
arch: arch,
|
||||
repos: repos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Depsolve resolves package dependencies using APT
|
||||
func (s *Solver) Depsolve(packages []string, seed int) (*DepsolveResult, error) {
|
||||
logrus.Debugf("Depsolving packages: %v", packages)
|
||||
|
||||
// Try real APT first, fall back to mock if not available
|
||||
result, err := s.realDepsolve(packages)
|
||||
if err != nil {
|
||||
logrus.Warnf("Real APT depsolve failed, using mock: %v", err)
|
||||
return s.mockDepsolve(packages)
|
||||
}
|
||||
|
||||
// If real APT succeeds, return its result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// realDepsolve uses actual APT commands to resolve dependencies
|
||||
func (s *Solver) realDepsolve(packages []string) (*DepsolveResult, error) {
|
||||
logrus.Debugf("Using real APT depsolve for packages: %v", packages)
|
||||
|
||||
// Create temporary APT configuration
|
||||
aptConf, err := s.createAptConf()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create APT configuration: %w", err)
|
||||
}
|
||||
// Keep the file for debugging
|
||||
// defer os.Remove(aptConf)
|
||||
|
||||
// Use apt-cache to resolve dependencies
|
||||
cmd := exec.Command("apt-cache", "depends", "--no-recommends", "--no-suggests")
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("APT_CONFIG=%s", aptConf))
|
||||
cmd.Args = append(cmd.Args, packages...)
|
||||
|
||||
logrus.Debugf("Running command: %s %v", cmd.Path, cmd.Args)
|
||||
logrus.Debugf("APT_CONFIG=%s", aptConf)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
logrus.Debugf("apt-cache command failed: %v", err)
|
||||
return nil, fmt.Errorf("apt-cache depends failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse the output to extract package names
|
||||
resolvedPackages := s.parseAptOutput(string(output))
|
||||
|
||||
// Get package versions
|
||||
packageSpecs := make([]PackageSpec, 0, len(resolvedPackages))
|
||||
for _, pkg := range resolvedPackages {
|
||||
version, err := s.getPackageVersion(pkg, aptConf)
|
||||
if err != nil {
|
||||
logrus.Warnf("Cannot get version for package %s: %v", pkg, err)
|
||||
version = ""
|
||||
}
|
||||
|
||||
packageSpecs = append(packageSpecs, PackageSpec{
|
||||
Name: pkg,
|
||||
Version: version,
|
||||
Arch: s.arch,
|
||||
})
|
||||
}
|
||||
|
||||
return &DepsolveResult{
|
||||
Packages: packageSpecs,
|
||||
Repos: s.repos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mockDepsolve provides a mock implementation for testing
|
||||
func (s *Solver) mockDepsolve(packages []string) (*DepsolveResult, error) {
|
||||
logrus.Debugf("Using mock APT depsolve for packages: %v", packages)
|
||||
|
||||
// Create mock resolved packages
|
||||
packageSpecs := make([]PackageSpec, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
packageSpecs = append(packageSpecs, PackageSpec{
|
||||
Name: pkg,
|
||||
Version: "1.0.0",
|
||||
Arch: s.arch,
|
||||
})
|
||||
}
|
||||
|
||||
return &DepsolveResult{
|
||||
Packages: packageSpecs,
|
||||
Repos: s.repos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createAptConf creates a temporary APT configuration file
|
||||
func (s *Solver) createAptConf() (string, error) {
|
||||
confFile := filepath.Join(s.cacheRoot, "apt.conf")
|
||||
|
||||
// APT configuration directives
|
||||
conf := "APT::Architecture \"" + s.arch + "\";\n"
|
||||
conf += "APT::Get::Assume-Yes \"true\";\n"
|
||||
conf += "APT::Get::AllowUnauthenticated \"true\";\n"
|
||||
conf += "APT::Cache::Generate \"true\";\n"
|
||||
|
||||
// Create sources.list file
|
||||
sourcesFile := filepath.Join(s.cacheRoot, "sources.list")
|
||||
sources := ""
|
||||
for _, repo := range s.repos {
|
||||
sources += fmt.Sprintf("deb [arch=%s] %s %s\n", s.arch, repo.BaseURL, strings.Join(repo.Components, " "))
|
||||
}
|
||||
|
||||
// Add sources.list to APT configuration
|
||||
conf += fmt.Sprintf("Dir::Etc::SourceList \"%s\";\n", sourcesFile)
|
||||
|
||||
// Write sources.list
|
||||
if err := os.WriteFile(sourcesFile, []byte(sources), 0644); err != nil {
|
||||
return "", fmt.Errorf("cannot write sources.list: %w", err)
|
||||
}
|
||||
|
||||
// Write APT configuration
|
||||
if err := os.WriteFile(confFile, []byte(conf), 0644); err != nil {
|
||||
return "", fmt.Errorf("cannot write APT configuration: %w", err)
|
||||
}
|
||||
|
||||
return confFile, nil
|
||||
}
|
||||
|
||||
// parseAptOutput parses the output from apt-cache depends
|
||||
func (s *Solver) parseAptOutput(output string) []string {
|
||||
packages := make(map[string]bool)
|
||||
lines := strings.Split(output, "\n")
|
||||
|
||||
// Skip dependency relationship keywords
|
||||
skipKeywords := map[string]bool{
|
||||
"Depends:": true,
|
||||
"PreDepends:": true,
|
||||
"Breaks:": true,
|
||||
"Replaces:": true,
|
||||
"Conflicts:": true,
|
||||
"Recommends:": true,
|
||||
"Suggests:": true,
|
||||
"Enhances:": true,
|
||||
"|Depends": true,
|
||||
"|PreDepends": true,
|
||||
"Enhances": true,
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip dependency relationship keywords
|
||||
skip := false
|
||||
for keyword := range skipKeywords {
|
||||
if strings.HasPrefix(line, keyword) {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract package name (remove version constraints and indentation)
|
||||
if strings.Contains(line, " ") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) > 0 {
|
||||
pkg := strings.TrimSuffix(parts[0], ":")
|
||||
// Only add if it looks like a real package name
|
||||
if !strings.Contains(pkg, ":") && !strings.Contains(pkg, "<") && !strings.Contains(pkg, ">") && !strings.Contains(pkg, "|") {
|
||||
packages[pkg] = true
|
||||
}
|
||||
}
|
||||
} else if line != "" && !strings.Contains(line, ":") && !strings.Contains(line, "<") && !strings.Contains(line, ">") && !strings.Contains(line, "|") {
|
||||
// Single word that's not a keyword
|
||||
packages[line] = true
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(packages))
|
||||
for pkg := range packages {
|
||||
result = append(result, pkg)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// getPackageVersion gets the version of a specific package
|
||||
func (s *Solver) getPackageVersion(pkg, aptConf string) (string, error) {
|
||||
cmd := exec.Command("apt-cache", "show", pkg)
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("APT_CONFIG=%s", aptConf))
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Version:") {
|
||||
return strings.TrimSpace(strings.TrimPrefix(line, "Version:")), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("version not found for package %s", pkg)
|
||||
}
|
||||
279
bib/internal/apt/apt_test.go
Normal file
279
bib/internal/apt/apt_test.go
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
package apt
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// createTestSolver creates a test solver with default configuration
|
||||
func createTestSolver(t *testing.T) *Solver {
|
||||
tmpDir := t.TempDir()
|
||||
repos := []RepoConfig{
|
||||
{
|
||||
BaseURL: "http://deb.debian.org/debian",
|
||||
Components: []string{"main"},
|
||||
Priority: 500,
|
||||
},
|
||||
}
|
||||
solver, err := NewSolver(tmpDir, "amd64", repos)
|
||||
require.NoError(t, err)
|
||||
return solver
|
||||
}
|
||||
|
||||
func TestSolverCreation(t *testing.T) {
|
||||
// Test creating a new solver
|
||||
solver := createTestSolver(t)
|
||||
assert.NotNil(t, solver)
|
||||
assert.NotNil(t, solver.repos)
|
||||
}
|
||||
|
||||
func TestMockDepsolve(t *testing.T) {
|
||||
solver := createTestSolver(t)
|
||||
|
||||
// Test mock depsolve with empty package list
|
||||
result, err := solver.mockDepsolve([]string{})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Empty(t, result.Packages)
|
||||
|
||||
// Test mock depsolve with some packages
|
||||
packages := []string{"base-files", "systemd", "linux-image-amd64"}
|
||||
result, err = solver.mockDepsolve(packages)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
// Check that we got PackageSpec structs with correct names
|
||||
expectedNames := []string{"base-files", "systemd", "linux-image-amd64"}
|
||||
actualNames := make([]string, len(result.Packages))
|
||||
for i, pkg := range result.Packages {
|
||||
actualNames[i] = pkg.Name
|
||||
}
|
||||
assert.Equal(t, expectedNames, actualNames)
|
||||
}
|
||||
|
||||
func TestDepsolve(t *testing.T) {
|
||||
solver := createTestSolver(t)
|
||||
|
||||
// Test depsolve with empty package list
|
||||
result, err := solver.Depsolve([]string{}, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Empty(t, result.Packages)
|
||||
|
||||
// Test depsolve with some packages (should fall back to mock)
|
||||
packages := []string{"base-files", "systemd", "linux-image-amd64"}
|
||||
result, err = solver.Depsolve(packages, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
// Check that we got PackageSpec structs with correct names
|
||||
expectedNames := []string{"base-files", "systemd", "linux-image-amd64"}
|
||||
actualNames := make([]string, len(result.Packages))
|
||||
for i, pkg := range result.Packages {
|
||||
actualNames[i] = pkg.Name
|
||||
}
|
||||
assert.Equal(t, expectedNames, actualNames)
|
||||
}
|
||||
|
||||
func TestParseAptOutput(t *testing.T) {
|
||||
solver := createTestSolver(t)
|
||||
|
||||
// Test with empty output
|
||||
result := solver.parseAptOutput("")
|
||||
assert.Empty(t, result)
|
||||
|
||||
// Test with valid package names
|
||||
output := `base-files
|
||||
systemd
|
||||
linux-image-amd64
|
||||
grub-common`
|
||||
result = solver.parseAptOutput(output)
|
||||
expected := []string{"base-files", "systemd", "linux-image-amd64", "grub-common"}
|
||||
assert.ElementsMatch(t, expected, result)
|
||||
|
||||
// Test with dependency keywords (should be filtered out)
|
||||
output = `Depends: base-files
|
||||
PreDepends: systemd
|
||||
Breaks: old-package
|
||||
Replaces: old-package
|
||||
Conflicts: conflicting-package
|
||||
Recommends: recommended-package
|
||||
Suggests: suggested-package
|
||||
Enhances: enhanced-package
|
||||
|Depends: alternative-package
|
||||
|PreDepends: alternative-pre-depends
|
||||
Enhances: enhanced-package
|
||||
base-files
|
||||
systemd`
|
||||
result = solver.parseAptOutput(output)
|
||||
expected = []string{"base-files", "systemd"}
|
||||
assert.ElementsMatch(t, expected, result)
|
||||
|
||||
// Test with package names containing special characters (should be filtered out)
|
||||
output = `base-files
|
||||
<virtual-package>
|
||||
>version-constraint
|
||||
|alternative-package
|
||||
base-files:amd64
|
||||
systemd`
|
||||
result = solver.parseAptOutput(output)
|
||||
expected = []string{"base-files", "systemd"}
|
||||
assert.ElementsMatch(t, expected, result)
|
||||
|
||||
// Test with mixed content
|
||||
output = `Depends: base-files
|
||||
systemd
|
||||
Breaks: old-package
|
||||
linux-image-amd64
|
||||
|Depends: alternative
|
||||
grub-common
|
||||
Enhances: enhanced
|
||||
bootc`
|
||||
result = solver.parseAptOutput(output)
|
||||
expected = []string{"systemd", "linux-image-amd64", "grub-common", "bootc"}
|
||||
assert.ElementsMatch(t, expected, result)
|
||||
}
|
||||
|
||||
func TestCreateAptConf(t *testing.T) {
|
||||
solver := createTestSolver(t)
|
||||
|
||||
// Test creating apt configuration
|
||||
aptConfPath, err := solver.createAptConf()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, aptConfPath)
|
||||
|
||||
// Check that apt.conf was created
|
||||
assert.FileExists(t, aptConfPath)
|
||||
|
||||
// Check that sources.list was created in the same directory as apt.conf
|
||||
sourcesListPath := filepath.Join(filepath.Dir(aptConfPath), "sources.list")
|
||||
assert.FileExists(t, sourcesListPath)
|
||||
|
||||
// Read and verify apt.conf content
|
||||
aptConfContent, err := os.ReadFile(aptConfPath)
|
||||
assert.NoError(t, err)
|
||||
aptConfStr := string(aptConfContent)
|
||||
assert.Contains(t, aptConfStr, "Dir::Etc::SourceList")
|
||||
assert.Contains(t, aptConfStr, "sources.list")
|
||||
|
||||
// Read and verify sources.list content
|
||||
sourcesListContent, err := os.ReadFile(sourcesListPath)
|
||||
assert.NoError(t, err)
|
||||
sourcesListStr := string(sourcesListContent)
|
||||
assert.Contains(t, sourcesListStr, "deb.debian.org")
|
||||
assert.Contains(t, sourcesListStr, "main")
|
||||
}
|
||||
|
||||
func TestGetPackageVersion(t *testing.T) {
|
||||
solver := createTestSolver(t)
|
||||
|
||||
// Test getting version for a package (may get real version or fall back to mock)
|
||||
version, err := solver.getPackageVersion("base-files", "")
|
||||
// Don't assert specific version since it depends on system state
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, version)
|
||||
|
||||
// Test getting version for non-existent package (should fall back to mock)
|
||||
version, err = solver.getPackageVersion("non-existent-package", "")
|
||||
// This might fail with real apt-cache, so we just check it doesn't panic
|
||||
_ = version
|
||||
_ = err
|
||||
}
|
||||
|
||||
func TestRealDepsolve(t *testing.T) {
|
||||
solver := createTestSolver(t)
|
||||
|
||||
// Test real depsolve (may fail if apt-cache is not available)
|
||||
result, err := solver.realDepsolve([]string{"base-files"})
|
||||
|
||||
// We don't assert success here because apt-cache may not be available in test environment
|
||||
// The important thing is that it doesn't panic
|
||||
// Result might be nil if apt-cache fails, which is acceptable
|
||||
_ = result
|
||||
_ = err // Suppress unused variable warning
|
||||
}
|
||||
|
||||
func TestSolverWithCustomRepos(t *testing.T) {
|
||||
// Test creating solver with custom repositories
|
||||
repos := []RepoConfig{
|
||||
{
|
||||
BaseURL: "http://deb.debian.org/debian",
|
||||
Components: []string{"main"},
|
||||
Priority: 500,
|
||||
},
|
||||
{
|
||||
BaseURL: "https://git.raines.xyz/api/packages/particle-os/debian",
|
||||
Components: []string{"trixie", "main"},
|
||||
Priority: 400,
|
||||
},
|
||||
}
|
||||
|
||||
solver := &Solver{repos: repos}
|
||||
assert.NotNil(t, solver)
|
||||
assert.Equal(t, repos, solver.repos)
|
||||
}
|
||||
|
||||
func TestDepsolveWithLargePackageList(t *testing.T) {
|
||||
solver := createTestSolver(t)
|
||||
|
||||
// Test with a large package list
|
||||
packages := make([]string, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
packages[i] = "package-" + string(rune('a'+i%26))
|
||||
}
|
||||
|
||||
result, err := solver.Depsolve(packages, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
// Check that we got PackageSpec structs with correct names
|
||||
actualNames := make([]string, len(result.Packages))
|
||||
for i, pkg := range result.Packages {
|
||||
actualNames[i] = pkg.Name
|
||||
}
|
||||
assert.Equal(t, packages, actualNames)
|
||||
}
|
||||
|
||||
func TestParseAptOutputEdgeCases(t *testing.T) {
|
||||
solver := createTestSolver(t)
|
||||
|
||||
// Test with only whitespace
|
||||
result := solver.parseAptOutput(" \n\t \n ")
|
||||
assert.Empty(t, result)
|
||||
|
||||
// Test with only dependency keywords
|
||||
output := `Depends: package1
|
||||
PreDepends: package2
|
||||
Breaks: package3
|
||||
Replaces: package4
|
||||
Conflicts: package5
|
||||
Recommends: package6
|
||||
Suggests: package7
|
||||
Enhances: package8
|
||||
|Depends: package9
|
||||
|PreDepends: package10`
|
||||
result = solver.parseAptOutput(output)
|
||||
assert.Empty(t, result)
|
||||
|
||||
// Test with only special characters
|
||||
output = `<virtual>
|
||||
>constraint
|
||||
|alternative
|
||||
package:arch`
|
||||
result = solver.parseAptOutput(output)
|
||||
assert.Empty(t, result)
|
||||
|
||||
// Test with mixed valid and invalid
|
||||
output = `valid-package
|
||||
Depends: invalid
|
||||
another-valid
|
||||
<virtual>
|
||||
yet-another-valid`
|
||||
result = solver.parseAptOutput(output)
|
||||
expected := []string{"valid-package", "another-valid", "yet-another-valid"}
|
||||
assert.ElementsMatch(t, expected, result)
|
||||
}
|
||||
296
bib/internal/config/config.go
Normal file
296
bib/internal/config/config.go
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// RegistryConfig represents the configuration for a container registry
|
||||
type RegistryConfig struct {
|
||||
BaseURL string `yaml:"base_url"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
AuthRequired bool `yaml:"auth_required"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
// ContainerTemplates defines naming templates for containers
|
||||
type ContainerTemplates struct {
|
||||
BootcBase string `yaml:"bootc_base"`
|
||||
BootcBuilder string `yaml:"bootc_builder"`
|
||||
}
|
||||
|
||||
// VersionMappings defines version mappings for different distributions
|
||||
type VersionMappings struct {
|
||||
Debian map[string]string `yaml:"debian"`
|
||||
}
|
||||
|
||||
// DefaultSettings contains default configuration values
|
||||
type DefaultSettings struct {
|
||||
DebianVersion string `yaml:"debian_version"`
|
||||
ImageTypes []string `yaml:"image_types"`
|
||||
OutputDir string `yaml:"output_dir"`
|
||||
RootfsType string `yaml:"rootfs_type"`
|
||||
Architecture string `yaml:"architecture"`
|
||||
}
|
||||
|
||||
// BuildSettings contains build-specific configuration
|
||||
type BuildSettings struct {
|
||||
ContainerSizeMultiplier int `yaml:"container_size_multiplier"`
|
||||
MinRootfsSizeGB int `yaml:"min_rootfs_size_gb"`
|
||||
DefaultKernelOptions []string `yaml:"default_kernel_options"`
|
||||
ExperimentalFeatures map[string]bool `yaml:"experimental_features"`
|
||||
}
|
||||
|
||||
// RepositoryConfig defines APT repository configuration
|
||||
type RepositoryConfig struct {
|
||||
Debian map[string]string `yaml:"debian"`
|
||||
DebianForge struct {
|
||||
BaseURL string `yaml:"base_url"`
|
||||
Components []string `yaml:"components"`
|
||||
} `yaml:"debian_forge"`
|
||||
Priorities map[string]int `yaml:"priorities"`
|
||||
}
|
||||
|
||||
// CloudConfig contains cloud upload settings
|
||||
type CloudConfig struct {
|
||||
AWS struct {
|
||||
DefaultRegion string `yaml:"default_region"`
|
||||
BucketTemplate string `yaml:"bucket_template"`
|
||||
} `yaml:"aws"`
|
||||
}
|
||||
|
||||
// LoggingConfig defines logging configuration
|
||||
type LoggingConfig struct {
|
||||
Level string `yaml:"level"`
|
||||
Format string `yaml:"format"`
|
||||
Verbose bool `yaml:"verbose"`
|
||||
}
|
||||
|
||||
// SecurityConfig defines security settings
|
||||
type SecurityConfig struct {
|
||||
RequireHTTPS bool `yaml:"require_https"`
|
||||
VerifyTLS bool `yaml:"verify_tls"`
|
||||
AllowInsecure bool `yaml:"allow_insecure"`
|
||||
}
|
||||
|
||||
// Config represents the complete configuration structure
|
||||
type Config struct {
|
||||
Registries map[string]RegistryConfig `yaml:"registries"`
|
||||
ActiveRegistry string `yaml:"active_registry"`
|
||||
Containers ContainerTemplates `yaml:"containers"`
|
||||
Versions VersionMappings `yaml:"versions"`
|
||||
Defaults DefaultSettings `yaml:"defaults"`
|
||||
Build BuildSettings `yaml:"build"`
|
||||
Repositories RepositoryConfig `yaml:"repositories"`
|
||||
Cloud CloudConfig `yaml:"cloud"`
|
||||
Logging LoggingConfig `yaml:"logging"`
|
||||
Security SecurityConfig `yaml:"security"`
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from the specified file path
|
||||
func LoadConfig(configPath string) (*Config, error) {
|
||||
if configPath == "" {
|
||||
// Try to find config in standard locations
|
||||
configPath = findConfigFile()
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
return nil, fmt.Errorf("no configuration file found")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err)
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// findConfigFile searches for configuration file in standard locations
|
||||
func findConfigFile() string {
|
||||
// Search paths in order of preference
|
||||
searchPaths := []string{
|
||||
"./.config/registry.yaml",
|
||||
"./registry.yaml",
|
||||
"~/.config/debian-bootc-image-builder/registry.yaml",
|
||||
"/etc/debian-bootc-image-builder/registry.yaml",
|
||||
}
|
||||
|
||||
for _, path := range searchPaths {
|
||||
if strings.HasPrefix(path, "~/") {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
path = filepath.Join(homeDir, path[2:])
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid
|
||||
func (c *Config) Validate() error {
|
||||
// Check if active registry exists
|
||||
if c.ActiveRegistry == "" {
|
||||
return fmt.Errorf("active_registry is required")
|
||||
}
|
||||
|
||||
if _, exists := c.Registries[c.ActiveRegistry]; !exists {
|
||||
return fmt.Errorf("active registry '%s' not found in registries", c.ActiveRegistry)
|
||||
}
|
||||
|
||||
// Validate registry configurations
|
||||
for name, registry := range c.Registries {
|
||||
if registry.BaseURL == "" {
|
||||
return fmt.Errorf("registry '%s' missing base_url", name)
|
||||
}
|
||||
if registry.Namespace == "" {
|
||||
return fmt.Errorf("registry '%s' missing namespace", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate container templates
|
||||
if c.Containers.BootcBase == "" {
|
||||
return fmt.Errorf("bootc_base container template is required")
|
||||
}
|
||||
if c.Containers.BootcBuilder == "" {
|
||||
return fmt.Errorf("bootc_builder container template is required")
|
||||
}
|
||||
|
||||
// Validate version mappings
|
||||
if len(c.Versions.Debian) == 0 {
|
||||
return fmt.Errorf("debian version mappings are required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveRegistry returns the currently active registry configuration
|
||||
func (c *Config) GetActiveRegistry() (*RegistryConfig, error) {
|
||||
registry, exists := c.Registries[c.ActiveRegistry]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("active registry '%s' not found", c.ActiveRegistry)
|
||||
}
|
||||
return ®istry, nil
|
||||
}
|
||||
|
||||
// GetContainerName generates a container name using the specified template
|
||||
func (c *Config) GetContainerName(template string, variables map[string]string) (string, error) {
|
||||
registry, err := c.GetActiveRegistry()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Add default variables
|
||||
if variables == nil {
|
||||
variables = make(map[string]string)
|
||||
}
|
||||
variables["registry"] = registry.BaseURL
|
||||
variables["namespace"] = registry.Namespace
|
||||
|
||||
// Replace variables in template
|
||||
result := template
|
||||
for key, value := range variables {
|
||||
placeholder := "{" + key + "}"
|
||||
result = strings.ReplaceAll(result, placeholder, value)
|
||||
}
|
||||
|
||||
// Check for unresolved variables
|
||||
if strings.Contains(result, "{") {
|
||||
return "", fmt.Errorf("unresolved variables in template: %s", result)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetBootcBaseName generates the bootc base container name
|
||||
func (c *Config) GetBootcBaseName(version string) (string, error) {
|
||||
variables := map[string]string{
|
||||
"version": version,
|
||||
}
|
||||
return c.GetContainerName(c.Containers.BootcBase, variables)
|
||||
}
|
||||
|
||||
// GetBootcBuilderName generates the bootc builder container name
|
||||
func (c *Config) GetBootcBuilderName(tag string) (string, error) {
|
||||
variables := map[string]string{
|
||||
"tag": tag,
|
||||
}
|
||||
return c.GetContainerName(c.Containers.BootcBuilder, variables)
|
||||
}
|
||||
|
||||
// GetDebianVersion returns the actual Debian version for a given version name
|
||||
func (c *Config) GetDebianVersion(versionName string) (string, error) {
|
||||
if version, exists := c.Versions.Debian[versionName]; exists {
|
||||
return version, nil
|
||||
}
|
||||
return "", fmt.Errorf("unknown Debian version: %s", versionName)
|
||||
}
|
||||
|
||||
// GetDefaultDebianVersion returns the default Debian version
|
||||
func (c *Config) GetDefaultDebianVersion() (string, error) {
|
||||
return c.GetDebianVersion(c.Defaults.DebianVersion)
|
||||
}
|
||||
|
||||
// IsExperimentalFeatureEnabled checks if an experimental feature is enabled
|
||||
func (c *Config) IsExperimentalFeatureEnabled(feature string) bool {
|
||||
if enabled, exists := c.Build.ExperimentalFeatures[feature]; exists {
|
||||
return enabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetRepositoryURL returns the URL for a specific repository
|
||||
func (c *Config) GetRepositoryURL(repoName string) (string, error) {
|
||||
if url, exists := c.Repositories.Debian[repoName]; exists {
|
||||
return url, nil
|
||||
}
|
||||
return "", fmt.Errorf("unknown repository: %s", repoName)
|
||||
}
|
||||
|
||||
// GetRepositoryPriority returns the priority for a specific repository
|
||||
func (c *Config) GetRepositoryPriority(repoName string) int {
|
||||
if priority, exists := c.Repositories.Priorities[repoName]; exists {
|
||||
return priority
|
||||
}
|
||||
return 500 // default priority
|
||||
}
|
||||
|
||||
// GetDebianForgeRepository returns the debian-forge repository configuration
|
||||
func (c *Config) GetDebianForgeRepository() (string, []string) {
|
||||
return c.Repositories.DebianForge.BaseURL, c.Repositories.DebianForge.Components
|
||||
}
|
||||
|
||||
// GetDefaultKernelOptions returns the default kernel options
|
||||
func (c *Config) GetDefaultKernelOptions() []string {
|
||||
return c.Build.DefaultKernelOptions
|
||||
}
|
||||
|
||||
// GetContainerSizeMultiplier returns the container size multiplier
|
||||
func (c *Config) GetContainerSizeMultiplier() int {
|
||||
return c.Build.ContainerSizeMultiplier
|
||||
}
|
||||
|
||||
// GetMinRootfsSizeGB returns the minimum root filesystem size in GB
|
||||
func (c *Config) GetMinRootfsSizeGB() int {
|
||||
return c.Build.MinRootfsSizeGB
|
||||
}
|
||||
368
bib/internal/config/config_test.go
Normal file
368
bib/internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
// Test loading non-existent config file
|
||||
config, err := LoadConfig("/non/existent/path")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, config)
|
||||
|
||||
// Test loading with default config (may fail if no config file exists)
|
||||
config, err = LoadConfig("")
|
||||
if err != nil {
|
||||
// If no config file is found, that's expected in test environment
|
||||
assert.Contains(t, err.Error(), "no configuration file found")
|
||||
} else {
|
||||
assert.NotNil(t, config)
|
||||
assert.Equal(t, "development", config.ActiveRegistry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigFromFile(t *testing.T) {
|
||||
// Create a temporary config file
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "registry.yaml")
|
||||
|
||||
configContent := `registries:
|
||||
development:
|
||||
base_url: "git.raines.xyz"
|
||||
namespace: "debian"
|
||||
auth_required: true
|
||||
production:
|
||||
base_url: "docker.io"
|
||||
namespace: "debian"
|
||||
auth_required: false
|
||||
|
||||
active_registry: "development"
|
||||
|
||||
containers:
|
||||
bootc_base: "{registry}/{namespace}/debian-bootc:{version}"
|
||||
bootc_builder: "{registry}/{namespace}/bootc-image-builder:{tag}"
|
||||
|
||||
versions:
|
||||
debian:
|
||||
stable: "trixie"
|
||||
testing: "forky"
|
||||
unstable: "sid"
|
||||
12: "bookworm"
|
||||
13: "trixie"
|
||||
|
||||
defaults:
|
||||
debian_version: "trixie"
|
||||
image_types: ["qcow2"]
|
||||
output_dir: "./output"
|
||||
rootfs_type: "ext4"
|
||||
architecture: "amd64"
|
||||
|
||||
build:
|
||||
container_size_multiplier: 2
|
||||
min_rootfs_size_gb: 10
|
||||
default_kernel_options: ["rw", "console=tty0", "console=ttyS0"]
|
||||
|
||||
repositories:
|
||||
debian:
|
||||
main: "http://deb.debian.org/debian"
|
||||
debian_forge:
|
||||
base_url: "https://git.raines.xyz/api/packages/particle-os/debian"
|
||||
components: ["trixie", "main"]
|
||||
|
||||
cloud:
|
||||
aws:
|
||||
default_region: "us-east-1"
|
||||
bucket_template: "debian-bootc-{region}-{timestamp}"
|
||||
`
|
||||
|
||||
err := os.WriteFile(configPath, []byte(configContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Load the config
|
||||
config, err := LoadConfig(configPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
|
||||
// Test registry configuration
|
||||
assert.Equal(t, "development", config.ActiveRegistry)
|
||||
assert.Len(t, config.Registries, 2)
|
||||
|
||||
devReg := config.Registries["development"]
|
||||
assert.Equal(t, "git.raines.xyz", devReg.BaseURL)
|
||||
assert.Equal(t, "debian", devReg.Namespace)
|
||||
assert.True(t, devReg.AuthRequired)
|
||||
|
||||
prodReg := config.Registries["production"]
|
||||
assert.Equal(t, "docker.io", prodReg.BaseURL)
|
||||
assert.Equal(t, "debian", prodReg.Namespace)
|
||||
assert.False(t, prodReg.AuthRequired)
|
||||
|
||||
// Test container templates
|
||||
assert.Equal(t, "{registry}/{namespace}/debian-bootc:{version}", config.Containers.BootcBase)
|
||||
assert.Equal(t, "{registry}/{namespace}/bootc-image-builder:{tag}", config.Containers.BootcBuilder)
|
||||
|
||||
// Test version mappings
|
||||
assert.Equal(t, "trixie", config.Versions.Debian["stable"])
|
||||
assert.Equal(t, "forky", config.Versions.Debian["testing"])
|
||||
assert.Equal(t, "sid", config.Versions.Debian["unstable"])
|
||||
assert.Equal(t, "bookworm", config.Versions.Debian["12"])
|
||||
assert.Equal(t, "trixie", config.Versions.Debian["13"])
|
||||
|
||||
// Test defaults
|
||||
assert.Equal(t, "trixie", config.Defaults.DebianVersion)
|
||||
assert.Equal(t, []string{"qcow2"}, config.Defaults.ImageTypes)
|
||||
assert.Equal(t, "./output", config.Defaults.OutputDir)
|
||||
assert.Equal(t, "ext4", config.Defaults.RootfsType)
|
||||
assert.Equal(t, "amd64", config.Defaults.Architecture)
|
||||
|
||||
// Test build settings
|
||||
assert.Equal(t, 2, config.Build.ContainerSizeMultiplier)
|
||||
assert.Equal(t, 10, config.Build.MinRootfsSizeGB)
|
||||
assert.Equal(t, []string{"rw", "console=tty0", "console=ttyS0"}, config.Build.DefaultKernelOptions)
|
||||
|
||||
// Test repositories
|
||||
assert.Len(t, config.Repositories.Debian, 1)
|
||||
|
||||
debianRepo := config.Repositories.Debian["main"]
|
||||
assert.Equal(t, "http://deb.debian.org/debian", debianRepo)
|
||||
|
||||
forgeRepo := config.Repositories.DebianForge
|
||||
assert.Equal(t, "https://git.raines.xyz/api/packages/particle-os/debian", forgeRepo.BaseURL)
|
||||
assert.Equal(t, []string{"trixie", "main"}, forgeRepo.Components)
|
||||
|
||||
// Test cloud configuration
|
||||
assert.Equal(t, "us-east-1", config.Cloud.AWS.DefaultRegion)
|
||||
assert.Equal(t, "debian-bootc-{region}-{timestamp}", config.Cloud.AWS.BucketTemplate)
|
||||
}
|
||||
|
||||
func TestConfigMethods(t *testing.T) {
|
||||
// Create a test config
|
||||
config := &Config{
|
||||
ActiveRegistry: "development",
|
||||
Registries: map[string]RegistryConfig{
|
||||
"development": {
|
||||
BaseURL: "git.raines.xyz",
|
||||
Namespace: "debian",
|
||||
AuthRequired: true,
|
||||
},
|
||||
},
|
||||
Containers: ContainerTemplates{
|
||||
BootcBase: "{registry}/{namespace}/debian-bootc:{version}",
|
||||
BootcBuilder: "{registry}/{namespace}/bootc-image-builder:{tag}",
|
||||
},
|
||||
Versions: VersionMappings{
|
||||
Debian: map[string]string{
|
||||
"stable": "trixie",
|
||||
"testing": "forky",
|
||||
"unstable": "sid",
|
||||
},
|
||||
},
|
||||
Defaults: DefaultSettings{
|
||||
DebianVersion: "stable",
|
||||
ImageTypes: []string{"qcow2"},
|
||||
OutputDir: "./output",
|
||||
RootfsType: "ext4",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
Build: BuildSettings{
|
||||
ContainerSizeMultiplier: 2,
|
||||
MinRootfsSizeGB: 10,
|
||||
DefaultKernelOptions: []string{"rw", "console=tty0"},
|
||||
},
|
||||
Repositories: RepositoryConfig{
|
||||
Debian: map[string]string{
|
||||
"main": "http://deb.debian.org/debian",
|
||||
},
|
||||
DebianForge: struct {
|
||||
BaseURL string `yaml:"base_url"`
|
||||
Components []string `yaml:"components"`
|
||||
}{
|
||||
BaseURL: "https://git.raines.xyz/api/packages/particle-os/debian",
|
||||
Components: []string{"trixie", "main"},
|
||||
},
|
||||
},
|
||||
Cloud: CloudConfig{
|
||||
AWS: struct {
|
||||
DefaultRegion string `yaml:"default_region"`
|
||||
BucketTemplate string `yaml:"bucket_template"`
|
||||
}{
|
||||
DefaultRegion: "us-east-1",
|
||||
BucketTemplate: "debian-bootc-{region}-{timestamp}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test GetActiveRegistry
|
||||
activeReg, err := config.GetActiveRegistry()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "git.raines.xyz", activeReg.BaseURL)
|
||||
|
||||
// Test GetDebianVersion
|
||||
version, err := config.GetDebianVersion("stable")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "trixie", version)
|
||||
|
||||
// Test GetDefaultDebianVersion
|
||||
defaultVersion, err := config.GetDefaultDebianVersion()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "trixie", defaultVersion)
|
||||
|
||||
// Test GetDefaultKernelOptions
|
||||
kernelOptions := config.GetDefaultKernelOptions()
|
||||
assert.Equal(t, []string{"rw", "console=tty0"}, kernelOptions)
|
||||
|
||||
// Test GetContainerSizeMultiplier
|
||||
multiplier := config.GetContainerSizeMultiplier()
|
||||
assert.Equal(t, 2, multiplier)
|
||||
|
||||
// Test GetMinRootfsSizeGB
|
||||
minSize := config.GetMinRootfsSizeGB()
|
||||
assert.Equal(t, 10, minSize)
|
||||
|
||||
// Test GetDebianForgeRepository
|
||||
baseURL, components := config.GetDebianForgeRepository()
|
||||
assert.Equal(t, "https://git.raines.xyz/api/packages/particle-os/debian", baseURL)
|
||||
assert.Equal(t, []string{"trixie", "main"}, components)
|
||||
}
|
||||
|
||||
func TestConfigWithInvalidYAML(t *testing.T) {
|
||||
// Create a temporary config file with invalid YAML
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "invalid.yaml")
|
||||
|
||||
invalidContent := `registries:
|
||||
development:
|
||||
base_url: "git.raines.xyz"
|
||||
namespace: "debian"
|
||||
auth_required: true
|
||||
production:
|
||||
base_url: "docker.io"
|
||||
namespace: "debian"
|
||||
auth_required: false
|
||||
|
||||
active_registry: "development"
|
||||
|
||||
containers:
|
||||
bootc_base: "{registry}/{namespace}/debian-bootc:{version}"
|
||||
bootc_builder: "{registry}/{namespace}/bootc-image-builder:{tag}"
|
||||
|
||||
versions:
|
||||
debian:
|
||||
stable: "trixie"
|
||||
testing: "forky"
|
||||
unstable: "sid"
|
||||
12: "bookworm"
|
||||
13: "trixie"
|
||||
|
||||
defaults:
|
||||
debian_version: "trixie"
|
||||
image_types: ["qcow2"]
|
||||
output_dir: "./output"
|
||||
rootfs_type: "ext4"
|
||||
architecture: "amd64"
|
||||
|
||||
build:
|
||||
container_size_multiplier: 2
|
||||
min_rootfs_size_gb: 10
|
||||
default_kernel_options: ["rw", "console=tty0", "console=ttyS0"]
|
||||
|
||||
repositories:
|
||||
debian:
|
||||
main: "http://deb.debian.org/debian"
|
||||
debian_forge:
|
||||
base_url: "https://git.raines.xyz/api/packages/particle-os/debian"
|
||||
components: ["trixie", "main"]
|
||||
|
||||
cloud:
|
||||
aws:
|
||||
default_region: "us-east-1"
|
||||
bucket_template: "debian-bootc-{region}-{timestamp}"
|
||||
`
|
||||
|
||||
err := os.WriteFile(configPath, []byte(invalidContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Load the config
|
||||
config, err := LoadConfig(configPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
}
|
||||
|
||||
func TestConfigWithMissingFields(t *testing.T) {
|
||||
// Create a minimal config file
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "minimal.yaml")
|
||||
|
||||
minimalContent := `registries:
|
||||
development:
|
||||
base_url: "git.raines.xyz"
|
||||
namespace: "debian"
|
||||
auth_required: true
|
||||
|
||||
active_registry: "development"
|
||||
|
||||
containers:
|
||||
bootc_base: "{registry}/{namespace}/debian-bootc:{version}"
|
||||
bootc_builder: "{registry}/{namespace}/bootc-image-builder:{tag}"
|
||||
|
||||
versions:
|
||||
debian:
|
||||
stable: "trixie"
|
||||
testing: "forky"
|
||||
unstable: "sid"
|
||||
12: "bookworm"
|
||||
13: "trixie"
|
||||
|
||||
defaults:
|
||||
debian_version: "trixie"
|
||||
image_types: ["qcow2"]
|
||||
output_dir: "./output"
|
||||
rootfs_type: "ext4"
|
||||
architecture: "amd64"
|
||||
|
||||
build:
|
||||
container_size_multiplier: 2
|
||||
min_rootfs_size_gb: 10
|
||||
default_kernel_options: ["rw", "console=tty0", "console=ttyS0"]
|
||||
|
||||
repositories:
|
||||
debian:
|
||||
main: "http://deb.debian.org/debian"
|
||||
debian_forge:
|
||||
base_url: "https://git.raines.xyz/api/packages/particle-os/debian"
|
||||
components: ["trixie", "main"]
|
||||
|
||||
cloud:
|
||||
aws:
|
||||
default_region: "us-east-1"
|
||||
bucket_template: "debian-bootc-{region}-{timestamp}"
|
||||
`
|
||||
|
||||
err := os.WriteFile(configPath, []byte(minimalContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Load the config
|
||||
config, err := LoadConfig(configPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
|
||||
// Test that missing fields have default values
|
||||
assert.Equal(t, "development", config.ActiveRegistry)
|
||||
assert.Len(t, config.Registries, 1)
|
||||
assert.Equal(t, "trixie", config.Versions.Debian["stable"])
|
||||
assert.Equal(t, "trixie", config.Defaults.DebianVersion)
|
||||
assert.Equal(t, []string{"qcow2"}, config.Defaults.ImageTypes)
|
||||
assert.Equal(t, "./output", config.Defaults.OutputDir)
|
||||
assert.Equal(t, "ext4", config.Defaults.RootfsType)
|
||||
assert.Equal(t, "amd64", config.Defaults.Architecture)
|
||||
assert.Equal(t, 2, config.Build.ContainerSizeMultiplier)
|
||||
assert.Equal(t, 10, config.Build.MinRootfsSizeGB)
|
||||
assert.Equal(t, []string{"rw", "console=tty0", "console=ttyS0"}, config.Build.DefaultKernelOptions)
|
||||
assert.Len(t, config.Repositories.Debian, 1)
|
||||
assert.Equal(t, "us-east-1", config.Cloud.AWS.DefaultRegion)
|
||||
}
|
||||
140
bib/internal/container/container.go
Normal file
140
bib/internal/container/container.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"debian-bootc-image-builder/internal/apt"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Container represents a container interface for APT operations
|
||||
type Container interface {
|
||||
Root() string
|
||||
InitAPT() error
|
||||
NewContainerSolver(cacheRoot, arch string, sourceInfo interface{}) (*apt.Solver, error)
|
||||
DefaultRootfsType() (string, error)
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// PodmanContainer implements the Container interface using Podman
|
||||
type PodmanContainer struct {
|
||||
root string
|
||||
// Add other fields as needed
|
||||
}
|
||||
|
||||
// New creates a new PodmanContainer instance
|
||||
func New(imageRef string) (*PodmanContainer, error) {
|
||||
// This is a simplified implementation
|
||||
// In a real implementation, this would:
|
||||
// 1. Pull the container image if needed
|
||||
// 2. Create a container instance
|
||||
// 3. Start the container
|
||||
// 4. Mount the container filesystem
|
||||
|
||||
// For now, we'll simulate this with a temporary directory
|
||||
root := filepath.Join("/tmp", "container-root", filepath.Base(imageRef))
|
||||
if err := os.MkdirAll(root, 0755); err != nil {
|
||||
return nil, fmt.Errorf("cannot create container root: %w", err)
|
||||
}
|
||||
|
||||
return &PodmanContainer{
|
||||
root: root,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Root returns the container's root filesystem path
|
||||
func (c *PodmanContainer) Root() string {
|
||||
return c.root
|
||||
}
|
||||
|
||||
// InitAPT initializes APT configuration in the container
|
||||
func (c *PodmanContainer) InitAPT() error {
|
||||
logrus.Debugf("Initializing APT in container root: %s", c.root)
|
||||
|
||||
// Create necessary APT directories
|
||||
aptDirs := []string{
|
||||
filepath.Join(c.root, "etc", "apt"),
|
||||
filepath.Join(c.root, "etc", "apt", "sources.list.d"),
|
||||
filepath.Join(c.root, "var", "lib", "apt"),
|
||||
filepath.Join(c.root, "var", "cache", "apt"),
|
||||
}
|
||||
|
||||
for _, dir := range aptDirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("cannot create APT directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create basic APT configuration
|
||||
aptConf := filepath.Join(c.root, "etc", "apt", "apt.conf")
|
||||
conf := `APT::Architecture "amd64";
|
||||
APT::Get::Assume-Yes "true";
|
||||
APT::Get::AllowUnauthenticated "true";
|
||||
`
|
||||
|
||||
if err := os.WriteFile(aptConf, []byte(conf), 0644); err != nil {
|
||||
return fmt.Errorf("cannot write APT configuration: %w", err)
|
||||
}
|
||||
|
||||
// Create sources.list for Debian repositories
|
||||
sourcesList := filepath.Join(c.root, "etc", "apt", "sources.list")
|
||||
sources := `deb http://deb.debian.org/debian trixie main
|
||||
deb http://security.debian.org/debian-security trixie-security main
|
||||
deb http://deb.debian.org/debian trixie-updates main
|
||||
`
|
||||
|
||||
if err := os.WriteFile(sourcesList, []byte(sources), 0644); err != nil {
|
||||
return fmt.Errorf("cannot write sources.list: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewContainerSolver creates a new APT solver for this container
|
||||
func (c *PodmanContainer) NewContainerSolver(cacheRoot, arch string, sourceInfo interface{}) (*apt.Solver, error) {
|
||||
// Define default repositories for Debian
|
||||
repos := []apt.RepoConfig{
|
||||
{
|
||||
BaseURL: "http://deb.debian.org/debian",
|
||||
Components: []string{"trixie", "main"},
|
||||
Arch: arch,
|
||||
Priority: 500,
|
||||
},
|
||||
{
|
||||
BaseURL: "http://security.debian.org/debian-security",
|
||||
Components: []string{"trixie-security", "main"},
|
||||
Arch: arch,
|
||||
Priority: 1000,
|
||||
},
|
||||
{
|
||||
BaseURL: "http://deb.debian.org/debian",
|
||||
Components: []string{"trixie-updates", "main"},
|
||||
Arch: arch,
|
||||
Priority: 500,
|
||||
},
|
||||
{
|
||||
BaseURL: "https://git.raines.xyz/api/packages/particle-os/debian",
|
||||
Components: []string{"trixie", "main"},
|
||||
Arch: arch,
|
||||
Priority: 400,
|
||||
},
|
||||
}
|
||||
|
||||
return apt.NewSolver(cacheRoot, arch, repos)
|
||||
}
|
||||
|
||||
// DefaultRootfsType returns the default root filesystem type for Debian
|
||||
func (c *PodmanContainer) DefaultRootfsType() (string, error) {
|
||||
// Debian typically uses ext4 as the default filesystem
|
||||
return "ext4", nil
|
||||
}
|
||||
|
||||
// Stop stops the container
|
||||
func (c *PodmanContainer) Stop() error {
|
||||
logrus.Debugf("Stopping container with root: %s", c.root)
|
||||
// In a real implementation, this would stop the container
|
||||
// For now, we'll just log it
|
||||
return nil
|
||||
}
|
||||
170
bib/internal/container/container_test.go
Normal file
170
bib/internal/container/container_test.go
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPodmanContainer(t *testing.T) {
|
||||
// Test creating a new PodmanContainer
|
||||
container := NewPodmanContainer()
|
||||
assert.NotNil(t, container)
|
||||
}
|
||||
|
||||
func TestPodmanContainerInitAPT(t *testing.T) {
|
||||
container := NewPodmanContainer()
|
||||
|
||||
// Test InitAPT (should not error)
|
||||
err := container.InitAPT()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPodmanContainerNewContainerSolver(t *testing.T) {
|
||||
container := NewPodmanContainer()
|
||||
|
||||
// Test creating a new container solver
|
||||
solver := container.NewContainerSolver()
|
||||
assert.NotNil(t, solver)
|
||||
|
||||
// Test that the solver has repositories configured
|
||||
assert.NotNil(t, solver.repos)
|
||||
assert.Len(t, solver.repos, 2)
|
||||
|
||||
// Check that debian repository is configured
|
||||
var debianRepo, forgeRepo bool
|
||||
for _, repo := range solver.repos {
|
||||
if repo.Name == "debian" {
|
||||
debianRepo = true
|
||||
assert.Equal(t, "http://deb.debian.org/debian", repo.BaseURL)
|
||||
assert.True(t, repo.Enabled)
|
||||
assert.Equal(t, 500, repo.Priority)
|
||||
}
|
||||
if repo.Name == "debian-forge" {
|
||||
forgeRepo = true
|
||||
assert.Equal(t, "https://git.raines.xyz/api/packages/particle-os/debian", repo.BaseURL)
|
||||
assert.True(t, repo.Enabled)
|
||||
assert.Equal(t, 400, repo.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, debianRepo, "debian repository should be configured")
|
||||
assert.True(t, forgeRepo, "debian-forge repository should be configured")
|
||||
}
|
||||
|
||||
func TestPodmanContainerDefaultRootfsType(t *testing.T) {
|
||||
container := NewPodmanContainer()
|
||||
|
||||
// Test getting default rootfs type
|
||||
rootfsType := container.DefaultRootfsType()
|
||||
assert.Equal(t, "ext4", rootfsType)
|
||||
}
|
||||
|
||||
func TestContainerInterface(t *testing.T) {
|
||||
// Test that PodmanContainer implements the Container interface
|
||||
var _ Container = NewPodmanContainer()
|
||||
}
|
||||
|
||||
func TestPodmanContainerSolverIntegration(t *testing.T) {
|
||||
container := NewPodmanContainer()
|
||||
solver := container.NewContainerSolver()
|
||||
|
||||
// Test that the solver can be used for depsolving
|
||||
packages := []string{"base-files", "systemd", "linux-image-amd64"}
|
||||
result, err := solver.Depsolve(packages, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, packages, result.Packages)
|
||||
}
|
||||
|
||||
func TestPodmanContainerRepositoryConfiguration(t *testing.T) {
|
||||
container := NewPodmanContainer()
|
||||
solver := container.NewContainerSolver()
|
||||
|
||||
// Test repository configuration
|
||||
repos := solver.repos
|
||||
assert.Len(t, repos, 2)
|
||||
|
||||
// Test debian repository
|
||||
debianRepo := repos[0]
|
||||
assert.Equal(t, "debian", debianRepo.Name)
|
||||
assert.Equal(t, "http://deb.debian.org/debian", debianRepo.BaseURL)
|
||||
assert.True(t, debianRepo.Enabled)
|
||||
assert.Equal(t, 500, debianRepo.Priority)
|
||||
|
||||
// Test debian-forge repository
|
||||
forgeRepo := repos[1]
|
||||
assert.Equal(t, "debian-forge", forgeRepo.Name)
|
||||
assert.Equal(t, "https://git.raines.xyz/api/packages/particle-os/debian", forgeRepo.BaseURL)
|
||||
assert.True(t, forgeRepo.Enabled)
|
||||
assert.Equal(t, 400, forgeRepo.Priority)
|
||||
}
|
||||
|
||||
func TestPodmanContainerMultipleInstances(t *testing.T) {
|
||||
// Test creating multiple container instances
|
||||
container1 := NewPodmanContainer()
|
||||
container2 := NewPodmanContainer()
|
||||
|
||||
assert.NotNil(t, container1)
|
||||
assert.NotNil(t, container2)
|
||||
|
||||
// Test that they are independent
|
||||
solver1 := container1.NewContainerSolver()
|
||||
solver2 := container2.NewContainerSolver()
|
||||
|
||||
assert.NotNil(t, solver1)
|
||||
assert.NotNil(t, solver2)
|
||||
|
||||
// Test that both solvers work independently
|
||||
packages := []string{"base-files", "systemd"}
|
||||
result1, err1 := solver1.Depsolve(packages, 0)
|
||||
result2, err2 := solver2.Depsolve(packages, 0)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, result1.Packages, result2.Packages)
|
||||
}
|
||||
|
||||
func TestPodmanContainerErrorHandling(t *testing.T) {
|
||||
container := NewPodmanContainer()
|
||||
|
||||
// Test that InitAPT doesn't fail
|
||||
err := container.InitAPT()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test that NewContainerSolver always returns a valid solver
|
||||
solver := container.NewContainerSolver()
|
||||
assert.NotNil(t, solver)
|
||||
|
||||
// Test that DefaultRootfsType always returns a valid type
|
||||
rootfsType := container.DefaultRootfsType()
|
||||
assert.NotEmpty(t, rootfsType)
|
||||
assert.Equal(t, "ext4", rootfsType)
|
||||
}
|
||||
|
||||
func TestPodmanContainerSolverWithEmptyPackages(t *testing.T) {
|
||||
container := NewPodmanContainer()
|
||||
solver := container.NewContainerSolver()
|
||||
|
||||
// Test depsolving with empty package list
|
||||
result, err := solver.Depsolve([]string{}, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Empty(t, result.Packages)
|
||||
}
|
||||
|
||||
func TestPodmanContainerSolverWithLargePackageList(t *testing.T) {
|
||||
container := NewPodmanContainer()
|
||||
solver := container.NewContainerSolver()
|
||||
|
||||
// Test depsolving with a large package list
|
||||
packages := make([]string, 1000)
|
||||
for i := 0; i < 1000; i++ {
|
||||
packages[i] = "package-" + string(rune('a'+i%26))
|
||||
}
|
||||
|
||||
result, err := solver.Depsolve(packages, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, packages, result.Packages)
|
||||
}
|
||||
99
bib/internal/distrodef/distrodef.go
Normal file
99
bib/internal/distrodef/distrodef.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package distrodef
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
// ImageDef is a structure containing extra information needed to build an image that cannot be extracted
|
||||
// from the container image itself. Currently, this is only the list of packages needed for the installer
|
||||
// ISO.
|
||||
type ImageDef struct {
|
||||
Packages []string `yaml:"packages"`
|
||||
}
|
||||
|
||||
func findDistroDef(defDirs []string, distro, wantedVerStr string) (string, error) {
|
||||
var bestFuzzyMatch string
|
||||
|
||||
bestFuzzyVer := &version.Version{}
|
||||
wantedVer, err := version.NewVersion(wantedVerStr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot parse wanted version string: %w", err)
|
||||
}
|
||||
|
||||
for _, defDir := range defDirs {
|
||||
// exact match
|
||||
matches, err := filepath.Glob(filepath.Join(defDir, fmt.Sprintf("%s-%s.yaml", distro, wantedVerStr)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(matches) == 1 {
|
||||
return matches[0], nil
|
||||
}
|
||||
|
||||
// fuzzy match
|
||||
matches, err = filepath.Glob(filepath.Join(defDir, fmt.Sprintf("%s-[0-9]*.yaml", distro)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, m := range matches {
|
||||
baseNoExt := strings.TrimSuffix(filepath.Base(m), ".yaml")
|
||||
haveVerStr := strings.SplitN(baseNoExt, "-", 2)[1]
|
||||
// this should never error (because of the glob above) but be defensive
|
||||
haveVer, err := version.NewVersion(haveVerStr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot parse distro version from %q: %w", m, err)
|
||||
}
|
||||
if wantedVer.Compare(haveVer) >= 0 && haveVer.Compare(bestFuzzyVer) > 0 {
|
||||
bestFuzzyVer = haveVer
|
||||
bestFuzzyMatch = m
|
||||
}
|
||||
}
|
||||
}
|
||||
if bestFuzzyMatch == "" {
|
||||
return "", fmt.Errorf("could not find def file for distro %s-%s", distro, wantedVerStr)
|
||||
}
|
||||
|
||||
return bestFuzzyMatch, nil
|
||||
}
|
||||
|
||||
func loadFile(defDirs []string, distro, ver string) ([]byte, error) {
|
||||
defPath, err := findDistroDef(defDirs, distro, ver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(defPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read def file %s for distro %s-%s: %v", defPath, distro, ver, err)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Loads a definition file for a given distro and image type
|
||||
func LoadImageDef(defDirs []string, distro, ver, it string) (*ImageDef, error) {
|
||||
data, err := loadFile(defDirs, distro, ver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var defs map[string]ImageDef
|
||||
if err := yaml.Unmarshal(data, &defs); err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal def file for distro %s: %v", distro, err)
|
||||
}
|
||||
|
||||
d, ok := defs[it]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not find def for distro %s and image type %s, available types: %s", distro, it, strings.Join(maps.Keys(defs), ", "))
|
||||
}
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
256
bib/internal/distrodef/distrodef_test.go
Normal file
256
bib/internal/distrodef/distrodef_test.go
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
package distrodef
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testDefLocation = "../data/defs"
|
||||
|
||||
func TestLoadSimple(t *testing.T) {
|
||||
def, err := LoadImageDef([]string{testDefLocation}, "debian", "trixie", "qcow2")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
}
|
||||
|
||||
func TestLoadFuzzy(t *testing.T) {
|
||||
def, err := LoadImageDef([]string{testDefLocation}, "debian", "trixie", "qcow2")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
}
|
||||
|
||||
func TestLoadUnhappy(t *testing.T) {
|
||||
_, err := LoadImageDef([]string{testDefLocation}, "ubuntu", "22.04", "qcow2")
|
||||
assert.ErrorContains(t, err, "could not find def file for distro ubuntu-22.04")
|
||||
|
||||
_, err = LoadImageDef([]string{testDefLocation}, "debian", "0", "qcow2")
|
||||
assert.ErrorContains(t, err, "could not find def file for distro debian-0")
|
||||
|
||||
_, err = LoadImageDef([]string{testDefLocation}, "debian", "trixie", "unsupported-type")
|
||||
assert.ErrorContains(t, err, "could not find def for distro debian and image type unsupported-type")
|
||||
|
||||
_, err = LoadImageDef([]string{testDefLocation}, "debian", "xxx", "qcow2")
|
||||
assert.ErrorContains(t, err, `cannot parse wanted version string: `)
|
||||
}
|
||||
|
||||
const fakeDefFileContent = `qcow2:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
ami:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- cloud-guest-utils
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
vmdk:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- grub-common
|
||||
- open-vm-tools
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
debian-installer:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- debian-installer
|
||||
- bootc
|
||||
- apt-ostree
|
||||
|
||||
calamares:
|
||||
packages:
|
||||
- base-files
|
||||
- systemd
|
||||
- linux-image-amd64
|
||||
- calamares
|
||||
- bootc
|
||||
- apt-ostree
|
||||
`
|
||||
|
||||
func makeFakeDistrodefRoot(t *testing.T, defFiles []string) (searchPaths []string) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
for _, defFile := range defFiles {
|
||||
p := filepath.Join(tmp, defFile)
|
||||
err := os.MkdirAll(filepath.Dir(p), 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(p, []byte(fakeDefFileContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
if !slices.Contains(searchPaths, filepath.Dir(p)) {
|
||||
searchPaths = append(searchPaths, filepath.Dir(p))
|
||||
}
|
||||
}
|
||||
|
||||
return searchPaths
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiDirs(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/debian-12.yaml",
|
||||
"b/debian-13.yaml",
|
||||
"c/debian-13.yaml",
|
||||
})
|
||||
assert.Equal(t, 3, len(defDirs))
|
||||
|
||||
def, err := findDistroDef(defDirs, "debian", "13")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "b/debian-13.yaml"))
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiDirsIgnoreENOENT(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/debian-13.yaml",
|
||||
})
|
||||
defDirs = append([]string{"/no/such/path"}, defDirs...)
|
||||
|
||||
def, err := findDistroDef(defDirs, "debian", "13")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "a/debian-13.yaml"))
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiFuzzy(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/debian-12.yaml",
|
||||
"b/debian-13.yaml",
|
||||
"b/b/debian-14.yaml",
|
||||
"c/debian-13.yaml",
|
||||
})
|
||||
// no debian-99, pick the closest
|
||||
def, err := findDistroDef(defDirs, "debian", "99")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "b/b/debian-14.yaml"))
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiFuzzyMinorReleases(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/debian-12.1.yaml",
|
||||
"b/debian-11.yaml",
|
||||
"c/debian-13.1.yaml",
|
||||
"d/debian-13.1.1.yaml",
|
||||
"b/b/debian-13.10.yaml",
|
||||
})
|
||||
def, err := findDistroDef(defDirs, "debian", "13.11")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "b/b/debian-13.10.yaml"), def)
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiFuzzyMinorReleasesIsZero(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/debian-13.yaml",
|
||||
"a/debian-14.yaml",
|
||||
})
|
||||
def, err := findDistroDef(defDirs, "debian", "14.0")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "a/debian-14.yaml"), def)
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiFuzzyError(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/debian-13.yaml",
|
||||
})
|
||||
// the best version we have is newer than what is requested, this
|
||||
// is an error
|
||||
_, err := findDistroDef(defDirs, "debian", "10")
|
||||
assert.ErrorContains(t, err, "could not find def file for distro debian-10")
|
||||
}
|
||||
|
||||
func TestFindDistroDefBadNumberIgnoresBadFiles(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/debian-NaN.yaml",
|
||||
})
|
||||
_, err := findDistroDef(defDirs, "debian", "13")
|
||||
assert.ErrorContains(t, err, "could not find def file for distro debian-13")
|
||||
}
|
||||
|
||||
func TestFindDistroDefCornerCases(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/debian-.yaml",
|
||||
"b/debian-1.yaml",
|
||||
"c/debian.yaml",
|
||||
})
|
||||
def, err := findDistroDef(defDirs, "debian", "2")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "b/debian-1.yaml"))
|
||||
}
|
||||
|
||||
func TestLoadImageDefWithDifferentImageTypes(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/debian-13.yaml",
|
||||
})
|
||||
|
||||
// Test qcow2 image type
|
||||
def, err := LoadImageDef(defDirs, "debian", "trixie", "qcow2")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
assert.Contains(t, def.Packages, "base-files")
|
||||
assert.Contains(t, def.Packages, "systemd")
|
||||
assert.Contains(t, def.Packages, "bootc")
|
||||
|
||||
// Test ami image type
|
||||
def, err = LoadImageDef(defDirs, "debian", "13", "ami")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
assert.Contains(t, def.Packages, "cloud-guest-utils")
|
||||
|
||||
// Test vmdk image type
|
||||
def, err = LoadImageDef(defDirs, "debian", "13", "vmdk")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
assert.Contains(t, def.Packages, "open-vm-tools")
|
||||
|
||||
// Test debian-installer image type
|
||||
def, err = LoadImageDef(defDirs, "debian", "13", "debian-installer")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
assert.Contains(t, def.Packages, "debian-installer")
|
||||
|
||||
// Test calamares image type
|
||||
def, err = LoadImageDef(defDirs, "debian", "13", "calamares")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
assert.Contains(t, def.Packages, "calamares")
|
||||
}
|
||||
|
||||
func TestLoadImageDefWithCodenames(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/debian-trixie.yaml",
|
||||
"b/debian-bookworm.yaml",
|
||||
"c/debian-sid.yaml",
|
||||
})
|
||||
|
||||
// Test with codename
|
||||
def, err := LoadImageDef(defDirs, "debian", "trixie", "qcow2")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
|
||||
// Test with numeric version
|
||||
def, err = LoadImageDef(defDirs, "debian", "13", "qcow2")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
|
||||
// Test with stable codename
|
||||
def, err = LoadImageDef(defDirs, "debian", "stable", "qcow2")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
}
|
||||
87
bib/internal/imagetypes/imagetypes.go
Normal file
87
bib/internal/imagetypes/imagetypes.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package imagetypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type imageType struct {
|
||||
Export string
|
||||
ISO bool
|
||||
}
|
||||
|
||||
var supportedImageTypes = map[string]imageType{
|
||||
"ami": imageType{Export: "image"},
|
||||
"qcow2": imageType{Export: "qcow2"},
|
||||
"raw": imageType{Export: "image"},
|
||||
"vmdk": imageType{Export: "vmdk"},
|
||||
"debian-installer": imageType{Export: "bootiso", ISO: true},
|
||||
"calamares": imageType{Export: "bootiso", ISO: true},
|
||||
}
|
||||
|
||||
// Available() returns a comma-separated list of supported image types
|
||||
func Available() string {
|
||||
keys := make([]string, 0, len(supportedImageTypes))
|
||||
for k := range supportedImageTypes {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
return strings.Join(keys, ", ")
|
||||
}
|
||||
|
||||
// ImageTypes contains the image types that are requested to be build
|
||||
type ImageTypes []string
|
||||
|
||||
// New takes image type names as input and returns a ImageTypes
|
||||
// object or an error if the image types are invalid.
|
||||
//
|
||||
// Note that it is not possible to mix iso/disk types
|
||||
func New(imageTypeNames ...string) (ImageTypes, error) {
|
||||
if len(imageTypeNames) == 0 {
|
||||
return nil, fmt.Errorf("cannot use an empty array as a build request")
|
||||
}
|
||||
|
||||
var ISOs, disks int
|
||||
for _, name := range imageTypeNames {
|
||||
imgType, ok := supportedImageTypes[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported image type %q, valid types are %s", name, Available())
|
||||
}
|
||||
if imgType.ISO {
|
||||
ISOs++
|
||||
} else {
|
||||
disks++
|
||||
}
|
||||
}
|
||||
if ISOs > 0 && disks > 0 {
|
||||
return nil, fmt.Errorf("cannot mix ISO/disk images in request %v", imageTypeNames)
|
||||
}
|
||||
|
||||
return ImageTypes(imageTypeNames), nil
|
||||
}
|
||||
|
||||
// Exports returns the list of osbuild manifest exports require to build
|
||||
// all images types.
|
||||
func (it ImageTypes) Exports() []string {
|
||||
exports := make([]string, 0, len(it))
|
||||
// XXX: this assumes a valid ImagTypes object
|
||||
for _, name := range it {
|
||||
imgType := supportedImageTypes[name]
|
||||
if !slices.Contains(exports, imgType.Export) {
|
||||
exports = append(exports, imgType.Export)
|
||||
}
|
||||
}
|
||||
|
||||
return exports
|
||||
}
|
||||
|
||||
// BuildsISO returns true if the image types build an ISO, note that
|
||||
// it is not possible to mix disk/iso.
|
||||
func (it ImageTypes) BuildsISO() bool {
|
||||
// XXX: this assumes a valid ImagTypes object
|
||||
return supportedImageTypes[it[0]].ISO
|
||||
}
|
||||
|
||||
133
bib/internal/imagetypes/imagetypes_test.go
Normal file
133
bib/internal/imagetypes/imagetypes_test.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package imagetypes_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"debian-bootc-image-builder/internal/imagetypes"
|
||||
)
|
||||
|
||||
type testCase struct {
|
||||
imageTypes []string
|
||||
expectedExports []string
|
||||
expectISO bool
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
func TestImageTypes(t *testing.T) {
|
||||
testCases := map[string]testCase{
|
||||
"qcow-disk": {
|
||||
imageTypes: []string{"qcow2"},
|
||||
expectedExports: []string{"qcow2"},
|
||||
expectISO: false,
|
||||
},
|
||||
"ami-disk": {
|
||||
imageTypes: []string{"ami"},
|
||||
expectedExports: []string{"image"},
|
||||
expectISO: false,
|
||||
},
|
||||
"vmdk-disk": {
|
||||
imageTypes: []string{"vmdk"},
|
||||
expectedExports: []string{"vmdk"},
|
||||
expectISO: false,
|
||||
},
|
||||
"qcow-ami-disk": {
|
||||
imageTypes: []string{"qcow2", "ami"},
|
||||
expectedExports: []string{"qcow2", "image"},
|
||||
expectISO: false,
|
||||
},
|
||||
"ami-raw": {
|
||||
imageTypes: []string{"ami", "raw"},
|
||||
expectedExports: []string{"image"},
|
||||
expectISO: false,
|
||||
},
|
||||
"all-disk": {
|
||||
imageTypes: []string{"ami", "raw", "vmdk", "qcow2"},
|
||||
expectedExports: []string{"image", "vmdk", "qcow2"},
|
||||
expectISO: false,
|
||||
},
|
||||
"debian-installer": {
|
||||
imageTypes: []string{"debian-installer"},
|
||||
expectedExports: []string{"bootiso"},
|
||||
expectISO: true,
|
||||
},
|
||||
"calamares": {
|
||||
imageTypes: []string{"calamares"},
|
||||
expectedExports: []string{"bootiso"},
|
||||
expectISO: true,
|
||||
},
|
||||
"bad-mix": {
|
||||
imageTypes: []string{"vmdk", "debian-installer"},
|
||||
expectedErr: errors.New("cannot mix ISO/disk images in request [vmdk debian-installer]"),
|
||||
},
|
||||
"bad-mix-part-2": {
|
||||
imageTypes: []string{"ami", "calamares"},
|
||||
expectedErr: errors.New("cannot mix ISO/disk images in request [ami calamares]"),
|
||||
},
|
||||
"bad-image-type": {
|
||||
imageTypes: []string{"bad"},
|
||||
expectedErr: errors.New(`unsupported image type "bad", valid types are ami, calamares, debian-installer, qcow2, raw, vmdk`),
|
||||
},
|
||||
"bad-in-good": {
|
||||
imageTypes: []string{"ami", "raw", "vmdk", "qcow2", "something-else-what-is-this"},
|
||||
expectedErr: errors.New(`unsupported image type "something-else-what-is-this", valid types are ami, calamares, debian-installer, qcow2, raw, vmdk`),
|
||||
},
|
||||
"all-bad": {
|
||||
imageTypes: []string{"bad1", "bad2", "bad3", "bad4", "bad5", "bad42"},
|
||||
expectedErr: errors.New(`unsupported image type "bad1", valid types are ami, calamares, debian-installer, qcow2, raw, vmdk`),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
it, err := imagetypes.New(tc.imageTypes...)
|
||||
if tc.expectedErr != nil {
|
||||
assert.Equal(t, err, tc.expectedErr)
|
||||
} else {
|
||||
assert.Equal(t, it.Exports(), tc.expectedExports)
|
||||
assert.Equal(t, it.BuildsISO(), tc.expectISO)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageTypesValidation(t *testing.T) {
|
||||
// Test empty image types
|
||||
_, err := imagetypes.New()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot use an empty array as a build request")
|
||||
|
||||
// Test valid single image type
|
||||
it, err := imagetypes.New("qcow2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"qcow2"}, it.Exports())
|
||||
assert.False(t, it.BuildsISO())
|
||||
|
||||
// Test valid multiple image types
|
||||
it, err = imagetypes.New("qcow2", "ami", "vmdk")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"qcow2", "image", "vmdk"}, it.Exports())
|
||||
assert.False(t, it.BuildsISO())
|
||||
|
||||
// Test ISO image type
|
||||
it, err = imagetypes.New("debian-installer")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"bootiso"}, it.Exports())
|
||||
assert.True(t, it.BuildsISO())
|
||||
}
|
||||
|
||||
func TestImageTypesSupportedTypes(t *testing.T) {
|
||||
// Test all supported image types
|
||||
supportedTypes := []string{"ami", "calamares", "debian-installer", "qcow2", "raw", "vmdk"}
|
||||
|
||||
for _, imageType := range supportedTypes {
|
||||
t.Run(imageType, func(t *testing.T) {
|
||||
it, err := imagetypes.New(imageType)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, it)
|
||||
})
|
||||
}
|
||||
}
|
||||
190
bib/internal/ux/errors.go
Normal file
190
bib/internal/ux/errors.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
package ux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrorType represents different categories of errors
|
||||
type ErrorType string
|
||||
|
||||
const (
|
||||
ErrorTypeConfig ErrorType = "configuration"
|
||||
ErrorTypePackage ErrorType = "package_management"
|
||||
ErrorTypeContainer ErrorType = "container"
|
||||
ErrorTypeManifest ErrorType = "manifest_generation"
|
||||
ErrorTypeFilesystem ErrorType = "filesystem"
|
||||
ErrorTypeNetwork ErrorType = "network"
|
||||
ErrorTypePermission ErrorType = "permission"
|
||||
ErrorTypeValidation ErrorType = "validation"
|
||||
ErrorTypeDependency ErrorType = "dependency"
|
||||
ErrorTypeBuild ErrorType = "build"
|
||||
)
|
||||
|
||||
// UserError represents a user-friendly error with context and suggestions
|
||||
type UserError struct {
|
||||
Type ErrorType
|
||||
Message string
|
||||
Context string
|
||||
Suggestion string
|
||||
OriginalErr error
|
||||
HelpURL string
|
||||
}
|
||||
|
||||
func (e *UserError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// NewUserError creates a new user-friendly error
|
||||
func NewUserError(errType ErrorType, message, context, suggestion string, originalErr error) *UserError {
|
||||
return &UserError{
|
||||
Type: errType,
|
||||
Message: message,
|
||||
Context: context,
|
||||
Suggestion: suggestion,
|
||||
OriginalErr: originalErr,
|
||||
}
|
||||
}
|
||||
|
||||
// WithHelpURL adds a help URL to the error
|
||||
func (e *UserError) WithHelpURL(url string) *UserError {
|
||||
e.HelpURL = url
|
||||
return e
|
||||
}
|
||||
|
||||
// FormatError formats an error for user display
|
||||
func FormatError(err error) string {
|
||||
if userErr, ok := err.(*UserError); ok {
|
||||
return formatUserError(userErr)
|
||||
}
|
||||
return formatGenericError(err)
|
||||
}
|
||||
|
||||
func formatUserError(err *UserError) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("❌ %s Error: %s\n", strings.Title(string(err.Type)), err.Message))
|
||||
|
||||
if err.Context != "" {
|
||||
sb.WriteString(fmt.Sprintf(" Context: %s\n", err.Context))
|
||||
}
|
||||
|
||||
if err.Suggestion != "" {
|
||||
sb.WriteString(fmt.Sprintf(" 💡 Suggestion: %s\n", err.Suggestion))
|
||||
}
|
||||
|
||||
if err.HelpURL != "" {
|
||||
sb.WriteString(fmt.Sprintf(" 📖 Help: %s\n", err.HelpURL))
|
||||
}
|
||||
|
||||
if err.OriginalErr != nil {
|
||||
sb.WriteString(fmt.Sprintf(" 🔍 Technical details: %v\n", err.OriginalErr))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func formatGenericError(err error) string {
|
||||
return fmt.Sprintf("❌ Error: %v\n", err)
|
||||
}
|
||||
|
||||
// Common error constructors for Debian-specific issues
|
||||
|
||||
// ConfigError creates a configuration-related error
|
||||
func ConfigError(message string, originalErr error) *UserError {
|
||||
return NewUserError(
|
||||
ErrorTypeConfig,
|
||||
message,
|
||||
"Configuration file or settings issue",
|
||||
"Check your .config/registry.yaml file and ensure all required fields are present",
|
||||
originalErr,
|
||||
).WithHelpURL("https://github.com/debian-bootc-image-builder/docs/configuration")
|
||||
}
|
||||
|
||||
// PackageError creates a package management error
|
||||
func PackageError(message string, originalErr error) *UserError {
|
||||
return NewUserError(
|
||||
ErrorTypePackage,
|
||||
message,
|
||||
"APT package resolution or installation issue",
|
||||
"Ensure your system has apt-cache available and repositories are properly configured",
|
||||
originalErr,
|
||||
).WithHelpURL("https://github.com/debian-bootc-image-builder/docs/package-management")
|
||||
}
|
||||
|
||||
// ContainerError creates a container-related error
|
||||
func ContainerError(message string, originalErr error) *UserError {
|
||||
return NewUserError(
|
||||
ErrorTypeContainer,
|
||||
message,
|
||||
"Container image or runtime issue",
|
||||
"Ensure the container image exists and podman/docker is properly configured",
|
||||
originalErr,
|
||||
).WithHelpURL("https://github.com/debian-bootc-image-builder/docs/containers")
|
||||
}
|
||||
|
||||
// ManifestError creates a manifest generation error
|
||||
func ManifestError(message string, originalErr error) *UserError {
|
||||
return NewUserError(
|
||||
ErrorTypeManifest,
|
||||
message,
|
||||
"OSBuild manifest generation issue",
|
||||
"Check your package definitions and ensure all required packages are available",
|
||||
originalErr,
|
||||
).WithHelpURL("https://github.com/debian-bootc-image-builder/docs/manifest-generation")
|
||||
}
|
||||
|
||||
// FilesystemError creates a filesystem-related error
|
||||
func FilesystemError(message string, originalErr error) *UserError {
|
||||
return NewUserError(
|
||||
ErrorTypeFilesystem,
|
||||
message,
|
||||
"Filesystem or disk operation issue",
|
||||
"Check available disk space and filesystem permissions",
|
||||
originalErr,
|
||||
).WithHelpURL("https://github.com/debian-bootc-image-builder/docs/filesystem")
|
||||
}
|
||||
|
||||
// PermissionError creates a permission-related error
|
||||
func PermissionError(message string, originalErr error) *UserError {
|
||||
return NewUserError(
|
||||
ErrorTypePermission,
|
||||
message,
|
||||
"Insufficient permissions for operation",
|
||||
"Run with appropriate permissions or check file/directory ownership",
|
||||
originalErr,
|
||||
).WithHelpURL("https://github.com/debian-bootc-image-builder/docs/permissions")
|
||||
}
|
||||
|
||||
// ValidationError creates a validation error
|
||||
func ValidationError(message string, originalErr error) *UserError {
|
||||
return NewUserError(
|
||||
ErrorTypeValidation,
|
||||
message,
|
||||
"Input validation failed",
|
||||
"Check your command line arguments and configuration values",
|
||||
originalErr,
|
||||
).WithHelpURL("https://github.com/debian-bootc-image-builder/docs/validation")
|
||||
}
|
||||
|
||||
// DependencyError creates a dependency error
|
||||
func DependencyError(message string, originalErr error) *UserError {
|
||||
return NewUserError(
|
||||
ErrorTypeDependency,
|
||||
message,
|
||||
"Missing or incompatible dependency",
|
||||
"Install required dependencies or check version compatibility",
|
||||
originalErr,
|
||||
).WithHelpURL("https://github.com/debian-bootc-image-builder/docs/dependencies")
|
||||
}
|
||||
|
||||
// BuildError creates a build process error
|
||||
func BuildError(message string, originalErr error) *UserError {
|
||||
return NewUserError(
|
||||
ErrorTypeBuild,
|
||||
message,
|
||||
"Image build process failed",
|
||||
"Check build logs and ensure all prerequisites are met",
|
||||
originalErr,
|
||||
).WithHelpURL("https://github.com/debian-bootc-image-builder/docs/build-process")
|
||||
}
|
||||
208
bib/internal/ux/progress.go
Normal file
208
bib/internal/ux/progress.go
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
package ux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProgressReporter handles progress reporting for long operations
|
||||
type ProgressReporter struct {
|
||||
writer io.Writer
|
||||
verbose bool
|
||||
startTime time.Time
|
||||
steps []ProgressStep
|
||||
current int
|
||||
}
|
||||
|
||||
// ProgressStep represents a step in a long operation
|
||||
type ProgressStep struct {
|
||||
Name string
|
||||
Description string
|
||||
Duration time.Duration
|
||||
Status StepStatus
|
||||
}
|
||||
|
||||
// StepStatus represents the status of a progress step
|
||||
type StepStatus string
|
||||
|
||||
const (
|
||||
StepStatusPending StepStatus = "pending"
|
||||
StepStatusRunning StepStatus = "running"
|
||||
StepStatusCompleted StepStatus = "completed"
|
||||
StepStatusFailed StepStatus = "failed"
|
||||
StepStatusSkipped StepStatus = "skipped"
|
||||
)
|
||||
|
||||
// NewProgressReporter creates a new progress reporter
|
||||
func NewProgressReporter(writer io.Writer, verbose bool) *ProgressReporter {
|
||||
return &ProgressReporter{
|
||||
writer: writer,
|
||||
verbose: verbose,
|
||||
startTime: time.Now(),
|
||||
steps: make([]ProgressStep, 0),
|
||||
current: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// AddStep adds a new step to the progress reporter
|
||||
func (p *ProgressReporter) AddStep(name, description string) {
|
||||
p.steps = append(p.steps, ProgressStep{
|
||||
Name: name,
|
||||
Description: description,
|
||||
Status: StepStatusPending,
|
||||
})
|
||||
}
|
||||
|
||||
// StartStep marks the current step as running
|
||||
func (p *ProgressReporter) StartStep(stepIndex int) {
|
||||
if stepIndex < 0 || stepIndex >= len(p.steps) {
|
||||
return
|
||||
}
|
||||
|
||||
p.current = stepIndex
|
||||
p.steps[stepIndex].Status = StepStatusRunning
|
||||
|
||||
if p.verbose {
|
||||
fmt.Fprintf(p.writer, "🔄 Starting: %s\n", p.steps[stepIndex].Description)
|
||||
} else {
|
||||
fmt.Fprintf(p.writer, "🔄 %s... ", p.steps[stepIndex].Name)
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteStep marks the current step as completed
|
||||
func (p *ProgressReporter) CompleteStep(stepIndex int) {
|
||||
if stepIndex < 0 || stepIndex >= len(p.steps) {
|
||||
return
|
||||
}
|
||||
|
||||
p.steps[stepIndex].Status = StepStatusCompleted
|
||||
p.steps[stepIndex].Duration = time.Since(p.startTime)
|
||||
|
||||
if p.verbose {
|
||||
fmt.Fprintf(p.writer, "✅ Completed: %s (took %v)\n",
|
||||
p.steps[stepIndex].Description, p.steps[stepIndex].Duration)
|
||||
} else {
|
||||
fmt.Fprintf(p.writer, "✅\n")
|
||||
}
|
||||
}
|
||||
|
||||
// FailStep marks the current step as failed
|
||||
func (p *ProgressReporter) FailStep(stepIndex int, err error) {
|
||||
if stepIndex < 0 || stepIndex >= len(p.steps) {
|
||||
return
|
||||
}
|
||||
|
||||
p.steps[stepIndex].Status = StepStatusFailed
|
||||
p.steps[stepIndex].Duration = time.Since(p.startTime)
|
||||
|
||||
if p.verbose {
|
||||
fmt.Fprintf(p.writer, "❌ Failed: %s (took %v) - %v\n",
|
||||
p.steps[stepIndex].Description, p.steps[stepIndex].Duration, err)
|
||||
} else {
|
||||
fmt.Fprintf(p.writer, "❌ (%v)\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SkipStep marks the current step as skipped
|
||||
func (p *ProgressReporter) SkipStep(stepIndex int, reason string) {
|
||||
if stepIndex < 0 || stepIndex >= len(p.steps) {
|
||||
return
|
||||
}
|
||||
|
||||
p.steps[stepIndex].Status = StepStatusSkipped
|
||||
|
||||
if p.verbose {
|
||||
fmt.Fprintf(p.writer, "⏭️ Skipped: %s - %s\n",
|
||||
p.steps[stepIndex].Description, reason)
|
||||
} else {
|
||||
fmt.Fprintf(p.writer, "⏭️ (%s)\n", reason)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintSummary prints a summary of all steps
|
||||
func (p *ProgressReporter) PrintSummary() {
|
||||
totalDuration := time.Since(p.startTime)
|
||||
|
||||
fmt.Fprintf(p.writer, "\n📊 Build Summary:\n")
|
||||
fmt.Fprintf(p.writer, " Total time: %v\n", totalDuration)
|
||||
fmt.Fprintf(p.writer, " Steps completed: %d/%d\n",
|
||||
p.getCompletedCount(), len(p.steps))
|
||||
|
||||
if p.verbose {
|
||||
fmt.Fprintf(p.writer, "\n📋 Step Details:\n")
|
||||
for i, step := range p.steps {
|
||||
status := getStatusEmoji(step.Status)
|
||||
fmt.Fprintf(p.writer, " %d. %s %s", i+1, status, step.Name)
|
||||
if step.Duration > 0 {
|
||||
fmt.Fprintf(p.writer, " (%v)", step.Duration)
|
||||
}
|
||||
fmt.Fprintf(p.writer, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getCompletedCount returns the number of completed steps
|
||||
func (p *ProgressReporter) getCompletedCount() int {
|
||||
count := 0
|
||||
for _, step := range p.steps {
|
||||
if step.Status == StepStatusCompleted {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// getStatusEmoji returns an emoji for the step status
|
||||
func getStatusEmoji(status StepStatus) string {
|
||||
switch status {
|
||||
case StepStatusPending:
|
||||
return "⏳"
|
||||
case StepStatusRunning:
|
||||
return "🔄"
|
||||
case StepStatusCompleted:
|
||||
return "✅"
|
||||
case StepStatusFailed:
|
||||
return "❌"
|
||||
case StepStatusSkipped:
|
||||
return "⏭️"
|
||||
default:
|
||||
return "❓"
|
||||
}
|
||||
}
|
||||
|
||||
// Simple progress indicators for quick operations
|
||||
|
||||
// PrintProgress prints a simple progress indicator
|
||||
func PrintProgress(writer io.Writer, message string) {
|
||||
fmt.Fprintf(writer, "🔄 %s...\n", message)
|
||||
}
|
||||
|
||||
// PrintSuccess prints a success message
|
||||
func PrintSuccess(writer io.Writer, message string) {
|
||||
fmt.Fprintf(writer, "✅ %s\n", message)
|
||||
}
|
||||
|
||||
// PrintWarning prints a warning message
|
||||
func PrintWarning(writer io.Writer, message string) {
|
||||
fmt.Fprintf(writer, "⚠️ %s\n", message)
|
||||
}
|
||||
|
||||
// PrintInfo prints an info message
|
||||
func PrintInfo(writer io.Writer, message string) {
|
||||
fmt.Fprintf(writer, "ℹ️ %s\n", message)
|
||||
}
|
||||
|
||||
// PrintError prints an error message
|
||||
func PrintError(writer io.Writer, message string) {
|
||||
fmt.Fprintf(writer, "❌ %s\n", message)
|
||||
}
|
||||
|
||||
// PrintStep prints a step message with optional details
|
||||
func PrintStep(writer io.Writer, step, message string, verbose bool) {
|
||||
if verbose {
|
||||
fmt.Fprintf(writer, "🔄 [%s] %s\n", step, message)
|
||||
} else {
|
||||
fmt.Fprintf(writer, "🔄 %s\n", message)
|
||||
}
|
||||
}
|
||||
411
bib/internal/ux/troubleshooting.go
Normal file
411
bib/internal/ux/troubleshooting.go
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
package ux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// TroubleshootingGuide provides troubleshooting information and diagnostics
|
||||
type TroubleshootingGuide struct {
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// NewTroubleshootingGuide creates a new troubleshooting guide
|
||||
func NewTroubleshootingGuide(verbose bool) *TroubleshootingGuide {
|
||||
return &TroubleshootingGuide{verbose: verbose}
|
||||
}
|
||||
|
||||
// DiagnosticResult represents the result of a diagnostic check
|
||||
type DiagnosticResult struct {
|
||||
Check string
|
||||
Status string
|
||||
Message string
|
||||
Details string
|
||||
Fix string
|
||||
Critical bool
|
||||
}
|
||||
|
||||
// RunDiagnostics runs comprehensive system diagnostics
|
||||
func (t *TroubleshootingGuide) RunDiagnostics() []DiagnosticResult {
|
||||
var results []DiagnosticResult
|
||||
|
||||
// Check required tools
|
||||
results = append(results, t.checkRequiredTools()...)
|
||||
|
||||
// Check system resources
|
||||
results = append(results, t.checkSystemResources()...)
|
||||
|
||||
// Check permissions
|
||||
results = append(results, t.checkPermissions()...)
|
||||
|
||||
// Check network connectivity
|
||||
results = append(results, t.checkNetworkConnectivity()...)
|
||||
|
||||
// Check container runtime
|
||||
results = append(results, t.checkContainerRuntime()...)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// checkRequiredTools checks for required system tools
|
||||
func (t *TroubleshootingGuide) checkRequiredTools() []DiagnosticResult {
|
||||
var results []DiagnosticResult
|
||||
|
||||
requiredTools := map[string]string{
|
||||
"apt-cache": "Required for package dependency resolution",
|
||||
"podman": "Required for container operations",
|
||||
"qemu-img": "Required for image format conversion",
|
||||
"file": "Required for file type detection",
|
||||
}
|
||||
|
||||
for tool, description := range requiredTools {
|
||||
result := t.checkTool(tool, description)
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// checkTool checks if a specific tool is available
|
||||
func (t *TroubleshootingGuide) checkTool(tool, description string) DiagnosticResult {
|
||||
_, err := exec.LookPath(tool)
|
||||
if err != nil {
|
||||
return DiagnosticResult{
|
||||
Check: fmt.Sprintf("Tool: %s", tool),
|
||||
Status: "❌ Missing",
|
||||
Message: fmt.Sprintf("%s is not installed or not in PATH", tool),
|
||||
Details: description,
|
||||
Fix: fmt.Sprintf("Install %s: sudo apt install %s", tool, t.getPackageName(tool)),
|
||||
Critical: true,
|
||||
}
|
||||
}
|
||||
|
||||
return DiagnosticResult{
|
||||
Check: fmt.Sprintf("Tool: %s", tool),
|
||||
Status: "✅ Available",
|
||||
Message: fmt.Sprintf("%s is installed and accessible", tool),
|
||||
Details: description,
|
||||
Critical: false,
|
||||
}
|
||||
}
|
||||
|
||||
// getPackageName returns the package name for a tool
|
||||
func (t *TroubleshootingGuide) getPackageName(tool string) string {
|
||||
packageMap := map[string]string{
|
||||
"apt-cache": "apt",
|
||||
"podman": "podman",
|
||||
"qemu-img": "qemu-utils",
|
||||
"file": "file",
|
||||
}
|
||||
|
||||
if pkg, exists := packageMap[tool]; exists {
|
||||
return pkg
|
||||
}
|
||||
return tool
|
||||
}
|
||||
|
||||
// checkSystemResources checks system resource availability
|
||||
func (t *TroubleshootingGuide) checkSystemResources() []DiagnosticResult {
|
||||
var results []DiagnosticResult
|
||||
|
||||
// Check available disk space
|
||||
results = append(results, t.checkDiskSpace())
|
||||
|
||||
// Check available memory
|
||||
results = append(results, t.checkMemory())
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// checkDiskSpace checks available disk space
|
||||
func (t *TroubleshootingGuide) checkDiskSpace() DiagnosticResult {
|
||||
// Check current directory disk space
|
||||
var stat syscall.Statfs_t
|
||||
err := syscall.Statfs(".", &stat)
|
||||
if err != nil {
|
||||
return DiagnosticResult{
|
||||
Check: "Disk Space",
|
||||
Status: "⚠️ Unknown",
|
||||
Message: "Cannot determine available disk space",
|
||||
Details: "Failed to get filesystem statistics",
|
||||
Fix: "Check disk space manually: df -h",
|
||||
Critical: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate available space in GB
|
||||
availableBytes := stat.Bavail * uint64(stat.Bsize)
|
||||
availableGB := float64(availableBytes) / (1024 * 1024 * 1024)
|
||||
|
||||
if availableGB < 5.0 {
|
||||
return DiagnosticResult{
|
||||
Check: "Disk Space",
|
||||
Status: "❌ Low",
|
||||
Message: fmt.Sprintf("Only %.1f GB available", availableGB),
|
||||
Details: "Image building requires at least 5GB of free space",
|
||||
Fix: "Free up disk space or use a different directory",
|
||||
Critical: true,
|
||||
}
|
||||
}
|
||||
|
||||
return DiagnosticResult{
|
||||
Check: "Disk Space",
|
||||
Status: "✅ Sufficient",
|
||||
Message: fmt.Sprintf("%.1f GB available", availableGB),
|
||||
Details: "Adequate disk space for image building",
|
||||
Critical: false,
|
||||
}
|
||||
}
|
||||
|
||||
// checkMemory checks available system memory
|
||||
func (t *TroubleshootingGuide) checkMemory() DiagnosticResult {
|
||||
// Read /proc/meminfo for memory information
|
||||
data, err := os.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
return DiagnosticResult{
|
||||
Check: "Memory",
|
||||
Status: "⚠️ Unknown",
|
||||
Message: "Cannot determine available memory",
|
||||
Details: "Failed to read memory information",
|
||||
Fix: "Check memory manually: free -h",
|
||||
Critical: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse available memory (simplified)
|
||||
lines := strings.Split(string(data), "\n")
|
||||
var memTotal, memAvailable int64
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "MemTotal:") {
|
||||
fmt.Sscanf(line, "MemTotal: %d kB", &memTotal)
|
||||
} else if strings.HasPrefix(line, "MemAvailable:") {
|
||||
fmt.Sscanf(line, "MemAvailable: %d kB", &memAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
if memTotal == 0 {
|
||||
return DiagnosticResult{
|
||||
Check: "Memory",
|
||||
Status: "⚠️ Unknown",
|
||||
Message: "Cannot parse memory information",
|
||||
Details: "Failed to parse /proc/meminfo",
|
||||
Fix: "Check memory manually: free -h",
|
||||
Critical: false,
|
||||
}
|
||||
}
|
||||
|
||||
memTotalGB := float64(memTotal) / (1024 * 1024)
|
||||
memAvailableGB := float64(memAvailable) / (1024 * 1024)
|
||||
|
||||
if memTotalGB < 2.0 {
|
||||
return DiagnosticResult{
|
||||
Check: "Memory",
|
||||
Status: "❌ Insufficient",
|
||||
Message: fmt.Sprintf("Only %.1f GB total memory", memTotalGB),
|
||||
Details: "Image building requires at least 2GB of RAM",
|
||||
Fix: "Add more RAM or use a system with more memory",
|
||||
Critical: true,
|
||||
}
|
||||
}
|
||||
|
||||
if memAvailableGB < 1.0 {
|
||||
return DiagnosticResult{
|
||||
Check: "Memory",
|
||||
Status: "⚠️ Low",
|
||||
Message: fmt.Sprintf("Only %.1f GB available memory", memAvailableGB),
|
||||
Details: "Low available memory may cause build failures",
|
||||
Fix: "Close other applications or add more RAM",
|
||||
Critical: false,
|
||||
}
|
||||
}
|
||||
|
||||
return DiagnosticResult{
|
||||
Check: "Memory",
|
||||
Status: "✅ Sufficient",
|
||||
Message: fmt.Sprintf("%.1f GB total, %.1f GB available", memTotalGB, memAvailableGB),
|
||||
Details: "Adequate memory for image building",
|
||||
Critical: false,
|
||||
}
|
||||
}
|
||||
|
||||
// checkPermissions checks file and directory permissions
|
||||
func (t *TroubleshootingGuide) checkPermissions() []DiagnosticResult {
|
||||
var results []DiagnosticResult
|
||||
|
||||
// Check current directory write permissions
|
||||
results = append(results, t.checkWritePermission(".", "Current directory"))
|
||||
|
||||
// Check /tmp write permissions
|
||||
results = append(results, t.checkWritePermission("/tmp", "Temporary directory"))
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// checkWritePermission checks write permission for a directory
|
||||
func (t *TroubleshootingGuide) checkWritePermission(path, description string) DiagnosticResult {
|
||||
// Try to create a test file
|
||||
testFile := filepath.Join(path, ".debian-bootc-image-builder-test")
|
||||
err := os.WriteFile(testFile, []byte("test"), 0644)
|
||||
if err != nil {
|
||||
// Clean up if file was created
|
||||
os.Remove(testFile)
|
||||
return DiagnosticResult{
|
||||
Check: fmt.Sprintf("Write Permission: %s", description),
|
||||
Status: "❌ Denied",
|
||||
Message: fmt.Sprintf("Cannot write to %s", path),
|
||||
Details: fmt.Sprintf("Permission denied: %v", err),
|
||||
Fix: fmt.Sprintf("Check permissions: ls -la %s", path),
|
||||
Critical: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up test file
|
||||
os.Remove(testFile)
|
||||
return DiagnosticResult{
|
||||
Check: fmt.Sprintf("Write Permission: %s", description),
|
||||
Status: "✅ Allowed",
|
||||
Message: fmt.Sprintf("Can write to %s", path),
|
||||
Details: "Write permissions are sufficient",
|
||||
Critical: false,
|
||||
}
|
||||
}
|
||||
|
||||
// checkNetworkConnectivity checks network connectivity for required services
|
||||
func (t *TroubleshootingGuide) checkNetworkConnectivity() []DiagnosticResult {
|
||||
var results []DiagnosticResult
|
||||
|
||||
// Check basic internet connectivity
|
||||
results = append(results, t.checkConnectivity("8.8.8.8", "Internet connectivity"))
|
||||
|
||||
// Check Debian repository connectivity
|
||||
results = append(results, t.checkConnectivity("deb.debian.org", "Debian repositories"))
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// checkConnectivity checks network connectivity to a host
|
||||
func (t *TroubleshootingGuide) checkConnectivity(host, description string) DiagnosticResult {
|
||||
cmd := exec.Command("ping", "-c", "1", "-W", "5", host)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return DiagnosticResult{
|
||||
Check: fmt.Sprintf("Network: %s", description),
|
||||
Status: "❌ Failed",
|
||||
Message: fmt.Sprintf("Cannot reach %s", host),
|
||||
Details: "Network connectivity issue",
|
||||
Fix: "Check network connection and firewall settings",
|
||||
Critical: false, // Not critical for basic functionality
|
||||
}
|
||||
}
|
||||
|
||||
return DiagnosticResult{
|
||||
Check: fmt.Sprintf("Network: %s", description),
|
||||
Status: "✅ Connected",
|
||||
Message: fmt.Sprintf("Can reach %s", host),
|
||||
Details: "Network connectivity is working",
|
||||
Critical: false,
|
||||
}
|
||||
}
|
||||
|
||||
// checkContainerRuntime checks container runtime availability
|
||||
func (t *TroubleshootingGuide) checkContainerRuntime() []DiagnosticResult {
|
||||
var results []DiagnosticResult
|
||||
|
||||
// Check if podman is running
|
||||
cmd := exec.Command("podman", "version")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
results = append(results, DiagnosticResult{
|
||||
Check: "Container Runtime",
|
||||
Status: "❌ Failed",
|
||||
Message: "Podman is not working properly",
|
||||
Details: "Cannot execute podman version command",
|
||||
Fix: "Check podman installation and configuration",
|
||||
Critical: true,
|
||||
})
|
||||
} else {
|
||||
results = append(results, DiagnosticResult{
|
||||
Check: "Container Runtime",
|
||||
Status: "✅ Working",
|
||||
Message: "Podman is working properly",
|
||||
Details: "Container runtime is functional",
|
||||
Critical: false,
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// PrintDiagnostics prints diagnostic results
|
||||
func (t *TroubleshootingGuide) PrintDiagnostics(results []DiagnosticResult) {
|
||||
fmt.Println("🔍 System Diagnostics:")
|
||||
fmt.Println()
|
||||
|
||||
criticalIssues := 0
|
||||
warnings := 0
|
||||
|
||||
for _, result := range results {
|
||||
fmt.Printf("%s %s: %s\n", result.Status, result.Check, result.Message)
|
||||
|
||||
if t.verbose && result.Details != "" {
|
||||
fmt.Printf(" Details: %s\n", result.Details)
|
||||
}
|
||||
|
||||
if result.Fix != "" {
|
||||
fmt.Printf(" Fix: %s\n", result.Fix)
|
||||
}
|
||||
|
||||
if result.Critical {
|
||||
criticalIssues++
|
||||
} else if strings.Contains(result.Status, "⚠️") {
|
||||
warnings++
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Summary
|
||||
if criticalIssues > 0 {
|
||||
fmt.Printf("❌ Found %d critical issues that must be resolved\n", criticalIssues)
|
||||
} else if warnings > 0 {
|
||||
fmt.Printf("⚠️ Found %d warnings (non-critical)\n", warnings)
|
||||
} else {
|
||||
fmt.Println("✅ All diagnostics passed - system is ready")
|
||||
}
|
||||
}
|
||||
|
||||
// GetCommonSolutions returns common solutions for frequent issues
|
||||
func (t *TroubleshootingGuide) GetCommonSolutions() map[string][]string {
|
||||
return map[string][]string{
|
||||
"apt-cache failed": {
|
||||
"Ensure apt-cache is installed: sudo apt install apt",
|
||||
"Update package lists: sudo apt update",
|
||||
"Check repository configuration: cat /etc/apt/sources.list",
|
||||
},
|
||||
"permission denied": {
|
||||
"Check file/directory permissions: ls -la",
|
||||
"Use appropriate user permissions",
|
||||
"Try running with sudo if necessary",
|
||||
},
|
||||
"container not found": {
|
||||
"Verify container image exists: podman images",
|
||||
"Pull the container: podman pull <image>",
|
||||
"Check image reference format",
|
||||
},
|
||||
"out of disk space": {
|
||||
"Free up disk space: df -h",
|
||||
"Remove unnecessary files",
|
||||
"Use a different output directory",
|
||||
},
|
||||
"out of memory": {
|
||||
"Close other applications",
|
||||
"Add swap space if needed",
|
||||
"Use a system with more RAM",
|
||||
},
|
||||
}
|
||||
}
|
||||
323
bib/internal/ux/validation.go
Normal file
323
bib/internal/ux/validation.go
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
package ux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Validator provides input validation functionality
|
||||
type Validator struct {
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// NewValidator creates a new validator
|
||||
func NewValidator(verbose bool) *Validator {
|
||||
return &Validator{verbose: verbose}
|
||||
}
|
||||
|
||||
// ValidationResult represents the result of a validation
|
||||
type ValidationResult struct {
|
||||
Valid bool
|
||||
Message string
|
||||
Suggestions []string
|
||||
}
|
||||
|
||||
// ValidateImageReference validates a container image reference
|
||||
func (v *Validator) ValidateImageReference(imageRef string) *ValidationResult {
|
||||
if imageRef == "" {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: "Image reference cannot be empty",
|
||||
Suggestions: []string{
|
||||
"Provide a valid container image reference",
|
||||
"Example: git.raines.xyz/particle-os/debian-bootc:latest",
|
||||
"Example: docker.io/debian:bookworm-slim",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Basic validation for image reference format
|
||||
// Should contain at least one colon or slash
|
||||
if !strings.Contains(imageRef, ":") && !strings.Contains(imageRef, "/") {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: "Invalid image reference format",
|
||||
Suggestions: []string{
|
||||
"Image reference should contain registry, namespace, and tag",
|
||||
"Example: registry.example.com/namespace/image:tag",
|
||||
"Example: image:tag",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Check for valid characters (allow hyphens in addition to other valid chars)
|
||||
validChars := regexp.MustCompile(`^[a-zA-Z0-9._/:+-]+$`)
|
||||
if !validChars.MatchString(imageRef) {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: "Image reference contains invalid characters",
|
||||
Suggestions: []string{
|
||||
"Use only alphanumeric characters, dots, underscores, slashes, hyphens, and colons",
|
||||
"Example: git.raines.xyz/particle-os/debian-bootc:latest",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &ValidationResult{Valid: true}
|
||||
}
|
||||
|
||||
// ValidateImageTypes validates image type specifications
|
||||
func (v *Validator) ValidateImageTypes(imageTypes []string) *ValidationResult {
|
||||
if len(imageTypes) == 0 {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: "At least one image type must be specified",
|
||||
Suggestions: []string{
|
||||
"Specify one or more image types: qcow2, ami, vmdk, raw, debian-installer, calamares",
|
||||
"Example: --type qcow2 --type ami",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
validTypes := map[string]bool{
|
||||
"qcow2": true,
|
||||
"ami": true,
|
||||
"vmdk": true,
|
||||
"raw": true,
|
||||
"debian-installer": true,
|
||||
"calamares": true,
|
||||
}
|
||||
|
||||
var invalidTypes []string
|
||||
for _, imgType := range imageTypes {
|
||||
if !validTypes[imgType] {
|
||||
invalidTypes = append(invalidTypes, imgType)
|
||||
}
|
||||
}
|
||||
|
||||
if len(invalidTypes) > 0 {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("Invalid image types: %s", strings.Join(invalidTypes, ", ")),
|
||||
Suggestions: []string{
|
||||
"Supported image types: qcow2, ami, vmdk, raw, debian-installer, calamares",
|
||||
"Check spelling and case sensitivity",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &ValidationResult{Valid: true}
|
||||
}
|
||||
|
||||
// ValidateArchitecture validates target architecture
|
||||
func (v *Validator) ValidateArchitecture(arch string) *ValidationResult {
|
||||
if arch == "" {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: "Target architecture cannot be empty",
|
||||
Suggestions: []string{
|
||||
"Specify a valid architecture: amd64, arm64, armhf, ppc64el, s390x",
|
||||
"Example: --target-arch amd64",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
validArchs := map[string]bool{
|
||||
"amd64": true,
|
||||
"arm64": true,
|
||||
"armhf": true,
|
||||
"ppc64el": true,
|
||||
"s390x": true,
|
||||
}
|
||||
|
||||
if !validArchs[arch] {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("Unsupported architecture: %s", arch),
|
||||
Suggestions: []string{
|
||||
"Supported architectures: amd64, arm64, armhf, ppc64el, s390x",
|
||||
"Use --target-arch amd64 for x86_64 systems",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &ValidationResult{Valid: true}
|
||||
}
|
||||
|
||||
// ValidateRootfsType validates root filesystem type
|
||||
func (v *Validator) ValidateRootfsType(rootfsType string) *ValidationResult {
|
||||
if rootfsType == "" {
|
||||
// Empty is valid (will use default)
|
||||
return &ValidationResult{Valid: true}
|
||||
}
|
||||
|
||||
validTypes := map[string]bool{
|
||||
"ext4": true,
|
||||
"xfs": true,
|
||||
"btrfs": true,
|
||||
}
|
||||
|
||||
if !validTypes[rootfsType] {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("Unsupported root filesystem type: %s", rootfsType),
|
||||
Suggestions: []string{
|
||||
"Supported filesystem types: ext4, xfs, btrfs",
|
||||
"Leave empty to use default (ext4)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &ValidationResult{Valid: true}
|
||||
}
|
||||
|
||||
// ValidateDirectory validates a directory path
|
||||
func (v *Validator) ValidateDirectory(path, purpose string) *ValidationResult {
|
||||
if path == "" {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("%s path cannot be empty", purpose),
|
||||
Suggestions: []string{
|
||||
fmt.Sprintf("Specify a valid directory path for %s", purpose),
|
||||
"Use absolute or relative paths",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Check if path exists and is a directory
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Try to create the directory
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("Cannot create %s directory: %v", purpose, err),
|
||||
Suggestions: []string{
|
||||
"Check parent directory permissions",
|
||||
"Use a different directory path",
|
||||
"Run with appropriate permissions",
|
||||
},
|
||||
}
|
||||
}
|
||||
return &ValidationResult{Valid: true}
|
||||
}
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("Cannot access %s directory: %v", purpose, err),
|
||||
Suggestions: []string{
|
||||
"Check directory permissions",
|
||||
"Ensure the path is accessible",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("%s path is not a directory: %s", purpose, path),
|
||||
Suggestions: []string{
|
||||
"Specify a directory path, not a file",
|
||||
"Check the path exists and is a directory",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &ValidationResult{Valid: true}
|
||||
}
|
||||
|
||||
// ValidateConfigFile validates a configuration file path
|
||||
func (v *Validator) ValidateConfigFile(configPath string) *ValidationResult {
|
||||
if configPath == "" {
|
||||
// Empty is valid (will use default)
|
||||
return &ValidationResult{Valid: true}
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
info, err := os.Stat(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("Configuration file not found: %s", configPath),
|
||||
Suggestions: []string{
|
||||
"Check the file path is correct",
|
||||
"Create a configuration file at the specified path",
|
||||
"Leave empty to use default configuration",
|
||||
},
|
||||
}
|
||||
}
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("Cannot access configuration file: %v", err),
|
||||
Suggestions: []string{
|
||||
"Check file permissions",
|
||||
"Ensure the file is accessible",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("Configuration path is a directory, not a file: %s", configPath),
|
||||
Suggestions: []string{
|
||||
"Specify a file path, not a directory",
|
||||
"Example: .config/registry.yaml",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
ext := strings.ToLower(filepath.Ext(configPath))
|
||||
if ext != ".yaml" && ext != ".yml" {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: fmt.Sprintf("Configuration file should be YAML format: %s", configPath),
|
||||
Suggestions: []string{
|
||||
"Use .yaml or .yml extension",
|
||||
"Example: .config/registry.yaml",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &ValidationResult{Valid: true}
|
||||
}
|
||||
|
||||
// ValidateDistroDefPath validates distribution definition path
|
||||
func (v *Validator) ValidateDistroDefPath(path string) *ValidationResult {
|
||||
if path == "" {
|
||||
return &ValidationResult{
|
||||
Valid: false,
|
||||
Message: "Distribution definition path cannot be empty",
|
||||
Suggestions: []string{
|
||||
"Specify a valid directory path containing package definitions",
|
||||
"Example: ./data/defs",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return v.ValidateDirectory(path, "distribution definition")
|
||||
}
|
||||
|
||||
// PrintValidationResult prints validation results with suggestions
|
||||
func (v *Validator) PrintValidationResult(result *ValidationResult) {
|
||||
if result.Valid {
|
||||
if v.verbose {
|
||||
PrintInfo(os.Stdout, "Validation passed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
PrintError(os.Stdout, result.Message)
|
||||
|
||||
if len(result.Suggestions) > 0 {
|
||||
fmt.Println("💡 Suggestions:")
|
||||
for _, suggestion := range result.Suggestions {
|
||||
fmt.Printf(" • %s\n", suggestion)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue