Initial commit
This commit is contained in:
commit
3326d796f0
87 changed files with 15792 additions and 0 deletions
97
bib/internal/aptsolver/aptsolver.go
Normal file
97
bib/internal/aptsolver/aptsolver.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package aptsolver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/osbuild/images/pkg/arch"
|
||||
"github.com/osbuild/images/pkg/bib/osinfo"
|
||||
)
|
||||
|
||||
// AptSolver implements package dependency resolution for Debian using apt
|
||||
type AptSolver struct {
|
||||
arch arch.Arch
|
||||
osInfo *osinfo.Info
|
||||
cacheDir string
|
||||
}
|
||||
|
||||
// DepsolveResult represents the result of apt dependency resolution
|
||||
type DepsolveResult struct {
|
||||
Packages []string
|
||||
Repos []interface{}
|
||||
}
|
||||
|
||||
// NewAptSolver creates a new apt-based solver for Debian
|
||||
func NewAptSolver(cacheDir string, arch arch.Arch, osInfo *osinfo.Info) *AptSolver {
|
||||
return &AptSolver{
|
||||
arch: arch,
|
||||
osInfo: osInfo,
|
||||
cacheDir: cacheDir,
|
||||
}
|
||||
}
|
||||
|
||||
// Depsolve resolves package dependencies using apt
|
||||
func (s *AptSolver) Depsolve(packages []string, maxAttempts int) (*DepsolveResult, error) {
|
||||
// For now, we'll return the packages as-is since apt dependency resolution
|
||||
// is more complex and would require running apt in a chroot
|
||||
// This is a simplified implementation that will be enhanced later
|
||||
|
||||
result := &DepsolveResult{
|
||||
Packages: packages,
|
||||
Repos: []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "debian",
|
||||
"baseurls": []string{"http://deb.debian.org/debian"},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "debian-security",
|
||||
"baseurls": []string{"http://deb.debian.org/debian-security"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetArch returns the architecture for this solver
|
||||
func (s *AptSolver) GetArch() arch.Arch {
|
||||
return s.arch
|
||||
}
|
||||
|
||||
// GetOSInfo returns the OS information for this solver
|
||||
func (s *AptSolver) GetOSInfo() *osinfo.Info {
|
||||
return s.osInfo
|
||||
}
|
||||
|
||||
// ValidatePackages checks if the specified packages are available in Debian repositories
|
||||
func (s *AptSolver) ValidatePackages(packages []string) error {
|
||||
// This is a simplified validation - in a real implementation,
|
||||
// we would query the Debian package database
|
||||
for _, pkg := range packages {
|
||||
if !strings.HasPrefix(pkg, "linux-") &&
|
||||
!strings.HasPrefix(pkg, "grub-") &&
|
||||
!strings.HasPrefix(pkg, "initramfs-") &&
|
||||
pkg != "util-linux" &&
|
||||
pkg != "parted" &&
|
||||
pkg != "e2fsprogs" &&
|
||||
pkg != "dosfstools" &&
|
||||
pkg != "efibootmgr" &&
|
||||
pkg != "systemd" &&
|
||||
pkg != "dbus" &&
|
||||
pkg != "sudo" {
|
||||
// For now, we'll assume these are valid Debian packages
|
||||
// In a real implementation, we would validate against the package database
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPackageInfo retrieves information about a specific package
|
||||
func (s *AptSolver) GetPackageInfo(packageName string) (map[string]interface{}, error) {
|
||||
// This is a placeholder - in a real implementation, we would query apt
|
||||
// for detailed package information
|
||||
return map[string]interface{}{
|
||||
"name": packageName,
|
||||
"version": "latest",
|
||||
"arch": s.arch.String(),
|
||||
}, nil
|
||||
}
|
||||
66
bib/internal/debian-patch/bootc_validation.go
Normal file
66
bib/internal/debian-patch/bootc_validation.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package debianpatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// IsBootcImage checks if an image is a bootc image by looking for either
|
||||
// com.redhat.bootc=true or com.debian.bootc=true labels
|
||||
func IsBootcImage(labels map[string]string) bool {
|
||||
// Check for Red Hat bootc label (for compatibility)
|
||||
if val, exists := labels["com.redhat.bootc"]; exists && val == "true" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for Debian bootc label (our addition)
|
||||
if val, exists := labels["com.debian.bootc"]; exists && val == "true" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateBootcImage validates that an image has the required bootc markers
|
||||
func ValidateBootcImage(labels map[string]string, imageRef string) error {
|
||||
if !IsBootcImage(labels) {
|
||||
return fmt.Errorf("image %s is not a bootc image (missing com.redhat.bootc=true or com.debian.bootc=true label)", imageRef)
|
||||
}
|
||||
|
||||
// Check for required OSTree labels
|
||||
if val, exists := labels["ostree.bootable"]; !exists || val != "true" {
|
||||
return fmt.Errorf("image %s is not a bootc image (missing ostree.bootable=true label)", imageRef)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBootcType returns the type of bootc image (redhat, debian, or unknown)
|
||||
func GetBootcType(labels map[string]string) string {
|
||||
if val, exists := labels["com.redhat.bootc"]; exists && val == "true" {
|
||||
return "redhat"
|
||||
}
|
||||
|
||||
if val, exists := labels["com.debian.bootc"]; exists && val == "true" {
|
||||
return "debian"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// ValidateDebianBootcImage performs Debian-specific validation
|
||||
func ValidateDebianBootcImage(labels map[string]string, imageRef string) error {
|
||||
// First, validate it's a bootc image
|
||||
if err := ValidateBootcImage(labels, imageRef); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if it's specifically a Debian bootc image
|
||||
if GetBootcType(labels) != "debian" {
|
||||
return fmt.Errorf("image %s is not a Debian bootc image (missing com.debian.bootc=true label)", imageRef)
|
||||
}
|
||||
|
||||
// Additional Debian-specific validations can be added here
|
||||
// For example, checking for Debian-specific labels or configurations
|
||||
|
||||
return nil
|
||||
}
|
||||
85
bib/internal/debian-patch/images_wrapper.go
Normal file
85
bib/internal/debian-patch/images_wrapper.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package debianpatch
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BootcImageInfo contains the validation logic for bootc images
|
||||
type BootcImageInfo struct {
|
||||
Labels map[string]string `json:"Labels"`
|
||||
}
|
||||
|
||||
// ContainerImage represents a container image with labels
|
||||
type ContainerImage struct {
|
||||
Labels map[string]string
|
||||
Ref string
|
||||
}
|
||||
|
||||
// ValidateImage checks if an image is a valid bootc image using our Debian-aware validation
|
||||
func ValidateImage(imageRef string) error {
|
||||
// This function will be called before the upstream images library validation
|
||||
// In practice, you'd need to:
|
||||
// 1. Inspect the container image to get labels
|
||||
// 2. Call ValidateBootcImage(labels, imageRef)
|
||||
|
||||
// For now, this is a placeholder that demonstrates the integration point
|
||||
// The actual implementation would need to integrate with the container inspection logic
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreValidateImage performs validation before the upstream images library processes the image
|
||||
func PreValidateImage(imageRef string) error {
|
||||
// This is called before the upstream validation
|
||||
// We can add Debian-specific pre-validation here
|
||||
|
||||
// Check if the image reference looks like a Debian image
|
||||
if strings.Contains(imageRef, "debian") || strings.Contains(imageRef, "particle-os") {
|
||||
// This is a hint that we might be dealing with a Debian image
|
||||
// We could add additional validation here
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostValidateImage performs validation after the upstream images library processes the image
|
||||
func PostValidateImage(imageRef string, labels map[string]string) error {
|
||||
// This is called after the upstream validation
|
||||
// We can add Debian-specific post-validation here
|
||||
|
||||
// Check if this is a Debian bootc image
|
||||
if GetBootcType(labels) == "debian" {
|
||||
// Perform Debian-specific validations
|
||||
return ValidateDebianBootcImage(labels, imageRef)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetImageLabels extracts labels from a container image
|
||||
// This is a placeholder - in practice, you'd integrate with the actual container inspection
|
||||
func GetImageLabels(imageRef string) (map[string]string, error) {
|
||||
// This would integrate with the actual container inspection logic
|
||||
// For now, return an empty map as a placeholder
|
||||
return make(map[string]string), nil
|
||||
}
|
||||
|
||||
// IsDebianImage checks if an image is specifically a Debian image
|
||||
func IsDebianImage(labels map[string]string) bool {
|
||||
return GetBootcType(labels) == "debian"
|
||||
}
|
||||
|
||||
// GetDebianVersion extracts Debian version information from labels
|
||||
func GetDebianVersion(labels map[string]string) string {
|
||||
// Check for Debian-specific version labels
|
||||
if version, exists := labels["org.debian.version"]; exists {
|
||||
return version
|
||||
}
|
||||
|
||||
// Check for general version labels
|
||||
if version, exists := labels["version"]; exists {
|
||||
return version
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
78
bib/internal/debian-patch/test_validation.go
Normal file
78
bib/internal/debian-patch/test_validation.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type ContainerInspect struct {
|
||||
Labels map[string]string `json:"Labels"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("Usage: go run test_validation.go <image-tag>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
imageTag := os.Args[1]
|
||||
|
||||
// Inspect the container image
|
||||
cmd := exec.Command("podman", "inspect", imageTag)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
fmt.Printf("Error inspecting image %s: %v\n", imageTag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Parse the JSON output
|
||||
var containers []ContainerInspect
|
||||
if err := json.Unmarshal(output, &containers); err != nil {
|
||||
fmt.Printf("Error parsing JSON: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(containers) == 0 {
|
||||
fmt.Printf("No container information found for %s\n", imageTag)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
labels := containers[0].Labels
|
||||
fmt.Printf("Image: %s\n", imageTag)
|
||||
fmt.Printf("Labels: %v\n", labels)
|
||||
|
||||
// Test our validation logic
|
||||
isBootc := false
|
||||
bootcType := "unknown"
|
||||
|
||||
if val, exists := labels["com.redhat.bootc"]; exists && val == "true" {
|
||||
isBootc = true
|
||||
bootcType = "redhat"
|
||||
}
|
||||
|
||||
if val, exists := labels["com.debian.bootc"]; exists && val == "true" {
|
||||
isBootc = true
|
||||
bootcType = "debian"
|
||||
}
|
||||
|
||||
hasOstreeBootable := false
|
||||
if val, exists := labels["ostree.bootable"]; exists && val == "true" {
|
||||
hasOstreeBootable = true
|
||||
}
|
||||
|
||||
fmt.Printf("Is bootc image: %t\n", isBootc)
|
||||
fmt.Printf("Bootc type: %s\n", bootcType)
|
||||
fmt.Printf("Has ostree.bootable: %t\n", hasOstreeBootable)
|
||||
|
||||
if isBootc && hasOstreeBootable {
|
||||
fmt.Printf("✅ Image %s is a valid bootc image\n", imageTag)
|
||||
if bootcType == "debian" {
|
||||
fmt.Printf("✅ Image %s is specifically a Debian bootc image\n", imageTag)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("❌ Image %s is not a valid bootc image\n", imageTag)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
98
bib/internal/distrodef/distrodef.go
Normal file
98
bib/internal/distrodef/distrodef.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
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
|
||||
}
|
||||
148
bib/internal/distrodef/distrodef_test.go
Normal file
148
bib/internal/distrodef/distrodef_test.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
package distrodef
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testDefLocation = "test_defs"
|
||||
|
||||
func TestLoadSimple(t *testing.T) {
|
||||
def, err := LoadImageDef([]string{testDefLocation}, "fedoratest", "41", "anaconda-iso")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
}
|
||||
|
||||
func TestLoadFuzzy(t *testing.T) {
|
||||
def, err := LoadImageDef([]string{testDefLocation}, "fedoratest", "99", "anaconda-iso")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, def.Packages)
|
||||
}
|
||||
|
||||
func TestLoadUnhappy(t *testing.T) {
|
||||
_, err := LoadImageDef([]string{testDefLocation}, "lizard", "42", "anaconda-iso")
|
||||
assert.ErrorContains(t, err, "could not find def file for distro lizard-42")
|
||||
_, err = LoadImageDef([]string{testDefLocation}, "fedoratest", "0", "anaconda-iso")
|
||||
assert.ErrorContains(t, err, "could not find def file for distro fedoratest-0")
|
||||
|
||||
_, err = LoadImageDef([]string{testDefLocation}, "fedoratest", "41", "anaconda-disk")
|
||||
assert.ErrorContains(t, err, "could not find def for distro fedoratest and image type anaconda-disk")
|
||||
|
||||
_, err = LoadImageDef([]string{testDefLocation}, "fedoratest", "xxx", "anaconda-disk")
|
||||
assert.ErrorContains(t, err, `cannot parse wanted version string: `)
|
||||
}
|
||||
|
||||
const fakeDefFileContent = "anaconda-iso:\n packages: \n - foo\n"
|
||||
|
||||
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/fedora-39.yaml",
|
||||
"b/fedora-41.yaml",
|
||||
"c/fedora-41.yaml",
|
||||
})
|
||||
assert.Equal(t, 3, len(defDirs))
|
||||
|
||||
def, err := findDistroDef(defDirs, "fedora", "41")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "b/fedora-41.yaml"))
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiDirsIgnoreENOENT(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/fedora-41.yaml",
|
||||
})
|
||||
defDirs = append([]string{"/no/such/path"}, defDirs...)
|
||||
|
||||
def, err := findDistroDef(defDirs, "fedora", "41")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "a/fedora-41.yaml"))
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiFuzzy(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/fedora-39.yaml",
|
||||
"b/fedora-41.yaml",
|
||||
"b/b/fedora-42.yaml",
|
||||
"c/fedora-41.yaml",
|
||||
})
|
||||
// no fedora-99, pick the closest
|
||||
def, err := findDistroDef(defDirs, "fedora", "99")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "b/b/fedora-42.yaml"))
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiFuzzyMinorReleases(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/centos-8.9.yaml",
|
||||
"b/centos-7.yaml",
|
||||
"c/centos-9.1.yaml",
|
||||
"d/centos-9.1.1.yaml",
|
||||
"b/b/centos-9.10.yaml",
|
||||
})
|
||||
def, err := findDistroDef(defDirs, "centos", "9.11")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "b/b/centos-9.10.yaml"), def)
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiFuzzyMinorReleasesIsZero(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/centos-9.yaml",
|
||||
"a/centos-10.yaml",
|
||||
})
|
||||
def, err := findDistroDef(defDirs, "centos", "10.0")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "a/centos-10.yaml"), def)
|
||||
}
|
||||
|
||||
func TestFindDistroDefMultiFuzzyError(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/fedora-40.yaml",
|
||||
})
|
||||
// the best version we have is newer than what is requested, this
|
||||
// is an error
|
||||
_, err := findDistroDef(defDirs, "fedora", "30")
|
||||
assert.ErrorContains(t, err, "could not find def file for distro fedora-30")
|
||||
}
|
||||
|
||||
func TestFindDistroDefBadNumberIgnoresBadFiles(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/fedora-NaN.yaml",
|
||||
})
|
||||
_, err := findDistroDef(defDirs, "fedora", "40")
|
||||
assert.ErrorContains(t, err, "could not find def file for distro fedora-40")
|
||||
}
|
||||
|
||||
func TestFindDistroDefCornerCases(t *testing.T) {
|
||||
defDirs := makeFakeDistrodefRoot(t, []string{
|
||||
"a/fedora-.yaml",
|
||||
"b/fedora-1.yaml",
|
||||
"c/fedora.yaml",
|
||||
})
|
||||
def, err := findDistroDef(defDirs, "fedora", "2")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(def, "b/fedora-1.yaml"))
|
||||
}
|
||||
4
bib/internal/distrodef/test_defs/fedoratest-41.yaml
Normal file
4
bib/internal/distrodef/test_defs/fedoratest-41.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
anaconda-iso:
|
||||
packages:
|
||||
- anaconda
|
||||
- curl
|
||||
88
bib/internal/imagetypes/imagetypes.go
Normal file
88
bib/internal/imagetypes/imagetypes.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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"},
|
||||
"vhd": imageType{Export: "vpc"},
|
||||
"gce": imageType{Export: "gce"},
|
||||
"anaconda-iso": imageType{Export: "bootiso", ISO: true},
|
||||
"iso": 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
|
||||
}
|
||||
90
bib/internal/imagetypes/imagetypes_test.go
Normal file
90
bib/internal/imagetypes/imagetypes_test.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package imagetypes_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/osbuild/bootc-image-builder/bib/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,
|
||||
},
|
||||
"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,
|
||||
},
|
||||
"iso": {
|
||||
imageTypes: []string{"iso"},
|
||||
expectedExports: []string{"bootiso"},
|
||||
expectISO: true,
|
||||
},
|
||||
"anaconda": {
|
||||
imageTypes: []string{"anaconda-iso"},
|
||||
expectedExports: []string{"bootiso"},
|
||||
expectISO: true,
|
||||
},
|
||||
"bad-mix": {
|
||||
imageTypes: []string{"vmdk", "anaconda-iso"},
|
||||
expectedErr: errors.New("cannot mix ISO/disk images in request [vmdk anaconda-iso]"),
|
||||
},
|
||||
"bad-mix-part-2": {
|
||||
imageTypes: []string{"ami", "iso"},
|
||||
expectedErr: errors.New("cannot mix ISO/disk images in request [ami iso]"),
|
||||
},
|
||||
"bad-image-type": {
|
||||
imageTypes: []string{"bad"},
|
||||
expectedErr: errors.New(`unsupported image type "bad", valid types are ami, anaconda-iso, gce, iso, qcow2, raw, vhd, 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, anaconda-iso, gce, iso, qcow2, raw, vhd, vmdk`),
|
||||
},
|
||||
"all-bad": {
|
||||
imageTypes: []string{"bad1", "bad2", "bad3", "bad4", "bad5", "bad42"},
|
||||
expectedErr: errors.New(`unsupported image type "bad1", valid types are ami, anaconda-iso, gce, iso, qcow2, raw, vhd, 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
78
bib/internal/solver/solver.go
Normal file
78
bib/internal/solver/solver.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package solver
|
||||
|
||||
import (
|
||||
"github.com/osbuild/images/pkg/arch"
|
||||
"github.com/osbuild/images/pkg/dnfjson"
|
||||
"github.com/osbuild/images/pkg/bib/osinfo"
|
||||
"github.com/particle-os/debian-bootc-image-builder/bib/internal/aptsolver"
|
||||
)
|
||||
|
||||
// Solver interface that can work with both dnfjson and apt solvers
|
||||
type Solver interface {
|
||||
Depsolve(packages []string, maxAttempts int) (interface{}, error)
|
||||
GetArch() arch.Arch
|
||||
GetOSInfo() *osinfo.Info
|
||||
}
|
||||
|
||||
// DNFJSONSolver wraps the original dnfjson.Solver
|
||||
type DNFJSONSolver struct {
|
||||
*dnfjson.Solver
|
||||
}
|
||||
|
||||
// NewDNFSolver creates a new DNF solver
|
||||
func NewDNFSolver(solver *dnfjson.Solver) *DNFJSONSolver {
|
||||
return &DNFJSONSolver{Solver: solver}
|
||||
}
|
||||
|
||||
// Depsolve resolves package dependencies using DNF
|
||||
func (s *DNFJSONSolver) Depsolve(packages []string, maxAttempts int) (interface{}, error) {
|
||||
// This is a simplified implementation - in a real implementation,
|
||||
// we would need to convert the packages to the proper format
|
||||
// For now, we'll return a mock result
|
||||
return &aptsolver.DepsolveResult{
|
||||
Packages: packages,
|
||||
Repos: []interface{}{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetArch returns the architecture for this solver
|
||||
func (s *DNFJSONSolver) GetArch() arch.Arch {
|
||||
// This is a simplified implementation - in a real implementation,
|
||||
// we would need to extract the arch from the dnfjson.Solver
|
||||
return arch.Current()
|
||||
}
|
||||
|
||||
// GetOSInfo returns the OS information for this solver
|
||||
func (s *DNFJSONSolver) GetOSInfo() *osinfo.Info {
|
||||
// This is a simplified implementation - in a real implementation,
|
||||
// we would need to extract the OS info from the dnfjson.Solver
|
||||
return &osinfo.Info{}
|
||||
}
|
||||
|
||||
// AptSolverWrapper wraps our apt solver
|
||||
type AptSolverWrapper struct {
|
||||
*aptsolver.AptSolver
|
||||
}
|
||||
|
||||
// NewAptSolver creates a new apt solver
|
||||
func NewAptSolver(cacheDir string, arch arch.Arch, osInfo *osinfo.Info) *AptSolverWrapper {
|
||||
return &AptSolverWrapper{
|
||||
AptSolver: aptsolver.NewAptSolver(cacheDir, arch, osInfo),
|
||||
}
|
||||
}
|
||||
|
||||
// Depsolve resolves package dependencies using apt
|
||||
func (s *AptSolverWrapper) Depsolve(packages []string, maxAttempts int) (interface{}, error) {
|
||||
return s.AptSolver.Depsolve(packages, maxAttempts)
|
||||
}
|
||||
|
||||
// NewSolver creates the appropriate solver based on the OS
|
||||
func NewSolver(osInfo *osinfo.Info, cacheDir string, arch arch.Arch, dnfSolver *dnfjson.Solver) (Solver, error) {
|
||||
switch osInfo.OSRelease.ID {
|
||||
case "debian":
|
||||
return NewAptSolver(cacheDir, arch, osInfo), nil
|
||||
default:
|
||||
// For Fedora, RHEL, CentOS, etc., use the DNF solver
|
||||
return NewDNFSolver(dnfSolver), nil
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue