Delete internal/blueprint/ and import from osbuild/blueprint

Import osbuild/blueprint v1.6.0
This commit is contained in:
Achilleas Koutsou 2025-03-25 17:15:30 +01:00
parent 362712a71d
commit cf956ff5a6
93 changed files with 2300 additions and 4163 deletions

View file

@ -1,506 +0,0 @@
// Package blueprint contains primitives for representing weldr blueprints
package blueprint
import (
"encoding/json"
"fmt"
"github.com/osbuild/images/pkg/crypt"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/coreos/go-semver/semver"
iblueprint "github.com/osbuild/images/pkg/blueprint"
)
// A Blueprint is a high-level description of an image.
type Blueprint struct {
Name string `json:"name" toml:"name"`
Description string `json:"description" toml:"description"`
Version string `json:"version,omitempty" toml:"version,omitempty"`
Packages []Package `json:"packages" toml:"packages"`
Modules []Package `json:"modules" toml:"modules"`
// Note, this is called "enabled modules" because we already have "modules" except
// the "modules" refers to packages and "enabled modules" refers to modularity modules.
EnabledModules []EnabledModule `json:"enabled_modules" toml:"enabled_modules"`
Groups []Group `json:"groups" toml:"groups"`
Containers []Container `json:"containers,omitempty" toml:"containers,omitempty"`
Customizations *Customizations `json:"customizations,omitempty" toml:"customizations"`
Distro string `json:"distro" toml:"distro"`
Arch string `json:"architecture,omitempty" toml:"architecture,omitempty"`
}
type Change struct {
Commit string `json:"commit" toml:"commit"`
Message string `json:"message" toml:"message"`
Revision *int `json:"revision" toml:"revision"`
Timestamp string `json:"timestamp" toml:"timestamp"`
Blueprint Blueprint `json:"-" toml:"-"`
}
// A Package specifies an RPM package.
type Package struct {
Name string `json:"name" toml:"name"`
Version string `json:"version,omitempty" toml:"version,omitempty"`
}
// A module specifies a modularity stream.
type EnabledModule struct {
Name string `json:"name" toml:"name"`
Stream string `json:"stream,omitempty" toml:"stream,omitempty"`
}
// A group specifies an package group.
type Group struct {
Name string `json:"name" toml:"name"`
}
type Container struct {
Source string `json:"source,omitempty" toml:"source"`
Name string `json:"name,omitempty" toml:"name,omitempty"`
TLSVerify *bool `json:"tls-verify,omitempty" toml:"tls-verify,omitempty"`
LocalStorage bool `json:"local-storage,omitempty" toml:"local-storage,omitempty"`
}
// DeepCopy returns a deep copy of the blueprint
// This uses json.Marshal and Unmarshal which are not very efficient
func (b *Blueprint) DeepCopy() Blueprint {
bpJSON, err := json.Marshal(b)
if err != nil {
panic(err)
}
var bp Blueprint
err = json.Unmarshal(bpJSON, &bp)
if err != nil {
panic(err)
}
return bp
}
// Initialize ensures that the blueprint has sane defaults for any missing fields
func (b *Blueprint) Initialize() error {
if len(b.Name) == 0 {
return fmt.Errorf("empty blueprint name not allowed")
}
if b.Packages == nil {
b.Packages = []Package{}
}
if b.Modules == nil {
b.Modules = []Package{}
}
if b.EnabledModules == nil {
b.EnabledModules = []EnabledModule{}
}
if b.Groups == nil {
b.Groups = []Group{}
}
if b.Containers == nil {
b.Containers = []Container{}
}
if b.Version == "" {
b.Version = "0.0.0"
}
// Return an error if the version is not valid
_, err := semver.NewVersion(b.Version)
if err != nil {
return fmt.Errorf("Invalid 'version', must use Semantic Versioning: %s", err.Error())
}
err = b.CryptPasswords()
if err != nil {
return fmt.Errorf("Error hashing passwords: %s", err.Error())
}
for i, pkg := range b.Packages {
if pkg.Name == "" {
var errMsg string
if pkg.Version == "" {
errMsg = fmt.Sprintf("Entry #%d has no name.", i+1)
} else {
errMsg = fmt.Sprintf("Entry #%d has version '%v' but no name.", i+1, pkg.Version)
}
return fmt.Errorf("All package entries need to contain the name of the package. %s", errMsg)
}
}
return nil
}
// BumpVersion increments the previous blueprint's version
// If the old version string is not vaild semver it will use the new version as-is
// This assumes that the new blueprint's version has already been validated via Initialize
func (b *Blueprint) BumpVersion(old string) {
var ver *semver.Version
ver, err := semver.NewVersion(old)
if err != nil {
return
}
ver.BumpPatch()
b.Version = ver.String()
}
// packages, modules, and groups all resolve to rpm packages right now. This
// function returns a combined list of "name-version" strings.
func (b *Blueprint) GetPackages() []string {
return b.GetPackagesEx(true)
}
func (b *Blueprint) GetPackagesEx(bootable bool) []string {
packages := []string{}
for _, pkg := range b.Packages {
packages = append(packages, pkg.ToNameVersion())
}
for _, pkg := range b.Modules {
packages = append(packages, pkg.ToNameVersion())
}
for _, group := range b.Groups {
packages = append(packages, "@"+group.Name)
}
if bootable {
kc := b.Customizations.GetKernel()
kpkg := Package{Name: kc.Name}
packages = append(packages, kpkg.ToNameVersion())
}
return packages
}
func (p Package) ToNameVersion() string {
// Omit version to prevent all packages with prefix of name to be installed
if p.Version == "*" || p.Version == "" {
return p.Name
}
return p.Name + "-" + p.Version
}
func (b *Blueprint) GetEnabledModules() []string {
modules := []string{}
for _, mod := range b.EnabledModules {
modules = append(modules, mod.ToNameStream())
}
return modules
}
func (p EnabledModule) ToNameStream() string {
return p.Name + ":" + p.Stream
}
// CryptPasswords ensures that all blueprint passwords are hashed
func (b *Blueprint) CryptPasswords() error {
if b.Customizations == nil {
return nil
}
// Any passwords for users?
for i := range b.Customizations.User {
// Missing or empty password
if b.Customizations.User[i].Password == nil {
continue
}
// Prevent empty password from being hashed
if len(*b.Customizations.User[i].Password) == 0 {
b.Customizations.User[i].Password = nil
continue
}
if !crypt.PasswordIsCrypted(*b.Customizations.User[i].Password) {
pw, err := crypt.CryptSHA512(*b.Customizations.User[i].Password)
if err != nil {
return err
}
// Replace the password with the
b.Customizations.User[i].Password = &pw
}
}
return nil
}
func Convert(bp Blueprint) iblueprint.Blueprint {
var pkgs []iblueprint.Package
if len(bp.Packages) > 0 {
pkgs = make([]iblueprint.Package, len(bp.Packages))
for idx := range bp.Packages {
pkgs[idx] = iblueprint.Package(bp.Packages[idx])
}
}
var modules []iblueprint.Package
if len(bp.Modules) > 0 {
modules = make([]iblueprint.Package, len(bp.Modules))
for idx := range bp.Modules {
modules[idx] = iblueprint.Package(bp.Modules[idx])
}
}
var enabledModules []iblueprint.EnabledModule
if len(bp.EnabledModules) > 0 {
enabledModules = make([]iblueprint.EnabledModule, len(bp.EnabledModules))
for idx := range bp.EnabledModules {
enabledModules[idx] = iblueprint.EnabledModule(bp.EnabledModules[idx])
}
}
var groups []iblueprint.Group
if len(bp.Groups) > 0 {
groups = make([]iblueprint.Group, len(bp.Groups))
for idx := range bp.Groups {
groups[idx] = iblueprint.Group(bp.Groups[idx])
}
}
var containers []iblueprint.Container
if len(bp.Containers) > 0 {
containers = make([]iblueprint.Container, len(bp.Containers))
for idx := range bp.Containers {
containers[idx] = iblueprint.Container(bp.Containers[idx])
}
}
var customizations *iblueprint.Customizations
if c := bp.Customizations; c != nil {
customizations = &iblueprint.Customizations{
Hostname: c.Hostname,
InstallationDevice: c.InstallationDevice,
}
if fdo := c.FDO; fdo != nil {
ifdo := iblueprint.FDOCustomization(*fdo)
customizations.FDO = &ifdo
}
if oscap := c.OpenSCAP; oscap != nil {
ioscap := iblueprint.OpenSCAPCustomization{
DataStream: oscap.DataStream,
ProfileID: oscap.ProfileID,
}
if tailoring := oscap.Tailoring; tailoring != nil {
itailoring := iblueprint.OpenSCAPTailoringCustomizations(*tailoring)
ioscap.Tailoring = &itailoring
}
customizations.OpenSCAP = &ioscap
}
if ign := c.Ignition; ign != nil {
iign := iblueprint.IgnitionCustomization{}
if embed := ign.Embedded; embed != nil {
iembed := iblueprint.EmbeddedIgnitionCustomization(*embed)
iign.Embedded = &iembed
}
if fb := ign.FirstBoot; fb != nil {
ifb := iblueprint.FirstBootIgnitionCustomization(*fb)
iign.FirstBoot = &ifb
}
customizations.Ignition = &iign
}
if dirs := c.Directories; dirs != nil {
idirs := make([]iblueprint.DirectoryCustomization, len(dirs))
for idx := range dirs {
idirs[idx] = iblueprint.DirectoryCustomization(dirs[idx])
}
customizations.Directories = idirs
}
if files := c.Files; files != nil {
ifiles := make([]iblueprint.FileCustomization, len(files))
for idx := range files {
ifiles[idx] = iblueprint.FileCustomization(files[idx])
}
customizations.Files = ifiles
}
if repos := c.Repositories; repos != nil {
irepos := make([]iblueprint.RepositoryCustomization, len(repos))
for idx := range repos {
irepos[idx] = iblueprint.RepositoryCustomization(repos[idx])
}
customizations.Repositories = irepos
}
if kernel := c.Kernel; kernel != nil {
ikernel := iblueprint.KernelCustomization(*kernel)
customizations.Kernel = &ikernel
}
if users := c.GetUsers(); users != nil { // contains both user customizations and converted sshkey customizations
iusers := make([]iblueprint.UserCustomization, len(users))
for idx := range users {
iusers[idx] = iblueprint.UserCustomization(users[idx])
}
customizations.User = iusers
}
if groups := c.Group; groups != nil {
igroups := make([]iblueprint.GroupCustomization, len(groups))
for idx := range groups {
igroups[idx] = iblueprint.GroupCustomization(groups[idx])
}
customizations.Group = igroups
}
if fs := c.Filesystem; fs != nil {
ifs := make([]iblueprint.FilesystemCustomization, len(fs))
for idx := range fs {
ifs[idx] = iblueprint.FilesystemCustomization(fs[idx])
}
customizations.Filesystem = ifs
}
if disk := c.Disk; disk != nil {
idisk := &iblueprint.DiskCustomization{
Type: disk.Type,
MinSize: disk.MinSize,
Partitions: make([]iblueprint.PartitionCustomization, len(disk.Partitions)),
}
for idx, part := range disk.Partitions {
ipart := iblueprint.PartitionCustomization{
Type: part.Type,
PartType: part.PartType,
MinSize: part.MinSize,
BtrfsVolumeCustomization: iblueprint.BtrfsVolumeCustomization{},
VGCustomization: iblueprint.VGCustomization{
Name: part.VGCustomization.Name,
},
FilesystemTypedCustomization: iblueprint.FilesystemTypedCustomization(part.FilesystemTypedCustomization),
}
if len(part.LogicalVolumes) > 0 {
ipart.LogicalVolumes = make([]iblueprint.LVCustomization, len(part.LogicalVolumes))
for lvidx, lv := range part.LogicalVolumes {
ipart.LogicalVolumes[lvidx] = iblueprint.LVCustomization{
Name: lv.Name,
MinSize: lv.MinSize,
FilesystemTypedCustomization: iblueprint.FilesystemTypedCustomization(lv.FilesystemTypedCustomization),
}
}
}
if len(part.Subvolumes) > 0 {
ipart.Subvolumes = make([]iblueprint.BtrfsSubvolumeCustomization, len(part.Subvolumes))
for svidx, sv := range part.Subvolumes {
ipart.Subvolumes[svidx] = iblueprint.BtrfsSubvolumeCustomization(sv)
}
}
idisk.Partitions[idx] = ipart
}
customizations.Disk = idisk
}
if tz := c.Timezone; tz != nil {
itz := iblueprint.TimezoneCustomization(*tz)
customizations.Timezone = &itz
}
if locale := c.Locale; locale != nil {
ilocale := iblueprint.LocaleCustomization(*locale)
customizations.Locale = &ilocale
}
if fw := c.Firewall; fw != nil {
ifw := iblueprint.FirewallCustomization{
Ports: fw.Ports,
}
if services := fw.Services; services != nil {
iservices := iblueprint.FirewallServicesCustomization(*services)
ifw.Services = &iservices
}
if zones := fw.Zones; zones != nil {
izones := make([]iblueprint.FirewallZoneCustomization, len(zones))
for idx := range zones {
izones[idx] = iblueprint.FirewallZoneCustomization(zones[idx])
}
ifw.Zones = izones
}
customizations.Firewall = &ifw
}
if services := c.Services; services != nil {
iservices := iblueprint.ServicesCustomization(*services)
customizations.Services = &iservices
}
if fips := c.FIPS; fips != nil {
customizations.FIPS = fips
}
if installer := c.Installer; installer != nil {
iinst := iblueprint.InstallerCustomization{
Unattended: installer.Unattended,
SudoNopasswd: installer.SudoNopasswd,
}
if installer.Kickstart != nil {
iinst.Kickstart = &iblueprint.Kickstart{
Contents: installer.Kickstart.Contents,
}
}
if installer.Modules != nil {
iinst.Modules = &iblueprint.AnacondaModules{
Enable: installer.Modules.Enable,
Disable: installer.Modules.Disable,
}
}
customizations.Installer = &iinst
}
if rpm := c.RPM; rpm != nil && rpm.ImportKeys != nil {
irpm := iblueprint.RPMCustomization{
ImportKeys: &iblueprint.RPMImportKeys{
Files: rpm.ImportKeys.Files,
},
}
customizations.RPM = &irpm
}
if rhsm := c.RHSM; rhsm != nil && rhsm.Config != nil {
irhsm := iblueprint.RHSMCustomization{
Config: &iblueprint.RHSMConfig{},
}
if plugins := rhsm.Config.DNFPlugins; plugins != nil {
irhsm.Config.DNFPlugins = &iblueprint.SubManDNFPluginsConfig{}
if plugins.ProductID != nil && plugins.ProductID.Enabled != nil {
irhsm.Config.DNFPlugins.ProductID = &iblueprint.DNFPluginConfig{
Enabled: common.ToPtr(*plugins.ProductID.Enabled),
}
}
if plugins.SubscriptionManager != nil && plugins.SubscriptionManager.Enabled != nil {
irhsm.Config.DNFPlugins.SubscriptionManager = &iblueprint.DNFPluginConfig{
Enabled: common.ToPtr(*plugins.SubscriptionManager.Enabled),
}
}
}
if subManConf := rhsm.Config.SubscriptionManager; subManConf != nil {
irhsm.Config.SubscriptionManager = &iblueprint.SubManConfig{}
if subManConf.RHSMConfig != nil && subManConf.RHSMConfig.ManageRepos != nil {
irhsm.Config.SubscriptionManager.RHSMConfig = &iblueprint.SubManRHSMConfig{
ManageRepos: common.ToPtr(*subManConf.RHSMConfig.ManageRepos),
}
}
if subManConf.RHSMCertdConfig != nil && subManConf.RHSMCertdConfig.AutoRegistration != nil {
irhsm.Config.SubscriptionManager.RHSMCertdConfig = &iblueprint.SubManRHSMCertdConfig{
AutoRegistration: common.ToPtr(*subManConf.RHSMCertdConfig.AutoRegistration),
}
}
}
customizations.RHSM = &irhsm
}
if ca := c.CACerts; ca != nil {
ica := iblueprint.CACustomization{
PEMCerts: ca.PEMCerts,
}
customizations.CACerts = &ica
}
}
ibp := iblueprint.Blueprint{
Name: bp.Name,
Description: bp.Description,
Version: bp.Version,
Packages: pkgs,
Modules: modules,
EnabledModules: enabledModules,
Groups: groups,
Containers: containers,
Customizations: customizations,
Distro: bp.Distro,
}
return ibp
}

View file

@ -1,636 +0,0 @@
package blueprint
import (
"testing"
iblueprint "github.com/osbuild/images/pkg/blueprint"
"github.com/stretchr/testify/assert"
"github.com/osbuild/osbuild-composer/internal/common"
)
func TestConvert(t *testing.T) {
tests := []struct {
name string
src Blueprint
expected iblueprint.Blueprint
}{
{
name: "empty",
src: Blueprint{},
expected: iblueprint.Blueprint{},
},
{
name: "everything",
src: Blueprint{
Name: "name",
Description: "desc",
Version: "version",
Packages: []Package{
{
Name: "package-name",
Version: "package-version",
},
},
Modules: []Package{
{
Name: "module-name",
Version: "module-version",
},
},
Groups: []Group{
{
Name: "group-name",
},
},
Containers: []Container{
{
Source: "source",
Name: "name",
TLSVerify: common.ToPtr(true),
},
},
Customizations: &Customizations{
Hostname: common.ToPtr("hostname"),
Kernel: &KernelCustomization{
Name: "kernel-name",
Append: "kernel-append",
},
SSHKey: []SSHKeyCustomization{
{
User: "ssh-user",
Key: "ssh-key",
},
},
User: []UserCustomization{
{
Name: "user-name",
Description: common.ToPtr("user-desc"),
Password: common.ToPtr("user-password"),
Key: common.ToPtr("user-key"),
Home: common.ToPtr("/home/user"),
Shell: common.ToPtr("fish"),
Groups: []string{"wheel"},
UID: common.ToPtr(42),
GID: common.ToPtr(2023),
},
},
Group: []GroupCustomization{
{
Name: "group",
GID: common.ToPtr(7),
},
},
Timezone: &TimezoneCustomization{
Timezone: common.ToPtr("timezone"),
NTPServers: []string{"ntp-server"},
},
Locale: &LocaleCustomization{
Languages: []string{"language"},
Keyboard: common.ToPtr("keyboard"),
},
Firewall: &FirewallCustomization{
Ports: []string{"80"},
Services: &FirewallServicesCustomization{
Enabled: []string{"ssh"},
Disabled: []string{"ntp"},
},
Zones: []FirewallZoneCustomization{
{
Name: common.ToPtr("name"),
Sources: []string{"src"},
},
},
},
Services: &ServicesCustomization{
Enabled: []string{"osbuild-composer.service"},
Disabled: []string{"lorax-composer.service"},
},
Filesystem: []FilesystemCustomization{
{
Mountpoint: "/usr",
MinSize: 1024,
},
},
Disk: &DiskCustomization{
MinSize: 10240,
Type: "gpt",
Partitions: []PartitionCustomization{
{
// this partition is invalid, since only one of
// btrfs, vg, or filesystem should be set, but
// the converter copies everything
// unconditionally, so let's test the full
// thing
Type: "plain",
MinSize: 1024,
PartType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
BtrfsVolumeCustomization: BtrfsVolumeCustomization{
Subvolumes: []BtrfsSubvolumeCustomization{
{
Name: "subvol1",
Mountpoint: "/subvol1",
},
{
Name: "subvol2",
Mountpoint: "/subvol2",
},
},
},
VGCustomization: VGCustomization{
Name: "vg1",
LogicalVolumes: []LVCustomization{
{
Name: "vg1lv1",
MinSize: 0,
FilesystemTypedCustomization: FilesystemTypedCustomization{
Mountpoint: "/one",
Label: "one",
FSType: "xfs",
},
},
{
Name: "vg1lv2",
MinSize: 0,
FilesystemTypedCustomization: FilesystemTypedCustomization{
Mountpoint: "/two",
Label: "two",
FSType: "ext4",
},
},
},
},
FilesystemTypedCustomization: FilesystemTypedCustomization{
Mountpoint: "/root",
Label: "roothome",
FSType: "xfs",
},
},
{
Type: "plain",
MinSize: 1024,
FilesystemTypedCustomization: FilesystemTypedCustomization{
Mountpoint: "/root",
Label: "roothome",
FSType: "xfs",
},
},
{
Type: "lvm",
MinSize: 1024,
VGCustomization: VGCustomization{
Name: "vg1",
LogicalVolumes: []LVCustomization{
{
Name: "vg1lv1",
MinSize: 0,
FilesystemTypedCustomization: FilesystemTypedCustomization{
Mountpoint: "/one",
Label: "one",
FSType: "xfs",
},
},
{
Name: "vg1lv2",
MinSize: 0,
FilesystemTypedCustomization: FilesystemTypedCustomization{
Mountpoint: "/two",
Label: "two",
FSType: "ext4",
},
},
},
},
},
{
Type: "btrfs",
MinSize: 1024,
BtrfsVolumeCustomization: BtrfsVolumeCustomization{
Subvolumes: []BtrfsSubvolumeCustomization{
{
Name: "subvol1",
Mountpoint: "/subvol1",
},
{
Name: "subvol2",
Mountpoint: "/subvol2",
},
},
},
},
},
},
InstallationDevice: "/dev/sda",
FDO: &FDOCustomization{
ManufacturingServerURL: "http://manufacturing.fdo",
DiunPubKeyInsecure: "insecure-pubkey",
DiunPubKeyHash: "hash-pubkey",
DiunPubKeyRootCerts: "root-certs",
DiMfgStringTypeMacIface: "iface",
},
OpenSCAP: &OpenSCAPCustomization{
DataStream: "stream",
ProfileID: "profile",
Tailoring: &OpenSCAPTailoringCustomizations{
Selected: []string{"cloth"},
Unselected: []string{"leather"},
},
},
Ignition: &IgnitionCustomization{
Embedded: &EmbeddedIgnitionCustomization{
Config: "ignition-config",
},
FirstBoot: &FirstBootIgnitionCustomization{
ProvisioningURL: "http://provisioning.edge",
},
},
Directories: []DirectoryCustomization{
{
Path: "/dir",
User: common.ToPtr("dir-user"),
Group: common.ToPtr("dir-group"),
Mode: "0777",
EnsureParents: true,
},
},
Files: []FileCustomization{
{
Path: "/file",
User: common.ToPtr("file-user`"),
Group: common.ToPtr("file-group"),
Mode: "0755",
Data: "literal easter egg",
},
},
Repositories: []RepositoryCustomization{
{
Id: "repoid",
BaseURLs: []string{"http://baseurl"},
GPGKeys: []string{"repo-gpgkey"},
Metalink: "http://metalink",
Mirrorlist: "http://mirrorlist",
Name: "reponame",
Priority: common.ToPtr(987),
Enabled: common.ToPtr(true),
GPGCheck: common.ToPtr(true),
RepoGPGCheck: common.ToPtr(true),
SSLVerify: common.ToPtr(true),
Filename: "repofile",
},
},
Installer: &InstallerCustomization{
Unattended: true,
SudoNopasswd: []string{"%group", "user"},
Kickstart: &Kickstart{
Contents: "# test kickstart addition created by osbuild-composer",
},
Modules: &AnacondaModules{
Enable: []string{
"org.fedoraproject.Anaconda.Modules.Localization",
"org.fedoraproject.Anaconda.Modules.Users",
},
Disable: []string{
"org.fedoraproject.Anaconda.Modules.Network",
},
},
},
RPM: &RPMCustomization{
ImportKeys: &RPMImportKeys{
Files: []string{"/root/gpg-key"},
},
},
RHSM: &RHSMCustomization{
Config: &RHSMConfig{
DNFPlugins: &SubManDNFPluginsConfig{
ProductID: &DNFPluginConfig{
Enabled: common.ToPtr(true),
},
SubscriptionManager: &DNFPluginConfig{
Enabled: common.ToPtr(false),
},
},
SubscriptionManager: &SubManConfig{
RHSMConfig: &SubManRHSMConfig{
ManageRepos: common.ToPtr(true),
},
RHSMCertdConfig: &SubManRHSMCertdConfig{
AutoRegistration: common.ToPtr(false),
},
},
},
},
CACerts: &CACustomization{
PEMCerts: []string{"pem-cert"},
},
},
Distro: "distro",
},
expected: iblueprint.Blueprint{
Name: "name",
Description: "desc",
Version: "version",
Packages: []iblueprint.Package{
{
Name: "package-name",
Version: "package-version",
},
},
Modules: []iblueprint.Package{
{
Name: "module-name",
Version: "module-version",
},
},
Groups: []iblueprint.Group{
{
Name: "group-name",
},
},
Containers: []iblueprint.Container{
{
Source: "source",
Name: "name",
TLSVerify: common.ToPtr(true),
},
},
Customizations: &iblueprint.Customizations{
Hostname: common.ToPtr("hostname"),
Kernel: &iblueprint.KernelCustomization{
Name: "kernel-name",
Append: "kernel-append",
},
User: []iblueprint.UserCustomization{
{
Name: "ssh-user", // converted from sshkey
Key: common.ToPtr("ssh-key"),
},
{
Name: "user-name",
Description: common.ToPtr("user-desc"),
Password: common.ToPtr("user-password"),
Key: common.ToPtr("user-key"),
Home: common.ToPtr("/home/user"),
Shell: common.ToPtr("fish"),
Groups: []string{"wheel"},
UID: common.ToPtr(42),
GID: common.ToPtr(2023),
},
},
Group: []iblueprint.GroupCustomization{
{
Name: "group",
GID: common.ToPtr(7),
},
},
Timezone: &iblueprint.TimezoneCustomization{
Timezone: common.ToPtr("timezone"),
NTPServers: []string{"ntp-server"},
},
Locale: &iblueprint.LocaleCustomization{
Languages: []string{"language"},
Keyboard: common.ToPtr("keyboard"),
},
Firewall: &iblueprint.FirewallCustomization{
Ports: []string{"80"},
Services: &iblueprint.FirewallServicesCustomization{
Enabled: []string{"ssh"},
Disabled: []string{"ntp"},
},
Zones: []iblueprint.FirewallZoneCustomization{
{
Name: common.ToPtr("name"),
Sources: []string{"src"},
},
},
},
Services: &iblueprint.ServicesCustomization{
Enabled: []string{"osbuild-composer.service"},
Disabled: []string{"lorax-composer.service"},
},
Filesystem: []iblueprint.FilesystemCustomization{
{
Mountpoint: "/usr",
MinSize: 1024,
},
},
Disk: &iblueprint.DiskCustomization{
MinSize: 10240,
Type: "gpt",
Partitions: []iblueprint.PartitionCustomization{
{
// this partition is invalid, since only one of
// btrfs, vg, or filesystem should be set, but
// the converter copies everything
// unconditionally, so let's test the full
// thing
Type: "plain",
MinSize: 1024,
PartType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
BtrfsVolumeCustomization: iblueprint.BtrfsVolumeCustomization{
Subvolumes: []iblueprint.BtrfsSubvolumeCustomization{
{
Name: "subvol1",
Mountpoint: "/subvol1",
},
{
Name: "subvol2",
Mountpoint: "/subvol2",
},
},
},
VGCustomization: iblueprint.VGCustomization{
Name: "vg1",
LogicalVolumes: []iblueprint.LVCustomization{
{
Name: "vg1lv1",
MinSize: 0,
FilesystemTypedCustomization: iblueprint.FilesystemTypedCustomization{
Mountpoint: "/one",
Label: "one",
FSType: "xfs",
},
},
{
Name: "vg1lv2",
MinSize: 0,
FilesystemTypedCustomization: iblueprint.FilesystemTypedCustomization{
Mountpoint: "/two",
Label: "two",
FSType: "ext4",
},
},
},
},
FilesystemTypedCustomization: iblueprint.FilesystemTypedCustomization{
Mountpoint: "/root",
Label: "roothome",
FSType: "xfs",
},
},
{
Type: "plain",
MinSize: 1024,
FilesystemTypedCustomization: iblueprint.FilesystemTypedCustomization{
Mountpoint: "/root",
Label: "roothome",
FSType: "xfs",
},
},
{
Type: "lvm",
MinSize: 1024,
VGCustomization: iblueprint.VGCustomization{
Name: "vg1",
LogicalVolumes: []iblueprint.LVCustomization{
{
Name: "vg1lv1",
MinSize: 0,
FilesystemTypedCustomization: iblueprint.FilesystemTypedCustomization{
Mountpoint: "/one",
Label: "one",
FSType: "xfs",
},
},
{
Name: "vg1lv2",
MinSize: 0,
FilesystemTypedCustomization: iblueprint.FilesystemTypedCustomization{
Mountpoint: "/two",
Label: "two",
FSType: "ext4",
},
},
},
},
},
{
Type: "btrfs",
MinSize: 1024,
BtrfsVolumeCustomization: iblueprint.BtrfsVolumeCustomization{
Subvolumes: []iblueprint.BtrfsSubvolumeCustomization{
{
Name: "subvol1",
Mountpoint: "/subvol1",
},
{
Name: "subvol2",
Mountpoint: "/subvol2",
},
},
},
},
},
},
InstallationDevice: "/dev/sda",
FDO: &iblueprint.FDOCustomization{
ManufacturingServerURL: "http://manufacturing.fdo",
DiunPubKeyInsecure: "insecure-pubkey",
DiunPubKeyHash: "hash-pubkey",
DiunPubKeyRootCerts: "root-certs",
DiMfgStringTypeMacIface: "iface",
},
OpenSCAP: &iblueprint.OpenSCAPCustomization{
DataStream: "stream",
ProfileID: "profile",
Tailoring: &iblueprint.OpenSCAPTailoringCustomizations{
Selected: []string{"cloth"},
Unselected: []string{"leather"},
},
},
Ignition: &iblueprint.IgnitionCustomization{
Embedded: &iblueprint.EmbeddedIgnitionCustomization{
Config: "ignition-config",
},
FirstBoot: &iblueprint.FirstBootIgnitionCustomization{
ProvisioningURL: "http://provisioning.edge",
},
},
Directories: []iblueprint.DirectoryCustomization{
{
Path: "/dir",
User: common.ToPtr("dir-user"),
Group: common.ToPtr("dir-group"),
Mode: "0777",
EnsureParents: true,
},
},
Files: []iblueprint.FileCustomization{
{
Path: "/file",
User: common.ToPtr("file-user`"),
Group: common.ToPtr("file-group"),
Mode: "0755",
Data: "literal easter egg",
},
},
Repositories: []iblueprint.RepositoryCustomization{
{
Id: "repoid",
BaseURLs: []string{"http://baseurl"},
GPGKeys: []string{"repo-gpgkey"},
Metalink: "http://metalink",
Mirrorlist: "http://mirrorlist",
Name: "reponame",
Priority: common.ToPtr(987),
Enabled: common.ToPtr(true),
GPGCheck: common.ToPtr(true),
RepoGPGCheck: common.ToPtr(true),
SSLVerify: common.ToPtr(true),
Filename: "repofile",
},
},
Installer: &iblueprint.InstallerCustomization{
Unattended: true,
SudoNopasswd: []string{"%group", "user"},
Kickstart: &iblueprint.Kickstart{
Contents: "# test kickstart addition created by osbuild-composer",
},
Modules: &iblueprint.AnacondaModules{
Enable: []string{
"org.fedoraproject.Anaconda.Modules.Localization",
"org.fedoraproject.Anaconda.Modules.Users",
},
Disable: []string{
"org.fedoraproject.Anaconda.Modules.Network",
},
},
},
RPM: &iblueprint.RPMCustomization{
ImportKeys: &iblueprint.RPMImportKeys{
Files: []string{"/root/gpg-key"},
},
},
RHSM: &iblueprint.RHSMCustomization{
Config: &iblueprint.RHSMConfig{
DNFPlugins: &iblueprint.SubManDNFPluginsConfig{
ProductID: &iblueprint.DNFPluginConfig{
Enabled: common.ToPtr(true),
},
SubscriptionManager: &iblueprint.DNFPluginConfig{
Enabled: common.ToPtr(false),
},
},
SubscriptionManager: &iblueprint.SubManConfig{
RHSMConfig: &iblueprint.SubManRHSMConfig{
ManageRepos: common.ToPtr(true),
},
RHSMCertdConfig: &iblueprint.SubManRHSMCertdConfig{
AutoRegistration: common.ToPtr(false),
},
},
},
},
CACerts: &iblueprint.CACustomization{
PEMCerts: []string{"pem-cert"},
},
},
Distro: "distro",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, Convert(tt.src))
})
}
}

View file

@ -1,251 +0,0 @@
package blueprint
import (
"encoding/json"
"strings"
"testing"
"github.com/BurntSushi/toml"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBlueprintParse(t *testing.T) {
blueprint := `
name = "test"
description = "Test"
version = "0.0.0"
[[packages]]
name = "httpd"
version = "2.4.*"
[[customizations.filesystem]]
mountpoint = "/var"
size = 2147483648
[[customizations.filesystem]]
mountpoint = "/opt"
size = "20 GB"
`
var bp Blueprint
err := toml.Unmarshal([]byte(blueprint), &bp)
require.Nil(t, err)
assert.Equal(t, bp.Name, "test")
assert.Equal(t, "/var", bp.Customizations.Filesystem[0].Mountpoint)
assert.Equal(t, uint64(2147483648), bp.Customizations.Filesystem[0].MinSize)
assert.Equal(t, "/opt", bp.Customizations.Filesystem[1].Mountpoint)
assert.Equal(t, uint64(20*1000*1000*1000), bp.Customizations.Filesystem[1].MinSize)
blueprint = `{
"name": "test",
"customizations": {
"filesystem": [{
"mountpoint": "/opt",
"minsize": "20 GiB"
}]
}
}`
err = json.Unmarshal([]byte(blueprint), &bp)
require.Nil(t, err)
assert.Equal(t, bp.Name, "test")
assert.Equal(t, "/opt", bp.Customizations.Filesystem[0].Mountpoint)
assert.Equal(t, uint64(20*1024*1024*1024), bp.Customizations.Filesystem[0].MinSize)
}
func TestDeepCopy(t *testing.T) {
bpOrig := Blueprint{
Name: "deepcopy-test",
Description: "Testing DeepCopy function",
Version: "0.0.1",
Packages: []Package{
{Name: "dep-package1", Version: "*"}},
Modules: []Package{
{Name: "dep-package2", Version: "*"}},
}
bpCopy := bpOrig.DeepCopy()
require.Equalf(t, bpOrig, bpCopy, "Blueprints.DeepCopy is different from original.")
// Modify the copy
bpCopy.Packages[0].Version = "1.2.3"
require.Equalf(t, bpOrig.Packages[0].Version, "*", "Blueprint.DeepCopy failed, original modified")
// Modify the original
bpOrig.Packages[0].Version = "42.0"
require.Equalf(t, bpCopy.Packages[0].Version, "1.2.3", "Blueprint.DeepCopy failed, copy modified.")
}
func TestBlueprintInitialize(t *testing.T) {
cases := []struct {
NewBlueprint Blueprint
ExpectedError bool
}{
{Blueprint{Name: "bp-test-1", Description: "Empty version", Version: ""}, false},
{Blueprint{Name: "bp-test-2", Description: "Invalid version 1", Version: "0"}, true},
{Blueprint{Name: "bp-test-2", Description: "Invalid version 2", Version: "0.0"}, true},
{Blueprint{Name: "bp-test-3", Description: "Invalid version 3", Version: "0.0.0.0"}, true},
{Blueprint{Name: "bp-test-4", Description: "Invalid version 4", Version: "0.a.0"}, true},
{Blueprint{Name: "bp-test-5", Description: "Invalid version 5", Version: "foo"}, true},
{Blueprint{Name: "bp-test-7", Description: "Zero version", Version: "0.0.0"}, false},
{Blueprint{Name: "bp-test-8", Description: "X.Y.Z version", Version: "2.1.3"}, false},
}
for _, c := range cases {
bp := c.NewBlueprint
err := bp.Initialize()
assert.Equalf(t, (err != nil), c.ExpectedError, "Initialize(%#v) returnted an unexpected error: %#v", c.NewBlueprint, err)
}
}
func TestBumpVersion(t *testing.T) {
cases := []struct {
NewBlueprint Blueprint
OldVersion string
ExpectedVersion string
}{
{Blueprint{Name: "bp-test-1", Description: "Empty version", Version: "0.0.1"}, "", "0.0.1"},
{Blueprint{Name: "bp-test-2", Description: "Invalid version 1", Version: "0.0.1"}, "0", "0.0.1"},
{Blueprint{Name: "bp-test-3", Description: "Invalid version 2", Version: "0.0.1"}, "0.0.0.0", "0.0.1"},
{Blueprint{Name: "bp-test-4", Description: "Invalid version 3", Version: "0.0.1"}, "0.a.0", "0.0.1"},
{Blueprint{Name: "bp-test-5", Description: "Invalid version 4", Version: "0.0.1"}, "foo", "0.0.1"},
{Blueprint{Name: "bp-test-6", Description: "Invalid version 5", Version: "0.0.1"}, "0.0", "0.0.1"},
{Blueprint{Name: "bp-test-8", Description: "Same version", Version: "4.2.0"}, "4.2.0", "4.2.1"},
}
for _, c := range cases {
bp := c.NewBlueprint
err := bp.Initialize()
require.NoError(t, err)
bp.BumpVersion(c.OldVersion)
assert.Equalf(t, c.ExpectedVersion, bp.Version, "BumpVersion(%#v) is expected to return %#v, but instead returned %#v.", c.OldVersion, c.ExpectedVersion, bp.Version)
}
}
func TestGetPackages(t *testing.T) {
bp := Blueprint{
Name: "packages-test",
Description: "Testing GetPackages function",
Version: "0.0.1",
Packages: []Package{
{Name: "tmux", Version: "1.2"}},
Modules: []Package{
{Name: "openssh-server", Version: "*"}},
Groups: []Group{
{Name: "anaconda-tools"}},
}
Received_packages := bp.GetPackages()
assert.ElementsMatch(t, []string{"tmux-1.2", "openssh-server", "@anaconda-tools", "kernel"}, Received_packages)
}
func TestKernelNameCustomization(t *testing.T) {
kernels := []string{"kernel", "kernel-debug", "kernel-rt"}
for _, k := range kernels {
// kernel in customizations
bp := Blueprint{
Name: "kernel-test",
Description: "Testing GetPackages function with custom Kernel",
Version: "0.0.1",
Packages: []Package{
{Name: "tmux", Version: "1.2"}},
Modules: []Package{
{Name: "openssh-server", Version: "*"}},
Groups: []Group{
{Name: "anaconda-tools"}},
Customizations: &Customizations{
Kernel: &KernelCustomization{
Name: k,
},
},
}
Received_packages := bp.GetPackages()
assert.ElementsMatch(t, []string{"tmux-1.2", "openssh-server", "@anaconda-tools", k}, Received_packages)
}
for _, k := range kernels {
// kernel in packages
bp := Blueprint{
Name: "kernel-test",
Description: "Testing GetPackages function with custom Kernel",
Version: "0.0.1",
Packages: []Package{
{Name: "tmux", Version: "1.2"},
{Name: k},
},
Modules: []Package{
{Name: "openssh-server", Version: "*"}},
Groups: []Group{
{Name: "anaconda-tools"}},
}
Received_packages := bp.GetPackages()
// adds default kernel as well
assert.ElementsMatch(t, []string{"tmux-1.2", k, "openssh-server", "@anaconda-tools", "kernel"}, Received_packages)
}
for _, bk := range kernels {
for _, ck := range kernels {
// all combos of both kernels
bp := Blueprint{
Name: "kernel-test",
Description: "Testing GetPackages function with custom Kernel",
Version: "0.0.1",
Packages: []Package{
{Name: "tmux", Version: "1.2"},
{Name: bk},
},
Modules: []Package{
{Name: "openssh-server", Version: "*"}},
Groups: []Group{
{Name: "anaconda-tools"}},
Customizations: &Customizations{
Kernel: &KernelCustomization{
Name: ck,
},
},
}
Received_packages := bp.GetPackages()
// both kernels are included, even if they're the same
assert.ElementsMatch(t, []string{"tmux-1.2", bk, "openssh-server", "@anaconda-tools", ck}, Received_packages)
}
}
}
// TestBlueprintPasswords check to make sure all passwords are hashed
func TestBlueprintPasswords(t *testing.T) {
blueprint := `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.user]]
name = "bart"
password = "nobodysawmedoit"
[[customizations.user]]
name = "lisa"
password = "$6$RWdHzrPfoM6BMuIP$gKYlBXQuJgP.G2j2twbOyxYjFDPUQw8Jp.gWe1WD/obX0RMyfgw5vt.Mn/tLLX4mQjaklSiIzoAW3HrVQRg4Q."
[[customizations.user]]
name = "maggie"
password = ""
`
var bp Blueprint
err := toml.Unmarshal([]byte(blueprint), &bp)
require.Nil(t, err)
require.Nil(t, bp.Initialize())
// Note: User entries are in the same order as the toml
users := bp.Customizations.GetUsers()
assert.Equal(t, "bart", users[0].Name)
assert.True(t, strings.HasPrefix(*users[0].Password, "$6$"))
assert.Equal(t, "lisa", users[1].Name)
assert.Equal(t, "$6$RWdHzrPfoM6BMuIP$gKYlBXQuJgP.G2j2twbOyxYjFDPUQw8Jp.gWe1WD/obX0RMyfgw5vt.Mn/tLLX4mQjaklSiIzoAW3HrVQRg4Q.", *users[1].Password)
assert.Equal(t, "maggie", users[2].Name)
assert.Nil(t, users[2].Password)
}

View file

@ -1,409 +0,0 @@
package blueprint
import (
"fmt"
"reflect"
"strings"
"github.com/osbuild/images/pkg/disk"
)
type Customizations struct {
Hostname *string `json:"hostname,omitempty" toml:"hostname,omitempty"`
Kernel *KernelCustomization `json:"kernel,omitempty" toml:"kernel,omitempty"`
SSHKey []SSHKeyCustomization `json:"sshkey,omitempty" toml:"sshkey,omitempty"`
User []UserCustomization `json:"user,omitempty" toml:"user,omitempty"`
Group []GroupCustomization `json:"group,omitempty" toml:"group,omitempty"`
Timezone *TimezoneCustomization `json:"timezone,omitempty" toml:"timezone,omitempty"`
Locale *LocaleCustomization `json:"locale,omitempty" toml:"locale,omitempty"`
Firewall *FirewallCustomization `json:"firewall,omitempty" toml:"firewall,omitempty"`
Services *ServicesCustomization `json:"services,omitempty" toml:"services,omitempty"`
Filesystem []FilesystemCustomization `json:"filesystem,omitempty" toml:"filesystem,omitempty"`
Disk *DiskCustomization `json:"disk,omitempty" toml:"disk,omitempty"`
InstallationDevice string `json:"installation_device,omitempty" toml:"installation_device,omitempty"`
PartitioningMode string `json:"partitioning_mode,omitempty" toml:"partitioning_mode,omitempty"`
FDO *FDOCustomization `json:"fdo,omitempty" toml:"fdo,omitempty"`
OpenSCAP *OpenSCAPCustomization `json:"openscap,omitempty" toml:"openscap,omitempty"`
Ignition *IgnitionCustomization `json:"ignition,omitempty" toml:"ignition,omitempty"`
Directories []DirectoryCustomization `json:"directories,omitempty" toml:"directories,omitempty"`
Files []FileCustomization `json:"files,omitempty" toml:"files,omitempty"`
Repositories []RepositoryCustomization `json:"repositories,omitempty" toml:"repositories,omitempty"`
FIPS *bool `json:"fips,omitempty" toml:"fips,omitempty"`
Installer *InstallerCustomization `json:"installer,omitempty" toml:"installer,omitempty"`
RPM *RPMCustomization `json:"rpm,omitempty" toml:"rpm,omitempty"`
RHSM *RHSMCustomization `json:"rhsm,omitempty" toml:"rhsm,omitempty"`
CACerts *CACustomization `json:"cacerts,omitempty" toml:"cacerts,omitempty"`
}
type IgnitionCustomization struct {
Embedded *EmbeddedIgnitionCustomization `json:"embedded,omitempty" toml:"embedded,omitempty"`
FirstBoot *FirstBootIgnitionCustomization `json:"firstboot,omitempty" toml:"firstboot,omitempty"`
}
type EmbeddedIgnitionCustomization struct {
Config string `json:"config,omitempty" toml:"config,omitempty"`
}
type FirstBootIgnitionCustomization struct {
ProvisioningURL string `json:"url,omitempty" toml:"url,omitempty"`
}
type FDOCustomization struct {
ManufacturingServerURL string `json:"manufacturing_server_url,omitempty" toml:"manufacturing_server_url,omitempty"`
DiunPubKeyInsecure string `json:"diun_pub_key_insecure,omitempty" toml:"diun_pub_key_insecure,omitempty"`
// This is the output of:
// echo "sha256:$(openssl x509 -fingerprint -sha256 -noout -in diun_cert.pem | cut -d"=" -f2 | sed 's/://g')"
DiunPubKeyHash string `json:"diun_pub_key_hash,omitempty" toml:"diun_pub_key_hash,omitempty"`
DiunPubKeyRootCerts string `json:"diun_pub_key_root_certs,omitempty" toml:"diun_pub_key_root_certs,omitempty"`
DiMfgStringTypeMacIface string `json:"di_mfg_string_type_mac_iface,omitempty" toml:"di_mfg_string_type_mac_iface,omitempty"`
}
type KernelCustomization struct {
Name string `json:"name,omitempty" toml:"name,omitempty"`
Append string `json:"append" toml:"append"`
}
type SSHKeyCustomization struct {
User string `json:"user" toml:"user"`
Key string `json:"key" toml:"key"`
}
type UserCustomization struct {
Name string `json:"name" toml:"name"`
Description *string `json:"description,omitempty" toml:"description,omitempty"`
Password *string `json:"password,omitempty" toml:"password,omitempty"`
Key *string `json:"key,omitempty" toml:"key,omitempty"`
Home *string `json:"home,omitempty" toml:"home,omitempty"`
Shell *string `json:"shell,omitempty" toml:"shell,omitempty"`
Groups []string `json:"groups,omitempty" toml:"groups,omitempty"`
UID *int `json:"uid,omitempty" toml:"uid,omitempty"`
GID *int `json:"gid,omitempty" toml:"gid,omitempty"`
ExpireDate *int `json:"expiredate,omitempty" toml:"expiredate,omitempty"`
ForcePasswordReset *bool `json:"force_password_reset,omitempty" toml:"force_password_reset,omitempty"`
}
type GroupCustomization struct {
Name string `json:"name" toml:"name"`
GID *int `json:"gid,omitempty" toml:"gid,omitempty"`
}
type TimezoneCustomization struct {
Timezone *string `json:"timezone,omitempty" toml:"timezone,omitempty"`
NTPServers []string `json:"ntpservers,omitempty" toml:"ntpservers,omitempty"`
}
type LocaleCustomization struct {
Languages []string `json:"languages,omitempty" toml:"languages,omitempty"`
Keyboard *string `json:"keyboard,omitempty" toml:"keyboard,omitempty"`
}
type FirewallCustomization struct {
Ports []string `json:"ports,omitempty" toml:"ports,omitempty"`
Services *FirewallServicesCustomization `json:"services,omitempty" toml:"services,omitempty"`
Zones []FirewallZoneCustomization `json:"zones,omitempty" toml:"zones,omitempty"`
}
type FirewallZoneCustomization struct {
Name *string `json:"name,omitempty" toml:"name,omitempty"`
Sources []string `json:"sources,omitempty" toml:"sources,omitempty"`
}
type FirewallServicesCustomization struct {
Enabled []string `json:"enabled,omitempty" toml:"enabled,omitempty"`
Disabled []string `json:"disabled,omitempty" toml:"disabled,omitempty"`
}
type ServicesCustomization struct {
Enabled []string `json:"enabled,omitempty" toml:"enabled,omitempty"`
Disabled []string `json:"disabled,omitempty" toml:"disabled,omitempty"`
Masked []string `json:"masked,omitempty" toml:"masked,omitempty"`
}
type OpenSCAPCustomization struct {
DataStream string `json:"datastream,omitempty" toml:"datastream,omitempty"`
ProfileID string `json:"profile_id,omitempty" toml:"profile_id,omitempty"`
Tailoring *OpenSCAPTailoringCustomizations `json:"tailoring,omitempty" toml:"tailoring,omitempty"`
JSONTailoring *OpenSCAPJSONTailoringCustomizations `json:"json_tailoring,omitempty" toml:"json_tailoring,omitempty"`
PolicyID string `json:"policy_id,omitempty" toml:"policy_id,omitempty"`
}
type OpenSCAPTailoringCustomizations struct {
Selected []string `json:"selected,omitempty" toml:"selected,omitempty"`
Unselected []string `json:"unselected,omitempty" toml:"unselected,omitempty"`
}
type OpenSCAPJSONTailoringCustomizations struct {
ProfileID string `json:"profile_id,omitempty" toml:"profile_id,omitempty"`
Filepath string `json:"filepath,omitempty" toml:"filepath,omitempty"`
}
type CACustomization struct {
PEMCerts []string `json:"pem_certs,omitempty" toml:"pem_certs,omitempty"`
}
type CustomizationError struct {
Message string
}
func (e *CustomizationError) Error() string {
return e.Message
}
// CheckCustomizations returns an error of type `CustomizationError`
// if `c` has any customizations not specified in `allowed`
func (c *Customizations) CheckAllowed(allowed ...string) error {
if c == nil {
return nil
}
allowMap := make(map[string]bool)
for _, a := range allowed {
allowMap[a] = true
}
t := reflect.TypeOf(*c)
v := reflect.ValueOf(*c)
for i := 0; i < t.NumField(); i++ {
empty := false
field := v.Field(i)
switch field.Kind() {
case reflect.String:
if field.String() == "" {
empty = true
}
case reflect.Array, reflect.Slice:
if field.Len() == 0 {
empty = true
}
case reflect.Ptr:
if field.IsNil() {
empty = true
}
default:
panic(fmt.Sprintf("unhandled customization field type %s, %s", v.Kind(), t.Field(i).Name))
}
if !empty && !allowMap[t.Field(i).Name] {
return &CustomizationError{fmt.Sprintf("'%s' is not allowed", t.Field(i).Name)}
}
}
return nil
}
func (c *Customizations) GetHostname() *string {
if c == nil {
return nil
}
return c.Hostname
}
func (c *Customizations) GetPrimaryLocale() (*string, *string) {
if c == nil {
return nil, nil
}
if c.Locale == nil {
return nil, nil
}
if len(c.Locale.Languages) == 0 {
return nil, c.Locale.Keyboard
}
return &c.Locale.Languages[0], c.Locale.Keyboard
}
func (c *Customizations) GetTimezoneSettings() (*string, []string) {
if c == nil {
return nil, nil
}
if c.Timezone == nil {
return nil, nil
}
return c.Timezone.Timezone, c.Timezone.NTPServers
}
func (c *Customizations) GetUsers() []UserCustomization {
if c == nil {
return nil
}
users := []UserCustomization{}
// prepend sshkey for backwards compat (overridden by users)
if len(c.SSHKey) > 0 {
for _, k := range c.SSHKey {
key := k.Key
users = append(users, UserCustomization{
Name: k.User,
Key: &key,
})
}
}
users = append(users, c.User...)
// sanitize user home directory in blueprint: if it has a trailing slash,
// it might lead to the directory not getting the correct selinux labels
for idx := range users {
u := users[idx]
if u.Home != nil {
homedir := strings.TrimRight(*u.Home, "/")
u.Home = &homedir
users[idx] = u
}
}
return users
}
func (c *Customizations) GetGroups() []GroupCustomization {
if c == nil {
return nil
}
return c.Group
}
func (c *Customizations) GetKernel() *KernelCustomization {
var name string
var append string
if c != nil && c.Kernel != nil {
name = c.Kernel.Name
append = c.Kernel.Append
}
if name == "" {
name = "kernel"
}
return &KernelCustomization{
Name: name,
Append: append,
}
}
func (c *Customizations) GetFirewall() *FirewallCustomization {
if c == nil {
return nil
}
return c.Firewall
}
func (c *Customizations) GetServices() *ServicesCustomization {
if c == nil {
return nil
}
return c.Services
}
func (c *Customizations) GetFilesystems() []FilesystemCustomization {
if c == nil {
return nil
}
return c.Filesystem
}
func (c *Customizations) GetFilesystemsMinSize() uint64 {
if c == nil {
return 0
}
var agg uint64
for _, m := range c.Filesystem {
agg += m.MinSize
}
// This ensures that file system customization `size` is a multiple of
// sector size (512)
if agg%512 != 0 {
agg = (agg/512 + 1) * 512
}
return agg
}
// GetPartitioningMode converts the string to a disk.PartitioningMode type
func (c *Customizations) GetPartitioningMode() (disk.PartitioningMode, error) {
if c == nil {
return disk.DefaultPartitioningMode, nil
}
switch c.PartitioningMode {
case "raw":
return disk.RawPartitioningMode, nil
case "lvm":
return disk.LVMPartitioningMode, nil
case "auto-lvm":
return disk.AutoLVMPartitioningMode, nil
case "":
return disk.DefaultPartitioningMode, nil
default:
return disk.DefaultPartitioningMode, fmt.Errorf("invalid partitioning mode '%s'", c.PartitioningMode)
}
}
func (c *Customizations) GetInstallationDevice() string {
if c == nil || c.InstallationDevice == "" {
return ""
}
return c.InstallationDevice
}
func (c *Customizations) GetFDO() *FDOCustomization {
if c == nil {
return nil
}
return c.FDO
}
func (c *Customizations) GetOpenSCAP() *OpenSCAPCustomization {
if c == nil {
return nil
}
return c.OpenSCAP
}
func (c *Customizations) GetIgnition() *IgnitionCustomization {
if c == nil {
return nil
}
return c.Ignition
}
func (c *Customizations) GetDirectories() []DirectoryCustomization {
if c == nil {
return nil
}
return c.Directories
}
func (c *Customizations) GetFiles() []FileCustomization {
if c == nil {
return nil
}
return c.Files
}
func (c *Customizations) GetRepositories() ([]RepositoryCustomization, error) {
if c == nil {
return nil, nil
}
for idx := range c.Repositories {
err := validateCustomRepository(&c.Repositories[idx])
if err != nil {
return nil, err
}
}
return c.Repositories, nil
}
func (c *Customizations) GetFIPS() bool {
if c == nil || c.FIPS == nil {
return false
}
return *c.FIPS
}

View file

@ -1,345 +0,0 @@
package blueprint
import (
"testing"
"github.com/osbuild/images/pkg/disk"
"github.com/stretchr/testify/assert"
)
func TestCheckAllowed(t *testing.T) {
Desc := "Test descritpion"
Pass := "testpass"
Key := "testkey"
Home := "Home"
Shell := "Shell"
Groups := []string{
"Group",
}
UID := 123
GID := 321
expectedUsers := []UserCustomization{
{
Name: "John",
Description: &Desc,
Password: &Pass,
Key: &Key,
Home: &Home,
Shell: &Shell,
Groups: Groups,
UID: &UID,
GID: &GID,
},
}
expectedHostname := "Hostname"
x := Customizations{Hostname: &expectedHostname, User: expectedUsers}
err := x.CheckAllowed("Hostname", "User")
assert.NoError(t, err)
// "User" not allowed anymore
err = x.CheckAllowed("Hostname")
assert.Error(t, err)
// "Hostname" not allowed anymore
err = x.CheckAllowed("User")
assert.Error(t, err)
}
func TestGetHostname(t *testing.T) {
expectedHostname := "Hostname"
TestCustomizations := Customizations{
Hostname: &expectedHostname,
}
retHostname := TestCustomizations.GetHostname()
assert.Equal(t, &expectedHostname, retHostname)
}
func TestGetKernel(t *testing.T) {
expectedKernel := KernelCustomization{
Append: "--test",
Name: "kernel",
}
TestCustomizations := Customizations{
Kernel: &expectedKernel,
}
retKernel := TestCustomizations.GetKernel()
assert.Equal(t, &expectedKernel, retKernel)
}
func TestSSHKey(t *testing.T) {
expectedSSHKeys := []SSHKeyCustomization{
{
User: "test-user",
Key: "test-key",
},
}
TestCustomizations := Customizations{
SSHKey: expectedSSHKeys,
}
retUser := TestCustomizations.GetUsers()[0].Name
retKey := *TestCustomizations.GetUsers()[0].Key
assert.Equal(t, expectedSSHKeys[0].User, retUser)
assert.Equal(t, expectedSSHKeys[0].Key, retKey)
}
func TestGetUsers(t *testing.T) {
Desc := "Test descritpion"
Pass := "testpass"
Key := "testkey"
Home := "Home"
Shell := "Shell"
Groups := []string{
"Group",
}
UID := 123
GID := 321
ExpireDate := 12345
expectedUsers := []UserCustomization{
{
Name: "John",
Description: &Desc,
Password: &Pass,
Key: &Key,
Home: &Home,
Shell: &Shell,
Groups: Groups,
UID: &UID,
GID: &GID,
ExpireDate: &ExpireDate,
},
}
TestCustomizations := Customizations{
User: expectedUsers,
}
retUsers := TestCustomizations.GetUsers()
assert.ElementsMatch(t, expectedUsers, retUsers)
}
func TestGetGroups(t *testing.T) {
GID := 1234
expectedGroups := []GroupCustomization{
{
Name: "TestGroup",
GID: &GID,
},
}
TestCustomizations := Customizations{
Group: expectedGroups,
}
retGroups := TestCustomizations.GetGroups()
assert.ElementsMatch(t, expectedGroups, retGroups)
}
func TestGetTimezoneSettings(t *testing.T) {
expectedTimezone := "testZONE"
expectedNTPServers := []string{
"server",
}
expectedTimezoneCustomization := TimezoneCustomization{
Timezone: &expectedTimezone,
NTPServers: expectedNTPServers,
}
TestCustomizations := Customizations{
Timezone: &expectedTimezoneCustomization,
}
retTimezone, retNTPServers := TestCustomizations.GetTimezoneSettings()
assert.Equal(t, expectedTimezone, *retTimezone)
assert.Equal(t, expectedNTPServers, retNTPServers)
}
func TestGetPrimaryLocale(t *testing.T) {
expectedLanguages := []string{
"enUS",
}
expectedKeyboard := "en"
expectedLocaleCustomization := LocaleCustomization{
Languages: expectedLanguages,
Keyboard: &expectedKeyboard,
}
TestCustomizations := Customizations{
Locale: &expectedLocaleCustomization,
}
retLanguage, retKeyboard := TestCustomizations.GetPrimaryLocale()
assert.Equal(t, expectedLanguages[0], *retLanguage)
assert.Equal(t, expectedKeyboard, *retKeyboard)
}
func TestGetFirewall(t *testing.T) {
expectedPorts := []string{"22", "9090"}
expectedServices := FirewallServicesCustomization{
Enabled: []string{"cockpit", "osbuild-composer"},
Disabled: []string{"TCP", "httpd"},
}
expectedFirewall := FirewallCustomization{
Ports: expectedPorts,
Services: &expectedServices,
}
TestCustomizations := Customizations{
Firewall: &expectedFirewall,
}
retFirewall := TestCustomizations.GetFirewall()
assert.ElementsMatch(t, expectedFirewall.Ports, retFirewall.Ports)
assert.ElementsMatch(t, expectedFirewall.Services.Enabled, retFirewall.Services.Enabled)
assert.ElementsMatch(t, expectedFirewall.Services.Disabled, retFirewall.Services.Disabled)
}
func TestGetServices(t *testing.T) {
expectedServices := ServicesCustomization{
Enabled: []string{"cockpit", "osbuild-composer"},
Disabled: []string{"sshd", "ftp"},
Masked: []string{"firewalld"},
}
TestCustomizations := Customizations{
Services: &expectedServices,
}
retServices := TestCustomizations.GetServices()
assert.ElementsMatch(t, expectedServices.Enabled, retServices.Enabled)
assert.ElementsMatch(t, expectedServices.Disabled, retServices.Disabled)
assert.ElementsMatch(t, expectedServices.Masked, retServices.Masked)
}
func TestError(t *testing.T) {
expectedError := CustomizationError{
Message: "test error",
}
retError := expectedError.Error()
assert.Equal(t, expectedError.Message, retError)
}
// This tests calling all the functions on a Blueprint with no Customizations
func TestNoCustomizationsInBlueprint(t *testing.T) {
TestBP := Blueprint{}
assert.Nil(t, TestBP.Customizations.GetHostname())
assert.Nil(t, TestBP.Customizations.GetUsers())
assert.Nil(t, TestBP.Customizations.GetGroups())
assert.Equal(t, &KernelCustomization{Name: "kernel"}, TestBP.Customizations.GetKernel())
assert.Nil(t, TestBP.Customizations.GetFirewall())
assert.Nil(t, TestBP.Customizations.GetServices())
nilLanguage, nilKeyboard := TestBP.Customizations.GetPrimaryLocale()
assert.Nil(t, nilLanguage)
assert.Nil(t, nilKeyboard)
nilTimezone, nilNTPServers := TestBP.Customizations.GetTimezoneSettings()
assert.Nil(t, nilTimezone)
assert.Nil(t, nilNTPServers)
}
// This tests additional scenarios where GetPrimaryLocale() returns nil values
func TestNilGetPrimaryLocale(t *testing.T) {
// Case empty Customization
TestCustomizationsEmpty := Customizations{}
retLanguage, retKeyboard := TestCustomizationsEmpty.GetPrimaryLocale()
assert.Nil(t, retLanguage)
assert.Nil(t, retKeyboard)
// Case empty Languages
expectedKeyboard := "en"
expectedLocaleCustomization := LocaleCustomization{
Keyboard: &expectedKeyboard,
}
TestCustomizations := Customizations{
Locale: &expectedLocaleCustomization,
}
retLanguage, retKeyboard = TestCustomizations.GetPrimaryLocale()
assert.Nil(t, retLanguage)
assert.Equal(t, expectedKeyboard, *retKeyboard)
}
// This tests additional scenario where GetTimezoneSEtting() returns nil values
func TestNilGetTimezoneSettings(t *testing.T) {
TestCustomizationsEmpty := Customizations{}
retTimezone, retNTPServers := TestCustomizationsEmpty.GetTimezoneSettings()
assert.Nil(t, retTimezone)
assert.Nil(t, retNTPServers)
}
func TestGetOpenSCAPConfig(t *testing.T) {
expectedOscap := OpenSCAPCustomization{
DataStream: "test-data-stream.xml",
ProfileID: "test_profile",
}
TestCustomizations := Customizations{
OpenSCAP: &expectedOscap,
}
retOpenSCAPCustomiztions := TestCustomizations.GetOpenSCAP()
assert.EqualValues(t, expectedOscap, *retOpenSCAPCustomiztions)
}
func TestGetPartitioningMode(t *testing.T) {
// No customizations returns Default which is actually AutoLVM,
// but that is handled by the images code
var c *Customizations
pm, err := c.GetPartitioningMode()
assert.NoError(t, err)
assert.Equal(t, disk.DefaultPartitioningMode, pm)
// Empty defaults to Default which is actually AutoLVM,
// but that is handled by the images code
c = &Customizations{}
_, err = c.GetPartitioningMode()
assert.NoError(t, err)
assert.Equal(t, disk.DefaultPartitioningMode, pm)
// Unknown mode returns an error
c = &Customizations{
PartitioningMode: "all-of-them",
}
_, err = c.GetPartitioningMode()
assert.Error(t, err)
// And a known mode returns the correct type
c = &Customizations{
PartitioningMode: "lvm",
}
pm, err = c.GetPartitioningMode()
assert.NoError(t, err)
assert.Equal(t, disk.LVMPartitioningMode, pm)
}

View file

@ -1,363 +0,0 @@
package blueprint
import (
"bytes"
"encoding/json"
"fmt"
"github.com/osbuild/images/pkg/datasizes"
)
type DiskCustomization struct {
Type string
MinSize uint64
Partitions []PartitionCustomization
}
type diskCustomizationMarshaler struct {
MinSize datasizes.Size `json:"minsize,omitempty" toml:"minsize,omitempty"`
Partitions []PartitionCustomization `json:"partitions,omitempty" toml:"partitions,omitempty"`
}
func (dc *DiskCustomization) UnmarshalJSON(data []byte) error {
var dcm diskCustomizationMarshaler
if err := json.Unmarshal(data, &dcm); err != nil {
return err
}
dc.MinSize = dcm.MinSize.Uint64()
dc.Partitions = dcm.Partitions
return nil
}
func (dc *DiskCustomization) UnmarshalTOML(data any) error {
return unmarshalTOMLviaJSON(dc, data)
}
// PartitionCustomization defines a single partition on a disk. The Type
// defines the kind of "payload" for the partition: plain, lvm, or btrfs.
// - plain: the payload will be a filesystem on a partition (e.g. xfs, ext4).
// See [FilesystemTypedCustomization] for extra fields.
// - lvm: the payload will be an LVM volume group. See [VGCustomization] for
// extra fields
// - btrfs: the payload will be a btrfs volume. See
// [BtrfsVolumeCustomization] for extra fields.
type PartitionCustomization struct {
// The type of payload for the partition (optional, defaults to "plain").
Type string `json:"type" toml:"type"`
// Minimum size of the partition that contains the filesystem (for "plain"
// filesystem), volume group ("lvm"), or btrfs volume ("btrfs"). The final
// size of the partition will be larger than the minsize if the sum of the
// contained volumes (logical volumes or subvolumes) is larger. In
// addition, certain mountpoints have required minimum sizes. See
// https://osbuild.org/docs/user-guide/partitioning for more details.
// (optional, defaults depend on payload and mountpoints).
MinSize uint64 `json:"minsize" toml:"minsize"`
// The partition type GUID for GPT partitions. For DOS partitions, this
// field can be used to set the (2 hex digit) partition type.
// If not set, the type will be automatically set based on the mountpoint
// or the payload type.
PartType string `json:"part_type,omitempty" toml:"part_type,omitempty"`
BtrfsVolumeCustomization
VGCustomization
FilesystemTypedCustomization
}
// A filesystem on a plain partition or LVM logical volume.
// Note the differences from [FilesystemCustomization]:
// - Adds a label.
// - Adds a filesystem type (fs_type).
// - Does not define a size. The size is defined by its container: a
// partition ([PartitionCustomization]) or LVM logical volume
// ([LVCustomization]).
//
// Setting the FSType to "swap" creates a swap area (and the Mountpoint must be
// empty).
type FilesystemTypedCustomization struct {
Mountpoint string `json:"mountpoint,omitempty" toml:"mountpoint,omitempty"`
Label string `json:"label,omitempty" toml:"label,omitempty"`
FSType string `json:"fs_type,omitempty" toml:"fs_type,omitempty"`
}
// An LVM volume group with one or more logical volumes.
type VGCustomization struct {
// Volume group name (optional, default will be automatically generated).
Name string `json:"name,omitempty" toml:"name,omitempty"`
LogicalVolumes []LVCustomization `json:"logical_volumes,omitempty" toml:"logical_volumes,omitempty"`
}
type LVCustomization struct {
// Logical volume name
Name string `json:"name,omitempty" toml:"name,omitempty"`
// Minimum size of the logical volume
MinSize uint64 `json:"minsize,omitempty" toml:"minsize,omitempty"`
FilesystemTypedCustomization
}
// Custom JSON unmarshaller for LVCustomization for handling the conversion of
// data sizes (minsize) expressed as strings to uint64.
func (lv *LVCustomization) UnmarshalJSON(data []byte) error {
var lvAnySize struct {
Name string `json:"name,omitempty" toml:"name,omitempty"`
MinSize any `json:"minsize,omitempty" toml:"minsize,omitempty"`
FilesystemTypedCustomization
}
if err := json.Unmarshal(data, &lvAnySize); err != nil {
return err
}
lv.Name = lvAnySize.Name
lv.FilesystemTypedCustomization = lvAnySize.FilesystemTypedCustomization
if lvAnySize.MinSize == nil {
return fmt.Errorf("minsize is required")
}
size, err := decodeSize(lvAnySize.MinSize)
if err != nil {
return err
}
lv.MinSize = size
return nil
}
// A btrfs volume consisting of one or more subvolumes.
type BtrfsVolumeCustomization struct {
Subvolumes []BtrfsSubvolumeCustomization `json:"subvolumes,omitempty" toml:"subvolumes,omitempty"`
}
type BtrfsSubvolumeCustomization struct {
// The name of the subvolume, which defines the location (path) on the
// root volume (required).
// See https://btrfs.readthedocs.io/en/latest/Subvolumes.html
Name string `json:"name" toml:"name"`
// Mountpoint for the subvolume.
Mountpoint string `json:"mountpoint" toml:"mountpoint"`
}
// Custom JSON unmarshaller that first reads the value of the "type" field and
// then deserialises the whole object into a struct that only contains the
// fields valid for that partition type. This ensures that no fields are set
// for the substructure of a different type than the one defined in the "type"
// fields.
func (v *PartitionCustomization) UnmarshalJSON(data []byte) error {
errPrefix := "JSON unmarshal:"
var typeSniffer struct {
Type string `json:"type"`
MinSize any `json:"minsize"`
}
if err := json.Unmarshal(data, &typeSniffer); err != nil {
return fmt.Errorf("%s %w", errPrefix, err)
}
partType := "plain"
if typeSniffer.Type != "" {
partType = typeSniffer.Type
}
switch partType {
case "plain":
if err := decodePlain(v, data); err != nil {
return fmt.Errorf("%s %w", errPrefix, err)
}
case "btrfs":
if err := decodeBtrfs(v, data); err != nil {
return fmt.Errorf("%s %w", errPrefix, err)
}
case "lvm":
if err := decodeLVM(v, data); err != nil {
return fmt.Errorf("%s %w", errPrefix, err)
}
default:
return fmt.Errorf("%s unknown partition type: %s", errPrefix, partType)
}
v.Type = partType
if typeSniffer.MinSize == nil {
return fmt.Errorf("minsize is required")
}
minsize, err := decodeSize(typeSniffer.MinSize)
if err != nil {
return fmt.Errorf("%s error decoding minsize for partition: %w", errPrefix, err)
}
v.MinSize = minsize
return nil
}
// decodePlain decodes the data into a struct that only embeds the
// FilesystemCustomization with DisallowUnknownFields. This ensures that when
// the type is "plain", none of the fields for btrfs or lvm are used.
func decodePlain(v *PartitionCustomization, data []byte) error {
var plain struct {
// Type and minsize are handled by the caller. These are added here to
// satisfy "DisallowUnknownFields" when decoding.
Type string `json:"type"`
MinSize any `json:"minsize"`
FilesystemTypedCustomization
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err := decoder.Decode(&plain)
if err != nil {
return fmt.Errorf("error decoding partition with type \"plain\": %w", err)
}
v.FilesystemTypedCustomization = plain.FilesystemTypedCustomization
return nil
}
// decodeBtrfs decodes the data into a struct that only embeds the
// BtrfsVolumeCustomization with DisallowUnknownFields. This ensures that when
// the type is btrfs, none of the fields for plain or lvm are used.
func decodeBtrfs(v *PartitionCustomization, data []byte) error {
var btrfs struct {
// Type and minsize are handled by the caller. These are added here to
// satisfy "DisallowUnknownFields" when decoding.
Type string `json:"type"`
MinSize any `json:"minsize"`
BtrfsVolumeCustomization
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err := decoder.Decode(&btrfs)
if err != nil {
return fmt.Errorf("error decoding partition with type \"btrfs\": %w", err)
}
v.BtrfsVolumeCustomization = btrfs.BtrfsVolumeCustomization
return nil
}
// decodeLVM decodes the data into a struct that only embeds the
// VGCustomization with DisallowUnknownFields. This ensures that when the type
// is lvm, none of the fields for plain or btrfs are used.
func decodeLVM(v *PartitionCustomization, data []byte) error {
var vg struct {
// Type and minsize are handled by the caller. These are added here to
// satisfy "DisallowUnknownFields" when decoding.
Type string `json:"type"`
MinSize any `json:"minsize"`
VGCustomization
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&vg); err != nil {
return fmt.Errorf("error decoding partition with type \"lvm\": %w", err)
}
v.VGCustomization = vg.VGCustomization
return nil
}
// Custom TOML unmarshaller that first reads the value of the "type" field and
// then deserialises the whole object into a struct that only contains the
// fields valid for that partition type. This ensures that no fields are set
// for the substructure of a different type than the one defined in the "type"
// fields.
func (v *PartitionCustomization) UnmarshalTOML(data any) error {
errPrefix := "TOML unmarshal:"
d, ok := data.(map[string]any)
if !ok {
return fmt.Errorf("%s customizations.partition is not an object", errPrefix)
}
partType := "plain"
if typeField, ok := d["type"]; ok {
typeStr, ok := typeField.(string)
if !ok {
return fmt.Errorf("%s type must be a string, got \"%v\" of type %T", errPrefix, typeField, typeField)
}
partType = typeStr
}
// serialise the data to JSON and reuse the subobject decoders
dataJSON, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("%s error while decoding partition customization: %w", errPrefix, err)
}
switch partType {
case "plain":
if err := decodePlain(v, dataJSON); err != nil {
return fmt.Errorf("%s %w", errPrefix, err)
}
case "btrfs":
if err := decodeBtrfs(v, dataJSON); err != nil {
return fmt.Errorf("%s %w", errPrefix, err)
}
case "lvm":
if err := decodeLVM(v, dataJSON); err != nil {
return fmt.Errorf("%s %w", errPrefix, err)
}
default:
return fmt.Errorf("%s unknown partition type: %s", errPrefix, partType)
}
v.Type = partType
minsizeField, ok := d["minsize"]
if !ok {
return fmt.Errorf("minsize is required")
}
minsize, err := decodeSize(minsizeField)
if err != nil {
return fmt.Errorf("%s error decoding minsize for partition: %w", errPrefix, err)
}
v.MinSize = minsize
return nil
}
func unmarshalTOMLviaJSON(u json.Unmarshaler, data any) error {
// This is the most efficient way to reuse code when unmarshaling
// structs in toml, it leaks json errors which is a bit sad but
// because the toml unmarshaler gives us not "[]byte" but an
// already pre-processed "any" we cannot just unmarshal into our
// "fooMarshaling" struct and reuse the result so we resort to
// this workaround (but toml will go away long term anyway).
dataJSON, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("error unmarshaling TOML data %v: %w", data, err)
}
if err := u.UnmarshalJSON(dataJSON); err != nil {
return fmt.Errorf("error decoding TOML %v: %w", data, err)
}
return nil
}
// decodeSize takes an integer or string representing a data size (with a data
// suffix) and returns the uint64 representation.
func decodeSize(size any) (uint64, error) {
switch s := size.(type) {
case string:
return datasizes.Parse(s)
case int64:
if s < 0 {
return 0, fmt.Errorf("cannot be negative")
}
return uint64(s), nil
case float64:
if s < 0 {
return 0, fmt.Errorf("cannot be negative")
}
// TODO: emit warning of possible truncation?
return uint64(s), nil
case uint64:
return s, nil
default:
return 0, fmt.Errorf("failed to convert value \"%v\" to number", size)
}
}

View file

@ -1,117 +0,0 @@
package blueprint
import (
"encoding/json"
"fmt"
"github.com/osbuild/osbuild-composer/internal/common"
)
type FilesystemCustomization struct {
Mountpoint string `json:"mountpoint,omitempty" toml:"mountpoint,omitempty"`
MinSize uint64 `json:"minsize,omitempty" toml:"minsize,omitempty"`
// Note: The TOML `size` tag has been deprecated in favor of `minsize`.
// we check for it in the TOML unmarshaler and use it as `minsize`.
// However due to the TOML marshaler implementation, we can omit adding
// a field for this tag and get the benifit of not having to export it.
}
func (fsc *FilesystemCustomization) UnmarshalTOML(data interface{}) error {
d, _ := data.(map[string]interface{})
switch d["mountpoint"].(type) {
case string:
fsc.Mountpoint = d["mountpoint"].(string)
default:
return fmt.Errorf("TOML unmarshal: mountpoint must be string, got %v of type %T", d["mountpoint"], d["mountpoint"])
}
var size uint64
var minsize uint64
// `size` is an alias for `minsize. We check for the `size` keyword
// for backwards compatibility. We don't export a `Size` field as
// we would like to discourage its use.
switch d["size"].(type) {
case int64:
size = uint64(d["size"].(int64))
case string:
s, err := common.DataSizeToUint64(d["size"].(string))
if err != nil {
return fmt.Errorf("TOML unmarshal: size is not valid filesystem size (%w)", err)
}
size = s
case nil:
size = 0
default:
return fmt.Errorf("TOML unmarshal: size must be integer or string, got %v of type %T", d["size"], d["size"])
}
switch d["minsize"].(type) {
case int64:
minsize = uint64(d["minsize"].(int64))
case string:
s, err := common.DataSizeToUint64(d["minsize"].(string))
if err != nil {
return fmt.Errorf("TOML unmarshal: minsize is not valid filesystem size (%w)", err)
}
minsize = s
case nil:
minsize = 0
default:
return fmt.Errorf("TOML unmarshal: minsize must be integer or string, got %v of type %T", d["minsize"], d["minsize"])
}
if size == 0 && minsize == 0 {
return fmt.Errorf("TOML unmarshal: minsize must be greater than 0, got %v", minsize)
}
if size > 0 && minsize == 0 {
fsc.MinSize = size
return nil
}
if size == 0 && minsize > 0 {
fsc.MinSize = minsize
return nil
}
if size > 0 && minsize > 0 {
return fmt.Errorf("TOML unmarshal: size and minsize cannot both be set (size is an alias for minsize)")
}
return nil
}
func (fsc *FilesystemCustomization) UnmarshalJSON(data []byte) error {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
d, _ := v.(map[string]interface{})
switch d["mountpoint"].(type) {
case string:
fsc.Mountpoint = d["mountpoint"].(string)
default:
return fmt.Errorf("JSON unmarshal: mountpoint must be string, got %v of type %T", d["mountpoint"], d["mountpoint"])
}
// The JSON specification only mentions float64 and Go defaults to it: https://go.dev/blog/json
switch d["minsize"].(type) {
case float64:
// Note that it uses different key than the TOML version
fsc.MinSize = uint64(d["minsize"].(float64))
case string:
size, err := common.DataSizeToUint64(d["minsize"].(string))
if err != nil {
return fmt.Errorf("JSON unmarshal: size is not valid filesystem size (%w)", err)
}
fsc.MinSize = size
default:
return fmt.Errorf("JSON unmarshal: minsize must be float64 number or string, got %v of type %T", d["minsize"], d["minsize"])
}
return nil
}

View file

@ -1,159 +0,0 @@
package blueprint
import (
"testing"
"github.com/BurntSushi/toml"
"github.com/stretchr/testify/assert"
)
func TestGetFilesystems(t *testing.T) {
expectedFilesystems := []FilesystemCustomization{
{
MinSize: 1024,
Mountpoint: "/",
},
}
TestCustomizations := Customizations{
Filesystem: expectedFilesystems,
}
retFilesystems := TestCustomizations.GetFilesystems()
assert.ElementsMatch(t, expectedFilesystems, retFilesystems)
}
func TestGetFilesystemsMinSize(t *testing.T) {
expectedFilesystems := []FilesystemCustomization{
{
MinSize: 1024,
Mountpoint: "/",
},
{
MinSize: 4096,
Mountpoint: "/var",
},
}
TestCustomizations := Customizations{
Filesystem: expectedFilesystems,
}
retFilesystemsSize := TestCustomizations.GetFilesystemsMinSize()
assert.EqualValues(t, uint64(5120), retFilesystemsSize)
}
func TestGetFilesystemsMinSizeNonSectorSize(t *testing.T) {
expectedFilesystems := []FilesystemCustomization{
{
MinSize: 1025,
Mountpoint: "/",
},
{
MinSize: 4097,
Mountpoint: "/var",
},
}
TestCustomizations := Customizations{
Filesystem: expectedFilesystems,
}
retFilesystemsSize := TestCustomizations.GetFilesystemsMinSize()
assert.EqualValues(t, uint64(5632), retFilesystemsSize)
}
func TestGetFilesystemsMinSizeTOML(t *testing.T) {
tests := []struct {
Name string
TOML string
Want []FilesystemCustomization
Error bool
}{
{
Name: "size set, no minsize",
TOML: `
[[customizations.filesystem]]
mountpoint = "/var"
size = 1024
`,
Want: []FilesystemCustomization{{MinSize: 1024, Mountpoint: "/var"}},
Error: false,
},
{
Name: "size set (string), no minsize",
TOML: `
[[customizations.filesystem]]
mountpoint = "/var"
size = "1KiB"
`,
Want: []FilesystemCustomization{{MinSize: 1024, Mountpoint: "/var"}},
Error: false,
},
{
Name: "minsize set, no size",
TOML: `
[[customizations.filesystem]]
mountpoint = "/var"
minsize = 1024
`,
Want: []FilesystemCustomization{{MinSize: 1024, Mountpoint: "/var"}},
Error: false,
},
{
Name: "minsize set (string), no size",
TOML: `
[[customizations.filesystem]]
mountpoint = "/var"
minsize = "1KiB"
`,
Want: []FilesystemCustomization{{MinSize: 1024, Mountpoint: "/var"}},
Error: false,
},
{
Name: "size and minsize set",
TOML: `
[[customizations.filesystem]]
mountpoint = "/var"
size = 1024
minsize = 1024
`,
Want: []FilesystemCustomization{},
Error: true,
},
{
Name: "size and minsize not set",
TOML: `
[[customizations.filesystem]]
mountpoint = "/var"
`,
Want: []FilesystemCustomization{},
Error: true,
},
}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
var blueprint Blueprint
err := toml.Unmarshal([]byte(tt.TOML), &blueprint)
if tt.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, blueprint.Customizations)
assert.Equal(t, tt.Want, blueprint.Customizations.Filesystem)
}
})
}
}

View file

@ -1,336 +0,0 @@
package blueprint
import (
"encoding/json"
"fmt"
"os"
"regexp"
"strconv"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/fsnode"
)
// validateModeString checks that the given string is a valid mode octal number
func validateModeString(mode string) error {
// Check that the mode string matches the octal format regular expression.
// The leading is optional.
if regexp.MustCompile(`^[0]{0,1}[0-7]{3}$`).MatchString(mode) {
return nil
}
return fmt.Errorf("invalid mode %s: must be an octal number", mode)
}
// DirectoryCustomization represents a directory to be created in the image
type DirectoryCustomization struct {
// Absolute path to the directory
Path string `json:"path" toml:"path"`
// Owner of the directory specified as a string (user name), int64 (UID) or nil
User interface{} `json:"user,omitempty" toml:"user,omitempty"`
// Owner of the directory specified as a string (group name), int64 (UID) or nil
Group interface{} `json:"group,omitempty" toml:"group,omitempty"`
// Permissions of the directory specified as an octal number
Mode string `json:"mode,omitempty" toml:"mode,omitempty"`
// EnsureParents ensures that all parent directories of the directory exist
EnsureParents bool `json:"ensure_parents,omitempty" toml:"ensure_parents,omitempty"`
}
// Custom TOML unmarshalling for DirectoryCustomization with validation
func (d *DirectoryCustomization) UnmarshalTOML(data interface{}) error {
var dir DirectoryCustomization
dataMap, _ := data.(map[string]interface{})
switch path := dataMap["path"].(type) {
case string:
dir.Path = path
default:
return fmt.Errorf("UnmarshalTOML: path must be a string")
}
switch user := dataMap["user"].(type) {
case string:
dir.User = user
case int64:
dir.User = user
case nil:
break
default:
return fmt.Errorf("UnmarshalTOML: user must be a string or an integer, got %T", user)
}
switch group := dataMap["group"].(type) {
case string:
dir.Group = group
case int64:
dir.Group = group
case nil:
break
default:
return fmt.Errorf("UnmarshalTOML: group must be a string or an integer")
}
switch mode := dataMap["mode"].(type) {
case string:
dir.Mode = mode
case nil:
break
default:
return fmt.Errorf("UnmarshalTOML: mode must be a string")
}
switch ensureParents := dataMap["ensure_parents"].(type) {
case bool:
dir.EnsureParents = ensureParents
case nil:
break
default:
return fmt.Errorf("UnmarshalTOML: ensure_parents must be a bool")
}
// try converting to fsnode.Directory to validate all values
_, err := dir.ToFsNodeDirectory()
if err != nil {
return err
}
*d = dir
return nil
}
// Custom JSON unmarshalling for DirectoryCustomization with validation
func (d *DirectoryCustomization) UnmarshalJSON(data []byte) error {
type directoryCustomization DirectoryCustomization
var dirPrivate directoryCustomization
if err := json.Unmarshal(data, &dirPrivate); err != nil {
return err
}
dir := DirectoryCustomization(dirPrivate)
if uid, ok := dir.User.(float64); ok {
// check if uid can be converted to int64
if uid != float64(int64(uid)) {
return fmt.Errorf("invalid user %f: must be an integer", uid)
}
dir.User = int64(uid)
}
if gid, ok := dir.Group.(float64); ok {
// check if gid can be converted to int64
if gid != float64(int64(gid)) {
return fmt.Errorf("invalid group %f: must be an integer", gid)
}
dir.Group = int64(gid)
}
// try converting to fsnode.Directory to validate all values
_, err := dir.ToFsNodeDirectory()
if err != nil {
return err
}
*d = dir
return nil
}
// ToFsNodeDirectory converts the DirectoryCustomization to an fsnode.Directory
func (d DirectoryCustomization) ToFsNodeDirectory() (*fsnode.Directory, error) {
var mode *os.FileMode
if d.Mode != "" {
err := validateModeString(d.Mode)
if err != nil {
return nil, err
}
modeNum, err := strconv.ParseUint(d.Mode, 8, 32)
if err != nil {
return nil, fmt.Errorf("invalid mode %s: %v", d.Mode, err)
}
// modeNum is parsed as an unsigned 32 bit int
/* #nosec G115 */
mode = common.ToPtr(os.FileMode(modeNum))
}
return fsnode.NewDirectory(d.Path, mode, d.User, d.Group, d.EnsureParents)
}
// DirectoryCustomizationsToFsNodeDirectories converts a slice of DirectoryCustomizations
// to a slice of fsnode.Directories
func DirectoryCustomizationsToFsNodeDirectories(dirs []DirectoryCustomization) ([]*fsnode.Directory, error) {
if len(dirs) == 0 {
return nil, nil
}
var fsDirs []*fsnode.Directory
var errors []error
for _, dir := range dirs {
fsDir, err := dir.ToFsNodeDirectory()
if err != nil {
errors = append(errors, err)
}
fsDirs = append(fsDirs, fsDir)
}
if len(errors) > 0 {
return nil, fmt.Errorf("invalid directory customizations: %v", errors)
}
return fsDirs, nil
}
// FileCustomization represents a file to be created in the image
type FileCustomization struct {
// Absolute path to the file
Path string `json:"path" toml:"path"`
// Owner of the directory specified as a string (user name), int64 (UID) or nil
User interface{} `json:"user,omitempty" toml:"user,omitempty"`
// Owner of the directory specified as a string (group name), int64 (UID) or nil
Group interface{} `json:"group,omitempty" toml:"group,omitempty"`
// Permissions of the file specified as an octal number
Mode string `json:"mode,omitempty" toml:"mode,omitempty"`
// Data is the file content in plain text
Data string `json:"data,omitempty" toml:"data,omitempty"`
}
// Custom TOML unmarshalling for FileCustomization with validation
func (f *FileCustomization) UnmarshalTOML(data interface{}) error {
var file FileCustomization
dataMap, _ := data.(map[string]interface{})
switch path := dataMap["path"].(type) {
case string:
file.Path = path
default:
return fmt.Errorf("UnmarshalTOML: path must be a string")
}
switch user := dataMap["user"].(type) {
case string:
file.User = user
case int64:
file.User = user
case nil:
break
default:
return fmt.Errorf("UnmarshalTOML: user must be a string or an integer")
}
switch group := dataMap["group"].(type) {
case string:
file.Group = group
case int64:
file.Group = group
case nil:
break
default:
return fmt.Errorf("UnmarshalTOML: group must be a string or an integer")
}
switch mode := dataMap["mode"].(type) {
case string:
file.Mode = mode
case nil:
break
default:
return fmt.Errorf("UnmarshalTOML: mode must be a string")
}
switch data := dataMap["data"].(type) {
case string:
file.Data = data
case nil:
break
default:
return fmt.Errorf("UnmarshalTOML: data must be a string")
}
// try converting to fsnode.File to validate all values
_, err := file.ToFsNodeFile()
if err != nil {
return err
}
*f = file
return nil
}
// Custom JSON unmarshalling for FileCustomization with validation
func (f *FileCustomization) UnmarshalJSON(data []byte) error {
type fileCustomization FileCustomization
var filePrivate fileCustomization
if err := json.Unmarshal(data, &filePrivate); err != nil {
return err
}
file := FileCustomization(filePrivate)
if uid, ok := file.User.(float64); ok {
// check if uid can be converted to int64
if uid != float64(int64(uid)) {
return fmt.Errorf("invalid user %f: must be an integer", uid)
}
file.User = int64(uid)
}
if gid, ok := file.Group.(float64); ok {
// check if gid can be converted to int64
if gid != float64(int64(gid)) {
return fmt.Errorf("invalid group %f: must be an integer", gid)
}
file.Group = int64(gid)
}
// try converting to fsnode.File to validate all values
_, err := file.ToFsNodeFile()
if err != nil {
return err
}
*f = file
return nil
}
// ToFsNodeFile converts the FileCustomization to an fsnode.File
func (f FileCustomization) ToFsNodeFile() (*fsnode.File, error) {
var data []byte
if f.Data != "" {
data = []byte(f.Data)
}
var mode *os.FileMode
if f.Mode != "" {
err := validateModeString(f.Mode)
if err != nil {
return nil, err
}
modeNum, err := strconv.ParseUint(f.Mode, 8, 32)
if err != nil {
return nil, fmt.Errorf("invalid mode %s: %v", f.Mode, err)
}
// modeNum is parsed as an unsigned 32 bit int
/* #nosec G115 */
mode = common.ToPtr(os.FileMode(modeNum))
}
return fsnode.NewFile(f.Path, mode, f.User, f.Group, data)
}
// FileCustomizationsToFsNodeFiles converts a slice of FileCustomization to a slice of *fsnode.File
func FileCustomizationsToFsNodeFiles(files []FileCustomization) ([]*fsnode.File, error) {
if len(files) == 0 {
return nil, nil
}
var fsFiles []*fsnode.File
var errors []error
for _, file := range files {
fsFile, err := file.ToFsNodeFile()
if err != nil {
errors = append(errors, err)
}
fsFiles = append(fsFiles, fsFile)
}
if len(errors) > 0 {
return nil, fmt.Errorf("invalid file customizations: %v", errors)
}
return fsFiles, nil
}

View file

@ -1,977 +0,0 @@
package blueprint
import (
"encoding/json"
"os"
"testing"
"github.com/BurntSushi/toml"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/fsnode"
"github.com/stretchr/testify/assert"
)
func TestDirectoryCustomizationToFsNodeDirectory(t *testing.T) {
ensureDirCreation := func(dir *fsnode.Directory, err error) *fsnode.Directory {
t.Helper()
assert.NoError(t, err)
assert.NotNil(t, dir)
return dir
}
testCases := []struct {
Name string
Dir DirectoryCustomization
WantDir *fsnode.Directory
Error bool
}{
{
Name: "empty",
Dir: DirectoryCustomization{},
Error: true,
},
{
Name: "path-only",
Dir: DirectoryCustomization{
Path: "/etc/dir",
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", nil, nil, nil, false)),
},
{
Name: "path-invalid",
Dir: DirectoryCustomization{
Path: "etc/dir",
},
Error: true,
},
{
Name: "path-and-mode",
Dir: DirectoryCustomization{
Path: "/etc/dir",
Mode: "0700",
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", common.ToPtr(os.FileMode(0700)), nil, nil, false)),
},
{
Name: "path-and-mode-no-leading-zero",
Dir: DirectoryCustomization{
Path: "/etc/dir",
Mode: "700",
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", common.ToPtr(os.FileMode(0700)), nil, nil, false)),
},
{
Name: "path-and-mode-invalid",
Dir: DirectoryCustomization{
Path: "/etc/dir",
Mode: "12345",
},
Error: true,
},
{
Name: "path-user-group-string",
Dir: DirectoryCustomization{
Path: "/etc/dir",
User: "root",
Group: "root",
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", nil, "root", "root", false)),
},
{
Name: "path-user-group-int64",
Dir: DirectoryCustomization{
Path: "/etc/dir",
User: int64(0),
Group: int64(0),
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", nil, int64(0), int64(0), false)),
},
{
Name: "path-and-user-invalid-string",
Dir: DirectoryCustomization{
Path: "/etc/dir",
User: "r@@t",
},
Error: true,
},
{
Name: "path-and-user-invalid-int64",
Dir: DirectoryCustomization{
Path: "/etc/dir",
User: -1,
},
Error: true,
},
{
Name: "path-and-group-invalid-string",
Dir: DirectoryCustomization{
Path: "/etc/dir",
Group: "r@@t",
},
Error: true,
},
{
Name: "path-and-group-invalid-int64",
Dir: DirectoryCustomization{
Path: "/etc/dir",
Group: -1,
},
Error: true,
},
{
Name: "path-and-ensure-parent-dirs",
Dir: DirectoryCustomization{
Path: "/etc/dir",
EnsureParents: true,
},
WantDir: ensureDirCreation(fsnode.NewDirectory("/etc/dir", nil, nil, nil, true)),
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
dir, err := tc.Dir.ToFsNodeDirectory()
if tc.Error {
assert.Error(t, err)
assert.Nil(t, dir)
} else {
assert.NoError(t, err)
assert.EqualValues(t, tc.WantDir, dir)
}
})
}
}
func TestDirectoryCustomizationsToFsNodeDirectories(t *testing.T) {
ensureDirCreation := func(dir *fsnode.Directory, err error) *fsnode.Directory {
t.Helper()
assert.NoError(t, err)
assert.NotNil(t, dir)
return dir
}
testCases := []struct {
Name string
Dirs []DirectoryCustomization
WantDirs []*fsnode.Directory
Error bool
}{
{
Name: "empty",
Dirs: []DirectoryCustomization{},
WantDirs: nil,
},
{
Name: "single-directory",
Dirs: []DirectoryCustomization{
{
Path: "/etc/dir",
User: "root",
Group: "root",
Mode: "0700",
EnsureParents: true,
},
},
WantDirs: []*fsnode.Directory{
ensureDirCreation(fsnode.NewDirectory(
"/etc/dir",
common.ToPtr(os.FileMode(0700)),
"root",
"root",
true,
)),
},
},
{
Name: "multiple-directories",
Dirs: []DirectoryCustomization{
{
Path: "/etc/dir",
User: "root",
Group: "root",
},
{
Path: "/etc/dir2",
User: int64(0),
Group: int64(0),
},
},
WantDirs: []*fsnode.Directory{
ensureDirCreation(fsnode.NewDirectory("/etc/dir", nil, "root", "root", false)),
ensureDirCreation(fsnode.NewDirectory("/etc/dir2", nil, int64(0), int64(0), false)),
},
},
{
Name: "multiple-directories-with-errors",
Dirs: []DirectoryCustomization{
{
Path: "/etc/../dir",
},
{
Path: "/etc/dir2",
User: "r@@t",
},
},
Error: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
dirs, err := DirectoryCustomizationsToFsNodeDirectories(tc.Dirs)
if tc.Error {
assert.Error(t, err)
assert.Nil(t, dirs)
} else {
assert.NoError(t, err)
assert.EqualValues(t, tc.WantDirs, dirs)
}
})
}
}
func TestDirectoryCustomizationUnmarshalTOML(t *testing.T) {
testCases := []struct {
Name string
TOML string
Want []DirectoryCustomization
Error bool
}{
{
Name: "directory-with-path",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.directories]]
path = "/etc/dir"
`,
Want: []DirectoryCustomization{
{
Path: "/etc/dir",
},
},
},
{
Name: "multiple-directories",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.directories]]
path = "/etc/dir1"
mode = "0700"
user = "root"
group = "root"
ensure_parents = true
[[customizations.directories]]
path = "/etc/dir2"
mode = "0755"
user = 0
group = 0
ensure_parents = true
[[customizations.directories]]
path = "/etc/dir3"
`,
Want: []DirectoryCustomization{
{
Path: "/etc/dir1",
Mode: "0700",
User: "root",
Group: "root",
EnsureParents: true,
},
{
Path: "/etc/dir2",
Mode: "0755",
User: int64(0),
Group: int64(0),
EnsureParents: true,
},
{
Path: "/etc/dir3",
},
},
},
{
Name: "invalid-directories",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.directories]]
path = "/etc/../dir1"
[[customizations.directories]]
path = "/etc/dir2"
mode = "12345"
[[customizations.directories]]
path = "/etc/dir3"
user = "r@@t"
[[customizations.directories]]
path = "/etc/dir4"
group = "r@@t"
[[customizations.directories]]
path = "/etc/dir5"
user = -1
[[customizations.directories]]
path = "/etc/dir6"
group = -1
[[customizations.directories]]
`,
Error: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
var blueprint Blueprint
err := toml.Unmarshal([]byte(tc.TOML), &blueprint)
if tc.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, blueprint.Customizations)
assert.Len(t, blueprint.Customizations.Directories, len(tc.Want))
assert.EqualValues(t, tc.Want, blueprint.Customizations.GetDirectories())
}
})
}
}
func TestDirectoryCustomizationUnmarshalJSON(t *testing.T) {
testCases := []struct {
Name string
JSON string
Want []DirectoryCustomization
Error bool
}{
{
Name: "directory-with-path",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"directories": [
{
"path": "/etc/dir"
}
]
}
}`,
Want: []DirectoryCustomization{
{
Path: "/etc/dir",
},
},
},
{
Name: "multiple-directories",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"directories": [
{
"path": "/etc/dir1",
"mode": "0700",
"user": "root",
"group": "root",
"ensure_parents": true
},
{
"path": "/etc/dir2",
"mode": "0755",
"user": 0,
"group": 0,
"ensure_parents": true
},
{
"path": "/etc/dir3"
}
]
}
}`,
Want: []DirectoryCustomization{
{
Path: "/etc/dir1",
Mode: "0700",
User: "root",
Group: "root",
EnsureParents: true,
},
{
Path: "/etc/dir2",
Mode: "0755",
User: int64(0),
Group: int64(0),
EnsureParents: true,
},
{
Path: "/etc/dir3",
},
},
},
{
Name: "invalid-directories",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"directories": [
{
"path": "/etc/../dir1"
},
{
"path": "/etc/dir2",
"mode": "12345"
},
{
"path": "/etc/dir3",
"user": "r@@t"
},
{
"path": "/etc/dir4",
"group": "r@@t"
},
{
"path": "/etc/dir5",
"user": -1
},
{
"path": "/etc/dir6",
"group": -1
}
{}
]
}
}`,
Error: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
var blueprint Blueprint
err := json.Unmarshal([]byte(tc.JSON), &blueprint)
if tc.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, blueprint.Customizations)
assert.Len(t, blueprint.Customizations.Directories, len(tc.Want))
assert.EqualValues(t, tc.Want, blueprint.Customizations.GetDirectories())
}
})
}
}
func TestFileCustomizationToFsNodeFile(t *testing.T) {
ensureFileCreation := func(file *fsnode.File, err error) *fsnode.File {
t.Helper()
assert.NoError(t, err)
assert.NotNil(t, file)
return file
}
testCases := []struct {
Name string
File FileCustomization
Want *fsnode.File
Error bool
}{
{
Name: "empty",
File: FileCustomization{},
Error: true,
},
{
Name: "path-only",
File: FileCustomization{
Path: "/etc/file",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, nil, nil, nil)),
},
{
Name: "path-invalid",
File: FileCustomization{
Path: "../etc/file",
},
Error: true,
},
{
Name: "path-and-mode",
File: FileCustomization{
Path: "/etc/file",
Mode: "0700",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", common.ToPtr(os.FileMode(0700)), nil, nil, nil)),
},
{
Name: "path-and-mode-no-leading-zero",
File: FileCustomization{
Path: "/etc/file",
Mode: "700",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", common.ToPtr(os.FileMode(0700)), nil, nil, nil)),
},
{
Name: "path-and-mode-invalid",
File: FileCustomization{
Path: "/etc/file",
Mode: "12345",
},
Error: true,
},
{
Name: "path-user-group-string",
File: FileCustomization{
Path: "/etc/file",
User: "root",
Group: "root",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, "root", "root", nil)),
},
{
Name: "path-user-group-int64",
File: FileCustomization{
Path: "/etc/file",
User: int64(0),
Group: int64(0),
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, int64(0), int64(0), nil)),
},
{
Name: "path-and-user-invalid-string",
File: FileCustomization{
Path: "/etc/file",
User: "r@@t",
},
Error: true,
},
{
Name: "path-and-user-invalid-int64",
File: FileCustomization{
Path: "/etc/file",
User: int64(-1),
},
Error: true,
},
{
Name: "path-and-group-string",
File: FileCustomization{
Path: "/etc/file",
Group: "root",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, nil, "root", nil)),
},
{
Name: "path-and-group-int64",
File: FileCustomization{
Path: "/etc/file",
Group: int64(0),
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, nil, int64(0), nil)),
},
{
Name: "path-and-group-invalid-string",
File: FileCustomization{
Path: "/etc/file",
Group: "r@@t",
},
Error: true,
},
{
Name: "path-and-group-invalid-int64",
File: FileCustomization{
Path: "/etc/file",
Group: int64(-1),
},
Error: true,
},
{
Name: "path-and-data",
File: FileCustomization{
Path: "/etc/file",
Data: "hello world",
},
Want: ensureFileCreation(fsnode.NewFile("/etc/file", nil, nil, nil, []byte("hello world"))),
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
file, err := tc.File.ToFsNodeFile()
if tc.Error {
assert.Error(t, err)
assert.Nil(t, file)
} else {
assert.NoError(t, err)
assert.EqualValues(t, tc.Want, file)
}
})
}
}
func TestFileCustomizationsToFsNodeFiles(t *testing.T) {
ensureFileCreation := func(file *fsnode.File, err error) *fsnode.File {
t.Helper()
assert.NoError(t, err)
assert.NotNil(t, file)
return file
}
testCases := []struct {
Name string
Files []FileCustomization
Want []*fsnode.File
Error bool
}{
{
Name: "empty",
Files: []FileCustomization{},
Want: nil,
},
{
Name: "single-file",
Files: []FileCustomization{
{
Path: "/etc/file",
User: "root",
Group: "root",
Mode: "0700",
Data: "hello world",
},
},
Want: []*fsnode.File{
ensureFileCreation(fsnode.NewFile(
"/etc/file",
common.ToPtr(os.FileMode(0700)),
"root",
"root",
[]byte("hello world"),
)),
},
},
{
Name: "multiple-files",
Files: []FileCustomization{
{
Path: "/etc/file",
Data: "hello world",
User: "root",
Group: "root",
},
{
Path: "/etc/file2",
Data: "hello world",
User: int64(0),
Group: int64(0),
},
},
Want: []*fsnode.File{
ensureFileCreation(fsnode.NewFile("/etc/file", nil, "root", "root", []byte("hello world"))),
ensureFileCreation(fsnode.NewFile("/etc/file2", nil, int64(0), int64(0), []byte("hello world"))),
},
},
{
Name: "multiple-files-with-errors",
Files: []FileCustomization{
{
Path: "/etc/../file",
Data: "hello world",
},
{
Path: "/etc/file2",
Data: "hello world",
User: "r@@t",
},
},
Error: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
files, err := FileCustomizationsToFsNodeFiles(tc.Files)
if tc.Error {
assert.Error(t, err)
assert.Nil(t, files)
} else {
assert.NoError(t, err)
assert.EqualValues(t, tc.Want, files)
}
})
}
}
func TestFileCustomizationUnmarshalTOML(t *testing.T) {
testCases := []struct {
Name string
TOML string
Want []FileCustomization
Error bool
}{
{
Name: "file-with-path",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.files]]
path = "/etc/file"
`,
Want: []FileCustomization{
{
Path: "/etc/file",
},
},
},
{
Name: "multiple-files",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.files]]
path = "/etc/file1"
mode = "0600"
user = "root"
group = "root"
data = "hello world"
[[customizations.files]]
path = "/etc/file2"
mode = "0644"
data = "hello world 2"
[[customizations.files]]
path = "/etc/file3"
user = 0
group = 0
data = "hello world 3"
`,
Want: []FileCustomization{
{
Path: "/etc/file1",
Mode: "0600",
User: "root",
Group: "root",
Data: "hello world",
},
{
Path: "/etc/file2",
Mode: "0644",
Data: "hello world 2",
},
{
Path: "/etc/file3",
User: int64(0),
Group: int64(0),
Data: "hello world 3",
},
},
},
{
Name: "invalid-files",
TOML: `
name = "test"
description = "Test"
version = "0.0.0"
[[customizations.files]]
path = "/etc/../file1"
[[customizations.files]]
path = "/etc/file2"
mode = "12345"
[[customizations.files]]
path = "/etc/file3"
user = "r@@t"
[[customizations.files]]
path = "/etc/file4"
group = "r@@t"
[[customizations.files]]
path = "/etc/file5"
user = -1
[[customizations.files]]
path = "/etc/file6"
group = -1
`,
Error: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
var blueprint Blueprint
err := toml.Unmarshal([]byte(tc.TOML), &blueprint)
if tc.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, blueprint.Customizations)
assert.Len(t, blueprint.Customizations.Files, len(tc.Want))
assert.EqualValues(t, tc.Want, blueprint.Customizations.Files)
}
})
}
}
func TestFileCustomizationUnmarshalJSON(t *testing.T) {
testCases := []struct {
Name string
JSON string
Want []FileCustomization
Error bool
}{
{
Name: "file-with-path",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"files": [
{
"path": "/etc/file"
}
]
}
}`,
Want: []FileCustomization{
{
Path: "/etc/file",
},
},
},
{
Name: "multiple-files",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"files": [
{
"path": "/etc/file1",
"mode": "0600",
"user": "root",
"group": "root",
"data": "hello world"
},
{
"path": "/etc/file2",
"mode": "0644",
"data": "hello world 2"
},
{
"path": "/etc/file3",
"user": 0,
"group": 0,
"data": "hello world 3"
}
]
}
}`,
Want: []FileCustomization{
{
Path: "/etc/file1",
Mode: "0600",
User: "root",
Group: "root",
Data: "hello world",
},
{
Path: "/etc/file2",
Mode: "0644",
Data: "hello world 2",
},
{
Path: "/etc/file3",
User: int64(0),
Group: int64(0),
Data: "hello world 3",
},
},
},
{
Name: "invalid-files",
JSON: `
{
"name": "test",
"description": "Test",
"version": "0.0.0",
"customizations": {
"files": [
{
"path": "/etc/../file1"
},
{
"path": "/etc/file2",
"mode": "12345"
},
{
"path": "/etc/file3",
"user": "r@@t"
},
{
"path": "/etc/file4",
"group": "r@@t"
},
{
"path": "/etc/file5",
"user": -1
},
{
"path": "/etc/file6",
"group": -1
}
]
}
}`,
Error: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
var blueprint Blueprint
err := json.Unmarshal([]byte(tc.JSON), &blueprint)
if tc.Error {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, blueprint.Customizations)
assert.Len(t, blueprint.Customizations.Files, len(tc.Want))
assert.EqualValues(t, tc.Want, blueprint.Customizations.Files)
}
})
}
}

View file

@ -1,17 +0,0 @@
package blueprint
type InstallerCustomization struct {
Unattended bool `json:"unattended,omitempty" toml:"unattended,omitempty"`
SudoNopasswd []string `json:"sudo-nopasswd,omitempty" toml:"sudo-nopasswd,omitempty"`
Kickstart *Kickstart `json:"kickstart,omitempty" toml:"kickstart,omitempty"`
Modules *AnacondaModules `json:"modules,omitempty" toml:"modules,omitempty"`
}
type Kickstart struct {
Contents string `json:"contents" toml:"contents"`
}
type AnacondaModules struct {
Enable []string `json:"enable,omitempty" toml:"enable,omitempty"`
Disable []string `json:"disable,omitempty" toml:"disable,omitempty"`
}

View file

@ -1,77 +0,0 @@
package blueprint
import (
"fmt"
"net/url"
"regexp"
"strings"
)
type RepositoryCustomization struct {
Id string `json:"id" toml:"id"`
BaseURLs []string `json:"baseurls,omitempty" toml:"baseurls,omitempty"`
GPGKeys []string `json:"gpgkeys,omitempty" toml:"gpgkeys,omitempty"`
Metalink string `json:"metalink,omitempty" toml:"metalink,omitempty"`
Mirrorlist string `json:"mirrorlist,omitempty" toml:"mirrorlist,omitempty"`
Name string `json:"name,omitempty" toml:"name,omitempty"`
Priority *int `json:"priority,omitempty" toml:"priority,omitempty"`
Enabled *bool `json:"enabled,omitempty" toml:"enabled,omitempty"`
GPGCheck *bool `json:"gpgcheck,omitempty" toml:"gpgcheck,omitempty"`
RepoGPGCheck *bool `json:"repo_gpgcheck,omitempty" toml:"repo_gpgcheck,omitempty"`
SSLVerify *bool `json:"sslverify,omitempty" toml:"sslverify,omitempty"`
ModuleHotfixes *bool `json:"module_hotfixes,omitempty" toml:"module_hotfixes,omitempty"`
Filename string `json:"filename,omitempty" toml:"filename,omitempty"`
// When set the repository will be used during the depsolve of
// payload repositories to install packages from it.
InstallFrom bool `json:"install_from" toml:"install_from"`
}
const repoFilenameRegex = "^[\\w.-]{1,250}\\.repo$"
func validateCustomRepository(repo *RepositoryCustomization) error {
if repo.Id == "" {
return fmt.Errorf("Repository ID is required")
}
filenameRegex := regexp.MustCompile(repoFilenameRegex)
if !filenameRegex.MatchString(repo.getFilename()) {
return fmt.Errorf("Repository filename %q is invalid", repo.getFilename())
}
if len(repo.BaseURLs) == 0 && repo.Mirrorlist == "" && repo.Metalink == "" {
return fmt.Errorf("Repository base URL, mirrorlist or metalink is required")
}
if repo.GPGCheck != nil && *repo.GPGCheck && len(repo.GPGKeys) == 0 {
return fmt.Errorf("Repository gpg check is set to true but no gpg keys are provided")
}
for _, key := range repo.GPGKeys {
// check for a valid GPG key prefix & contains GPG suffix
keyIsGPGKey := strings.HasPrefix(key, "-----BEGIN PGP PUBLIC KEY BLOCK-----") && strings.Contains(key, "-----END PGP PUBLIC KEY BLOCK-----")
// check for a valid URL
keyIsURL := false
_, err := url.ParseRequestURI(key)
if err == nil {
keyIsURL = true
}
if !keyIsGPGKey && !keyIsURL {
return fmt.Errorf("Repository gpg key is not a valid URL or a valid gpg key")
}
}
return nil
}
func (rc *RepositoryCustomization) getFilename() string {
if rc.Filename == "" {
return fmt.Sprintf("%s.repo", rc.Id)
}
if !strings.HasSuffix(rc.Filename, ".repo") {
return fmt.Sprintf("%s.repo", rc.Filename)
}
return rc.Filename
}

View file

@ -1,157 +0,0 @@
package blueprint
import (
"fmt"
"testing"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/stretchr/testify/assert"
)
func TestGetCustomRepositories(t *testing.T) {
testCases := []struct {
name string
expectedCustomizations Customizations
wantErr error
}{
{
name: "Test no errors",
expectedCustomizations: Customizations{
Repositories: []RepositoryCustomization{
{
Id: "example-1",
BaseURLs: []string{"http://example-1.com"},
},
{
Id: "example-2",
BaseURLs: []string{"http://example-2.com"},
},
},
},
wantErr: nil,
},
{
name: "Test empty id error",
expectedCustomizations: Customizations{
Repositories: []RepositoryCustomization{
{},
},
},
wantErr: fmt.Errorf("Repository ID is required"),
},
{
name: "Test empty baseurl, mirrorlist or metalink error",
expectedCustomizations: Customizations{
Repositories: []RepositoryCustomization{
{
Id: "example-1",
},
},
},
wantErr: fmt.Errorf("Repository base URL, mirrorlist or metalink is required"),
},
{
name: "Test missing GPG keys error",
expectedCustomizations: Customizations{
Repositories: []RepositoryCustomization{
{
Id: "example-1",
BaseURLs: []string{"http://example-1.com"},
GPGCheck: common.ToPtr(true),
},
},
},
wantErr: fmt.Errorf("Repository gpg check is set to true but no gpg keys are provided"),
},
{
name: "Test invalid GPG keys error",
expectedCustomizations: Customizations{
Repositories: []RepositoryCustomization{
{
Id: "example-1",
BaseURLs: []string{"http://example-1.com"},
GPGKeys: []string{"invalid"},
GPGCheck: common.ToPtr(true),
},
},
},
wantErr: fmt.Errorf("Repository gpg key is not a valid URL or a valid gpg key"),
},
{
name: "Test invalid repository filename error",
expectedCustomizations: Customizations{
Repositories: []RepositoryCustomization{
{
Id: "example-1",
BaseURLs: []string{"http://example-1.com"},
Filename: "!nval!d",
},
},
},
wantErr: fmt.Errorf("Repository filename %q is invalid", "!nval!d.repo"),
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
retCustomizations, err := tt.expectedCustomizations.GetRepositories()
assert.NoError(t, err)
assert.EqualValues(t, tt.expectedCustomizations.Repositories, retCustomizations)
} else {
_, err := tt.expectedCustomizations.GetRepositories()
assert.Equal(t, tt.wantErr, err)
}
})
}
}
func TestCustomRepoFilename(t *testing.T) {
testCases := []struct {
Name string
Repo RepositoryCustomization
WantFilename string
}{
{
Name: "Test default filename #1",
Repo: RepositoryCustomization{
Id: "example-1",
BaseURLs: []string{"http://example-1.com"},
},
WantFilename: "example-1.repo",
},
{
Name: "Test default filename #2",
Repo: RepositoryCustomization{
Id: "example-2",
BaseURLs: []string{"http://example-1.com"},
},
WantFilename: "example-2.repo",
},
{
Name: "Test custom filename",
Repo: RepositoryCustomization{
Id: "example-1",
BaseURLs: []string{"http://example-1.com"},
Filename: "test.repo",
},
WantFilename: "test.repo",
},
{
Name: "Test custom filename without extension",
Repo: RepositoryCustomization{
Id: "example-1",
BaseURLs: []string{"http://example-1.com"},
Filename: "test",
},
WantFilename: "test.repo",
},
}
for _, tt := range testCases {
t.Run(tt.Name, func(t *testing.T) {
got := tt.Repo.getFilename()
assert.Equal(t, tt.WantFilename, got)
})
}
}

View file

@ -1,35 +0,0 @@
package blueprint
// Subscription Manager [rhsm] configuration
type SubManRHSMConfig struct {
ManageRepos *bool `json:"manage_repos,omitempty" toml:"manage_repos,omitempty"`
}
// Subscription Manager [rhsmcertd] configuration
type SubManRHSMCertdConfig struct {
AutoRegistration *bool `json:"auto_registration,omitempty" toml:"auto_registration,omitempty"`
}
// Subscription Manager 'rhsm.conf' configuration
type SubManConfig struct {
RHSMConfig *SubManRHSMConfig `json:"rhsm,omitempty" toml:"rhsm,omitempty"`
RHSMCertdConfig *SubManRHSMCertdConfig `json:"rhsmcertd,omitempty" toml:"rhsmcertd,omitempty"`
}
type DNFPluginConfig struct {
Enabled *bool `json:"enabled,omitempty" toml:"enabled,omitempty"`
}
type SubManDNFPluginsConfig struct {
ProductID *DNFPluginConfig `json:"product_id,omitempty" toml:"product_id,omitempty"`
SubscriptionManager *DNFPluginConfig `json:"subscription_manager,omitempty" toml:"subscription_manager,omitempty"`
}
type RHSMConfig struct {
DNFPlugins *SubManDNFPluginsConfig `json:"dnf_plugins,omitempty" toml:"dnf_plugins,omitempty"`
SubscriptionManager *SubManConfig `json:"subscription_manager,omitempty" toml:"subscription_manager,omitempty"`
}
type RHSMCustomization struct {
Config *RHSMConfig `json:"config,omitempty" toml:"config,omitempty"`
}

View file

@ -1,10 +0,0 @@
package blueprint
type RPMImportKeys struct {
// File paths in the image to import keys from
Files []string `json:"files,omitempty" toml:"files,omitempty"`
}
type RPMCustomization struct {
ImportKeys *RPMImportKeys `json:"import_keys,omitempty" toml:"import_keys,omitempty"`
}

View file

@ -8,7 +8,7 @@ import (
"net/http"
"strings"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/osbuild-composer/internal/weldr"
)

View file

@ -10,13 +10,13 @@ import (
"github.com/google/uuid"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/customizations/subscription"
"github.com/osbuild/images/pkg/datasizes"
"github.com/osbuild/images/pkg/disk"
"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/reporegistry"
"github.com/osbuild/images/pkg/rhsm/facts"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/target"
)

View file

@ -5,13 +5,13 @@ import (
"io/fs"
"testing"
"github.com/osbuild/blueprint/pkg/blueprint"
repos "github.com/osbuild/images/data/repositories"
"github.com/osbuild/images/pkg/customizations/subscription"
"github.com/osbuild/images/pkg/datasizes"
"github.com/osbuild/images/pkg/disk"
"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/reporegistry"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/target"

View file

@ -17,12 +17,12 @@ import (
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/images/pkg/osbuild"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/sbom"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/jsondb"
"github.com/osbuild/osbuild-composer/internal/target"

View file

@ -7,11 +7,11 @@ import (
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/google/uuid"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/disk"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/ostree"
"github.com/osbuild/images/pkg/platform"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/cloud/gcp"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/target"

View file

@ -3,10 +3,10 @@ package v2
import (
"testing"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/distro/rhel/rhel9"
"github.com/osbuild/images/pkg/distro/test_distro"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/target"

View file

@ -22,6 +22,7 @@ import (
"github.com/osbuild/osbuild-composer/pkg/jobqueue"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/container"
"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/dnfjson"
@ -30,7 +31,6 @@ import (
"github.com/osbuild/images/pkg/reporegistry"
"github.com/osbuild/images/pkg/sbom"
"github.com/osbuild/osbuild-composer/internal/auth"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/prometheus"
"github.com/osbuild/osbuild-composer/internal/target"

View file

@ -4,10 +4,10 @@ import (
"time"
"github.com/google/uuid"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/target"
)

View file

@ -5,11 +5,11 @@ import (
"time"
"github.com/google/uuid"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/distro/test_distro"
"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/target"
)

View file

@ -8,12 +8,12 @@ import (
"time"
"github.com/google/uuid"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/target"
)

View file

@ -12,13 +12,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/distro/fedora"
"github.com/osbuild/images/pkg/distro/test_distro"
"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/target"
)

View file

@ -22,8 +22,8 @@ import (
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/osbuild-composer/internal/jsondb"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/target"

View file

@ -7,13 +7,13 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/suite"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/distro/test_distro"
"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/images/pkg/osbuild"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/target"
)

View file

@ -31,6 +31,7 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/osbuild/osbuild-composer/pkg/jobqueue"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/container"
"github.com/osbuild/images/pkg/distro"
@ -43,7 +44,6 @@ import (
"github.com/osbuild/images/pkg/rhsm/facts"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/sbom"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/store"
"github.com/osbuild/osbuild-composer/internal/target"

View file

@ -18,6 +18,7 @@ import (
"time"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/container"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/distro/test_distro"
@ -27,7 +28,6 @@ import (
"github.com/osbuild/images/pkg/ostree/mock_ostree_repo"
"github.com/osbuild/images/pkg/reporegistry"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
dnfjson_mock "github.com/osbuild/osbuild-composer/internal/mocks/dnfjson"
rpmmd_mock "github.com/osbuild/osbuild-composer/internal/mocks/rpmmd"

View file

@ -5,8 +5,8 @@ package weldr
import (
"github.com/google/uuid"
"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/store"
)