Delete internal/blueprint/ and import from osbuild/blueprint
Import osbuild/blueprint v1.6.0
This commit is contained in:
parent
362712a71d
commit
cf956ff5a6
93 changed files with 2300 additions and 4163 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue