debian-forge-composer/internal/cloudapi/v2/compose.go
rverdile 1559b4d760 cloudapi/v2: add templates to subscription image options
Adds optional templateName and templateUUID paramaters to image options,
each used if registering a system to a template a boot.
2025-05-12 10:07:19 +02:00

1450 lines
43 KiB
Go

package v2
// ComposeRequest methods to make it easier to use and test
import (
"crypto/rand"
"fmt"
"math"
"math/big"
"reflect"
"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/distro"
"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/common"
"github.com/osbuild/osbuild-composer/internal/target"
)
// Return the string representation of the partitioning mode
// default to auto-lvm (should never happen)
func (bcpm BlueprintCustomizationsPartitioningMode) String() string {
switch bcpm {
case BlueprintCustomizationsPartitioningModeAutoLvm:
return "auto-lvm"
case BlueprintCustomizationsPartitioningModeLvm:
return "lvm"
case BlueprintCustomizationsPartitioningModeRaw:
return "raw"
default:
return "auto-lvm"
}
}
// GetCustomizationsFromBlueprint populates a blueprint customization struct
// with the data from request Blueprint, which is similar but
// slightly different from the Cloudapi's Customizations section
// This starts with a new empty blueprint.Customization object
// If there are no customizations, it returns nil
func (rbp *Blueprint) GetCustomizationsFromBlueprintRequest() (*blueprint.Customizations, error) {
if rbp.Customizations == nil {
return nil, nil
}
c := &blueprint.Customizations{}
rbpc := rbp.Customizations
if rbpc.Hostname != nil {
c.Hostname = rbpc.Hostname
}
if rbpc.Kernel != nil {
kernel := &blueprint.KernelCustomization{}
if rbpc.Kernel.Name != nil {
kernel.Name = *rbpc.Kernel.Name
}
if rbpc.Kernel.Append != nil {
kernel.Append = *rbpc.Kernel.Append
}
c.Kernel = kernel
}
if rbpc.Sshkey != nil {
keys := []blueprint.SSHKeyCustomization{}
for _, key := range *rbpc.Sshkey {
keys = append(keys, blueprint.SSHKeyCustomization{
User: key.User,
Key: key.Key,
})
}
c.SSHKey = keys
}
if rbpc.User != nil {
var userCustomizations []blueprint.UserCustomization
for _, user := range *rbpc.User {
uc := blueprint.UserCustomization{
Name: user.Name,
Description: user.Description,
Password: user.Password,
Key: user.Key,
Home: user.Home,
Shell: user.Shell,
UID: user.Uid,
GID: user.Gid,
}
if user.Groups != nil {
uc.Groups = append(uc.Groups, *user.Groups...)
}
userCustomizations = append(userCustomizations, uc)
}
c.User = userCustomizations
}
if rbpc.Group != nil {
var groupCustomizations []blueprint.GroupCustomization
for _, group := range *rbpc.Group {
gc := blueprint.GroupCustomization{
Name: group.Name,
GID: group.Gid,
}
groupCustomizations = append(groupCustomizations, gc)
}
c.Group = groupCustomizations
}
if rbpc.Timezone != nil {
tz := &blueprint.TimezoneCustomization{
Timezone: rbpc.Timezone.Timezone,
}
if rbpc.Timezone.Ntpservers != nil {
tz.NTPServers = append(tz.NTPServers, *rbpc.Timezone.Ntpservers...)
}
c.Timezone = tz
}
if rbpc.Locale != nil {
locale := &blueprint.LocaleCustomization{
Keyboard: rbpc.Locale.Keyboard,
}
if rbpc.Locale.Languages != nil {
locale.Languages = append(locale.Languages, *rbpc.Locale.Languages...)
}
c.Locale = locale
}
if rbpc.Firewall != nil {
firewall := &blueprint.FirewallCustomization{}
if rbpc.Firewall.Ports != nil {
firewall.Ports = append(firewall.Ports, *rbpc.Firewall.Ports...)
}
if rbpc.Firewall.Services != nil {
enabled := []string{}
if rbpc.Firewall.Services.Enabled != nil {
enabled = append(enabled, *rbpc.Firewall.Services.Enabled...)
}
disabled := []string{}
if rbpc.Firewall.Services.Disabled != nil {
disabled = append(disabled, *rbpc.Firewall.Services.Disabled...)
}
firewall.Services = &blueprint.FirewallServicesCustomization{
Enabled: enabled,
Disabled: disabled,
}
}
if rbpc.Firewall.Zones != nil {
var zones []blueprint.FirewallZoneCustomization
for _, zone := range *rbpc.Firewall.Zones {
zc := blueprint.FirewallZoneCustomization{}
if zone.Name != nil {
zc.Name = zone.Name
}
if zone.Sources != nil {
zc.Sources = append(zc.Sources, *zone.Sources...)
}
zones = append(zones, zc)
}
firewall.Zones = zones
}
c.Firewall = firewall
}
if rbpc.Services != nil {
servicesCustomization := &blueprint.ServicesCustomization{}
if rbpc.Services.Enabled != nil {
servicesCustomization.Enabled = make([]string, len(*rbpc.Services.Enabled))
copy(servicesCustomization.Enabled, *rbpc.Services.Enabled)
}
if rbpc.Services.Disabled != nil {
servicesCustomization.Disabled = make([]string, len(*rbpc.Services.Disabled))
copy(servicesCustomization.Disabled, *rbpc.Services.Disabled)
}
if rbpc.Services.Masked != nil {
servicesCustomization.Masked = make([]string, len(*rbpc.Services.Masked))
copy(servicesCustomization.Masked, *rbpc.Services.Masked)
}
c.Services = servicesCustomization
}
if rbpc.Filesystem != nil {
var fsCustomizations []blueprint.FilesystemCustomization
for _, f := range *rbpc.Filesystem {
minSize, err := decodeMinsize(&f.Minsize)
if err != nil {
return nil, err
}
fsCustomizations = append(fsCustomizations,
blueprint.FilesystemCustomization{
Mountpoint: f.Mountpoint,
MinSize: minSize,
},
)
}
c.Filesystem = fsCustomizations
}
if rbpc.InstallationDevice != nil {
c.InstallationDevice = *rbpc.InstallationDevice
}
if rbpc.PartitioningMode != nil {
c.PartitioningMode = string(*rbpc.PartitioningMode)
}
if rbpc.Fdo != nil {
fdo := &blueprint.FDOCustomization{}
if rbpc.Fdo.DiunPubKeyHash != nil {
fdo.DiunPubKeyHash = *rbpc.Fdo.DiunPubKeyHash
}
if rbpc.Fdo.DiunPubKeyInsecure != nil {
fdo.DiunPubKeyInsecure = *rbpc.Fdo.DiunPubKeyInsecure
}
if rbpc.Fdo.DiunPubKeyRootCerts != nil {
fdo.DiunPubKeyRootCerts = *rbpc.Fdo.DiunPubKeyRootCerts
}
if rbpc.Fdo.DiMfgStringTypeMacIface != nil {
fdo.DiMfgStringTypeMacIface = *rbpc.Fdo.DiMfgStringTypeMacIface
}
if rbpc.Fdo.ManufacturingServerUrl != nil {
fdo.ManufacturingServerURL = *rbpc.Fdo.ManufacturingServerUrl
}
c.FDO = fdo
}
if rbpc.Openscap != nil {
oscap := &blueprint.OpenSCAPCustomization{
ProfileID: rbpc.Openscap.ProfileId,
}
if rbpc.Openscap.PolicyId != nil {
oscap.PolicyID = rbpc.Openscap.PolicyId.String()
}
if rbpc.Openscap.Datastream != nil {
oscap.DataStream = *rbpc.Openscap.Datastream
}
if rbpc.Openscap.Tailoring != nil && rbpc.Openscap.JsonTailoring != nil {
return nil, fmt.Errorf("OpenSCAP customization error: choose one option between OpenSCAP tailoring and OpenSCAP json tailoring")
}
if tailoring := rbpc.Openscap.Tailoring; tailoring != nil {
tc := blueprint.OpenSCAPTailoringCustomizations{}
if tailoring.Selected != nil && len(*tailoring.Selected) > 0 {
tc.Selected = append(tc.Selected, *tailoring.Selected...)
}
if tailoring.Unselected != nil && len(*tailoring.Unselected) > 0 {
tc.Unselected = append(tc.Unselected, *tailoring.Unselected...)
}
oscap.Tailoring = &tc
}
if jsonTailoring := rbpc.Openscap.JsonTailoring; jsonTailoring != nil {
oscap.JSONTailoring = &blueprint.OpenSCAPJSONTailoringCustomizations{
ProfileID: jsonTailoring.ProfileId,
Filepath: jsonTailoring.Filepath,
}
}
c.OpenSCAP = oscap
}
if rbpc.Ignition != nil {
ignition := &blueprint.IgnitionCustomization{}
if rbpc.Ignition.Embedded != nil {
ignition.Embedded = &blueprint.EmbeddedIgnitionCustomization{
Config: rbpc.Ignition.Embedded.Config,
}
}
if rbpc.Ignition.Firstboot != nil {
ignition.FirstBoot = &blueprint.FirstBootIgnitionCustomization{
ProvisioningURL: rbpc.Ignition.Firstboot.Url,
}
}
c.Ignition = ignition
}
if rbpc.Directories != nil {
var dirCustomizations []blueprint.DirectoryCustomization
for _, d := range *rbpc.Directories {
dirCustomization := blueprint.DirectoryCustomization{
Path: d.Path,
}
if d.Mode != nil {
dirCustomization.Mode = *d.Mode
}
if d.User != nil {
user0, err := d.User.AsDirectoryUser0()
if err == nil {
dirCustomization.User = user0
} else {
user1, err := d.User.AsDirectoryUser1()
if err != nil {
return nil, fmt.Errorf("invalid user: %w", err)
}
dirCustomization.User = user1
}
}
if d.Group != nil {
group0, err := d.Group.AsDirectoryGroup0()
if err == nil {
dirCustomization.Group = group0
} else {
group1, err := d.Group.AsDirectoryGroup1()
if err != nil {
return nil, fmt.Errorf("invalid group: %w", err)
}
dirCustomization.Group = group1
}
}
if d.EnsureParents != nil {
dirCustomization.EnsureParents = *d.EnsureParents
}
dirCustomizations = append(dirCustomizations, dirCustomization)
}
// Validate the directory customizations, because the Cloud API does not use the custom unmarshaller
_, err := blueprint.DirectoryCustomizationsToFsNodeDirectories(dirCustomizations)
if err != nil {
return nil, HTTPErrorWithInternal(ErrorInvalidCustomization, err)
}
c.Directories = dirCustomizations
}
if rbpc.Files != nil {
var fileCustomizations []blueprint.FileCustomization
for _, f := range *rbpc.Files {
fileCustomization := blueprint.FileCustomization{
Path: f.Path,
}
if f.Data != nil {
fileCustomization.Data = *f.Data
}
if f.Mode != nil {
fileCustomization.Mode = *f.Mode
}
if f.User != nil {
user0, err := f.User.AsBlueprintFileUser0()
if err == nil {
fileCustomization.User = user0
} else {
user1, err := f.User.AsBlueprintFileUser1()
if err != nil {
return nil, fmt.Errorf("invalid user: %w", err)
}
fileCustomization.User = user1
}
}
if f.Group != nil {
group0, err := f.Group.AsBlueprintFileGroup0()
if err == nil {
fileCustomization.Group = group0
} else {
group1, err := f.Group.AsBlueprintFileGroup1()
if err != nil {
return nil, fmt.Errorf("invalid group: %w", err)
}
if group1 != 0 {
fileCustomization.Group = group1
}
}
}
fileCustomizations = append(fileCustomizations, fileCustomization)
}
// Validate the file customizations, because the Cloud API does not use the custom unmarshaller
_, err := blueprint.FileCustomizationsToFsNodeFiles(fileCustomizations)
if err != nil {
return nil, HTTPErrorWithInternal(ErrorInvalidCustomization, err)
}
c.Files = fileCustomizations
}
if rbpc.Repositories != nil {
repoCustomizations := []blueprint.RepositoryCustomization{}
for _, repo := range *rbpc.Repositories {
repoCustomization := blueprint.RepositoryCustomization{
Id: repo.Id,
}
if repo.Name != nil {
repoCustomization.Name = *repo.Name
}
if repo.Filename != nil {
repoCustomization.Filename = *repo.Filename
}
if repo.Baseurls != nil && len(*repo.Baseurls) > 0 {
repoCustomization.BaseURLs = append(repoCustomization.BaseURLs, *repo.Baseurls...)
}
if repo.Gpgkeys != nil && len(*repo.Gpgkeys) > 0 {
repoCustomization.GPGKeys = append(repoCustomization.GPGKeys, *repo.Gpgkeys...)
}
if repo.Gpgcheck != nil {
repoCustomization.GPGCheck = repo.Gpgcheck
}
if repo.RepoGpgcheck != nil {
repoCustomization.RepoGPGCheck = repo.RepoGpgcheck
}
if repo.Enabled != nil {
repoCustomization.Enabled = repo.Enabled
}
if repo.Metalink != nil {
repoCustomization.Metalink = *repo.Metalink
}
if repo.Mirrorlist != nil {
repoCustomization.Mirrorlist = *repo.Mirrorlist
}
if repo.Sslverify != nil {
repoCustomization.SSLVerify = repo.Sslverify
}
if repo.Priority != nil {
repoCustomization.Priority = repo.Priority
}
if repo.ModuleHotfixes != nil {
repoCustomization.ModuleHotfixes = repo.ModuleHotfixes
}
repoCustomizations = append(repoCustomizations, repoCustomization)
}
c.Repositories = repoCustomizations
}
if rbpc.Fips != nil {
c.FIPS = rbpc.Fips
}
if installer := rbpc.Installer; installer != nil {
c.Installer = &blueprint.InstallerCustomization{}
if installer.Unattended != nil {
c.Installer.Unattended = *installer.Unattended
}
if installer.SudoNopasswd != nil {
c.Installer.SudoNopasswd = *installer.SudoNopasswd
}
}
if rpm := rbpc.Rpm; rpm != nil && rpm.ImportKeys != nil {
c.RPM = &blueprint.RPMCustomization{
ImportKeys: &blueprint.RPMImportKeys{
Files: *rpm.ImportKeys.Files,
},
}
}
if rhsm := rbpc.Rhsm; rhsm != nil && rhsm.Config != nil {
bpRhsm := &blueprint.RHSMCustomization{
Config: &blueprint.RHSMConfig{},
}
conf := rhsm.Config
if dnfPlugins := conf.DnfPlugins; dnfPlugins != nil {
bpRhsm.Config.DNFPlugins = &blueprint.SubManDNFPluginsConfig{}
if dnfPlugins.ProductId != nil && dnfPlugins.ProductId.Enabled != nil {
bpRhsm.Config.DNFPlugins.ProductID = &blueprint.DNFPluginConfig{
Enabled: common.ToPtr(*dnfPlugins.ProductId.Enabled),
}
}
if dnfPlugins.SubscriptionManager != nil && dnfPlugins.SubscriptionManager.Enabled != nil {
bpRhsm.Config.DNFPlugins.SubscriptionManager = &blueprint.DNFPluginConfig{
Enabled: common.ToPtr(*dnfPlugins.SubscriptionManager.Enabled),
}
}
}
if subMan := conf.SubscriptionManager; subMan != nil {
bpSubMan := &blueprint.SubManConfig{}
if subMan.Rhsm != nil {
bpSubMan.RHSMConfig = &blueprint.SubManRHSMConfig{}
if subMan.Rhsm.ManageRepos != nil {
bpSubMan.RHSMConfig.ManageRepos = common.ToPtr(*subMan.Rhsm.ManageRepos)
}
if subMan.Rhsm.AutoEnableYumPlugins != nil {
bpSubMan.RHSMConfig.AutoEnableYumPlugins = common.ToPtr(*subMan.Rhsm.AutoEnableYumPlugins)
}
}
if subMan.Rhsmcertd != nil && subMan.Rhsmcertd.AutoRegistration != nil {
bpSubMan.RHSMCertdConfig = &blueprint.SubManRHSMCertdConfig{
AutoRegistration: common.ToPtr(*subMan.Rhsmcertd.AutoRegistration),
}
}
bpRhsm.Config.SubscriptionManager = bpSubMan
}
c.RHSM = bpRhsm
}
disk, err := convertDiskCustomizations(rbpc.Disk)
if err != nil {
return nil, err
}
c.Disk = disk
bpDisk, err := convertDiskCustomizations(rbpc.Disk)
if err != nil {
return nil, err
}
c.Disk = bpDisk
return c, nil
}
// GetBlueprintFromCompose returns a base blueprint
// It is either constructed from the Blueprint passed in with the request, or it
// is an empty blueprint
func (request *ComposeRequest) GetBlueprintFromCompose() (blueprint.Blueprint, error) {
// nil or blank blueprint returns a valid empty blueprint
if request.Blueprint == nil || reflect.DeepEqual(*request.Blueprint, Blueprint{}) {
bp := blueprint.Blueprint{Name: "empty blueprint"}
err := bp.Initialize()
return bp, err
}
return ConvertRequestBP(*request.Blueprint)
}
// ConvertRequestBP takes a request Blueprint and returns a composer blueprint.Blueprint
func ConvertRequestBP(rbp Blueprint) (blueprint.Blueprint, error) {
var bp blueprint.Blueprint
// Copy all the parts from the OpenAPI Blueprint into a blueprint.Blueprint
// NOTE: Openapi fields may be nil, test for that first.
bp.Name = rbp.Name
if rbp.Description != nil {
bp.Description = *rbp.Description
}
if rbp.Version != nil {
bp.Version = *rbp.Version
}
if rbp.Distro != nil {
bp.Distro = *rbp.Distro
}
if rbp.Packages != nil {
for _, pkg := range *rbp.Packages {
newPkg := blueprint.Package{Name: pkg.Name}
if pkg.Version != nil {
newPkg.Version = *pkg.Version
}
bp.Packages = append(bp.Packages, newPkg)
}
}
if rbp.Modules != nil {
for _, pkg := range *rbp.Modules {
newPkg := blueprint.Package{Name: pkg.Name}
if pkg.Version != nil {
newPkg.Version = *pkg.Version
}
bp.Modules = append(bp.Modules, newPkg)
}
}
if rbp.EnabledModules != nil {
for _, em := range *rbp.EnabledModules {
bp.EnabledModules = append(bp.EnabledModules, blueprint.EnabledModule{
Name: em.Name,
Stream: em.Stream,
})
}
}
if rbp.Groups != nil {
for _, group := range *rbp.Groups {
bp.Groups = append(bp.Groups, blueprint.Group{
Name: group.Name,
})
}
}
if rbp.Containers != nil {
for _, c := range *rbp.Containers {
newC := blueprint.Container{Source: c.Source, TLSVerify: c.TlsVerify}
if c.Name != nil {
newC.Name = *c.Name
}
bp.Containers = append(bp.Containers, newC)
}
}
customizations, err := rbp.GetCustomizationsFromBlueprintRequest()
if err != nil {
return bp, err
}
bp.Customizations = customizations
err = bp.Initialize()
if err != nil {
return bp, HTTPErrorWithInternal(ErrorFailedToInitializeBlueprint, err)
}
return bp, nil
}
// GetBlueprintFromCustomizations returns a new Blueprint with all of the
// customizations set from the ComposeRequest.Customizations
func (request *ComposeRequest) GetBlueprintFromCustomizations() (blueprint.Blueprint, error) {
bp := blueprint.Blueprint{Name: "empty blueprint"}
err := bp.Initialize()
if err != nil {
return bp, HTTPErrorWithInternal(ErrorFailedToInitializeBlueprint, err)
}
if request.Customizations == nil {
return bp, nil
}
bp.Customizations = &blueprint.Customizations{}
// Set the blueprint customisation to take care of the user
if request.Customizations.Users != nil {
var userCustomizations []blueprint.UserCustomization
for _, user := range *request.Customizations.Users {
var groups []string
if user.Groups != nil {
groups = *user.Groups
} else {
groups = nil
}
userCustomizations = append(userCustomizations,
blueprint.UserCustomization{
Name: user.Name,
Key: user.Key,
Password: user.Password,
Groups: groups,
},
)
}
bp.Customizations.User = userCustomizations
}
if request.Customizations.Packages != nil {
for _, p := range *request.Customizations.Packages {
bp.Packages = append(bp.Packages, blueprint.Package{
Name: p,
})
}
}
if request.Customizations.EnabledModules != nil {
for _, em := range *request.Customizations.EnabledModules {
bp.EnabledModules = append(bp.EnabledModules, blueprint.EnabledModule{
Name: em.Name,
Stream: em.Stream,
})
}
}
if request.Customizations.Containers != nil {
for _, c := range *request.Customizations.Containers {
bc := blueprint.Container{
Source: c.Source,
TLSVerify: c.TlsVerify,
}
if c.Name != nil {
bc.Name = *c.Name
}
bp.Containers = append(bp.Containers, bc)
}
}
if request.Customizations.Directories != nil {
var dirCustomizations []blueprint.DirectoryCustomization
for _, d := range *request.Customizations.Directories {
dirCustomization := blueprint.DirectoryCustomization{
Path: d.Path,
}
if d.Mode != nil {
dirCustomization.Mode = *d.Mode
}
if d.User != nil {
user0, err := d.User.AsDirectoryUser0()
if err == nil {
dirCustomization.User = user0
} else {
user1, err := d.User.AsDirectoryUser1()
if err != nil {
return bp, fmt.Errorf("invalid user: %w", err)
}
dirCustomization.User = user1
}
}
if d.Group != nil {
group0, err := d.Group.AsDirectoryGroup0()
if err == nil {
dirCustomization.Group = group0
} else {
group1, err := d.Group.AsDirectoryGroup1()
if err != nil {
return bp, fmt.Errorf("invalid group: %w", err)
}
dirCustomization.Group = group1
}
}
if d.EnsureParents != nil {
dirCustomization.EnsureParents = *d.EnsureParents
}
dirCustomizations = append(dirCustomizations, dirCustomization)
}
// Validate the directory customizations, because the Cloud API does not use the custom unmarshaller
_, err := blueprint.DirectoryCustomizationsToFsNodeDirectories(dirCustomizations)
if err != nil {
return bp, HTTPErrorWithInternal(ErrorInvalidCustomization, err)
}
bp.Customizations.Directories = dirCustomizations
}
if request.Customizations.Files != nil {
var fileCustomizations []blueprint.FileCustomization
for _, f := range *request.Customizations.Files {
fileCustomization := blueprint.FileCustomization{
Path: f.Path,
}
if f.Data != nil {
fileCustomization.Data = *f.Data
}
if f.Mode != nil {
fileCustomization.Mode = *f.Mode
}
if f.User != nil {
user0, err := f.User.AsFileUser0()
if err == nil {
fileCustomization.User = user0
} else {
user1, err := f.User.AsFileUser1()
if err != nil {
return bp, fmt.Errorf("invalid user: %w", err)
}
fileCustomization.User = user1
}
}
if f.Group != nil {
group0, err := f.Group.AsFileGroup0()
if err == nil {
fileCustomization.Group = group0
} else {
group1, err := f.Group.AsFileGroup1()
if err != nil {
return bp, fmt.Errorf("invalid group: %w", err)
}
fileCustomization.Group = group1
}
}
fileCustomizations = append(fileCustomizations, fileCustomization)
}
// Validate the file customizations, because the Cloud API does not use the custom unmarshaller
_, err := blueprint.FileCustomizationsToFsNodeFiles(fileCustomizations)
if err != nil {
return bp, HTTPErrorWithInternal(ErrorInvalidCustomization, err)
}
bp.Customizations.Files = fileCustomizations
}
if request.Customizations.Filesystem != nil {
var fsCustomizations []blueprint.FilesystemCustomization
for _, f := range *request.Customizations.Filesystem {
fsCustomizations = append(fsCustomizations,
blueprint.FilesystemCustomization{
Mountpoint: f.Mountpoint,
MinSize: f.MinSize,
},
)
}
bp.Customizations.Filesystem = fsCustomizations
}
if request.Customizations.Services != nil {
servicesCustomization := &blueprint.ServicesCustomization{}
if request.Customizations.Services.Enabled != nil {
servicesCustomization.Enabled = make([]string, len(*request.Customizations.Services.Enabled))
copy(servicesCustomization.Enabled, *request.Customizations.Services.Enabled)
}
if request.Customizations.Services.Disabled != nil {
servicesCustomization.Disabled = make([]string, len(*request.Customizations.Services.Disabled))
copy(servicesCustomization.Disabled, *request.Customizations.Services.Disabled)
}
if request.Customizations.Services.Masked != nil {
servicesCustomization.Masked = make([]string, len(*request.Customizations.Services.Masked))
copy(servicesCustomization.Masked, *request.Customizations.Services.Masked)
}
bp.Customizations.Services = servicesCustomization
}
if request.Customizations.Openscap != nil {
openSCAPCustomization := &blueprint.OpenSCAPCustomization{
ProfileID: request.Customizations.Openscap.ProfileId,
}
if request.Customizations.Openscap.PolicyId != nil {
openSCAPCustomization.PolicyID = request.Customizations.Openscap.PolicyId.String()
}
if request.Customizations.Openscap.Tailoring != nil && request.Customizations.Openscap.JsonTailoring != nil {
return bp, fmt.Errorf("OpenSCAP customization error: choose one option between OpenSCAP tailoring and OpenSCAP json tailoring")
}
if tailoring := request.Customizations.Openscap.Tailoring; tailoring != nil {
tailoringCustomizations := blueprint.OpenSCAPTailoringCustomizations{}
if tailoring.Selected != nil && len(*tailoring.Selected) > 0 {
tailoringCustomizations.Selected = *tailoring.Selected
}
if tailoring.Unselected != nil && len(*tailoring.Unselected) > 0 {
tailoringCustomizations.Unselected = *tailoring.Unselected
}
openSCAPCustomization.Tailoring = &tailoringCustomizations
}
if jsonTailoring := request.Customizations.Openscap.JsonTailoring; jsonTailoring != nil {
openSCAPCustomization.JSONTailoring = &blueprint.OpenSCAPJSONTailoringCustomizations{
ProfileID: jsonTailoring.ProfileId,
Filepath: jsonTailoring.Filepath,
}
}
bp.Customizations.OpenSCAP = openSCAPCustomization
}
if request.Customizations.CustomRepositories != nil {
repoCustomizations := []blueprint.RepositoryCustomization{}
for _, repo := range *request.Customizations.CustomRepositories {
repoCustomization := blueprint.RepositoryCustomization{
Id: repo.Id,
}
if repo.Name != nil {
repoCustomization.Name = *repo.Name
}
if repo.Filename != nil {
repoCustomization.Filename = *repo.Filename
}
if repo.Baseurl != nil && len(*repo.Baseurl) > 0 {
repoCustomization.BaseURLs = *repo.Baseurl
}
if repo.Gpgkey != nil && len(*repo.Gpgkey) > 0 {
repoCustomization.GPGKeys = *repo.Gpgkey
}
if repo.CheckGpg != nil {
repoCustomization.GPGCheck = repo.CheckGpg
}
if repo.CheckRepoGpg != nil {
repoCustomization.RepoGPGCheck = repo.CheckRepoGpg
}
if repo.Enabled != nil {
repoCustomization.Enabled = repo.Enabled
}
if repo.Metalink != nil {
repoCustomization.Metalink = *repo.Metalink
}
if repo.Mirrorlist != nil {
repoCustomization.Mirrorlist = *repo.Mirrorlist
}
if repo.SslVerify != nil {
repoCustomization.SSLVerify = repo.SslVerify
}
if repo.Priority != nil {
repoCustomization.Priority = repo.Priority
}
if repo.ModuleHotfixes != nil {
repoCustomization.ModuleHotfixes = repo.ModuleHotfixes
}
repoCustomizations = append(repoCustomizations, repoCustomization)
}
bp.Customizations.Repositories = repoCustomizations
}
if request.Customizations.Hostname != nil {
bp.Customizations.Hostname = request.Customizations.Hostname
}
if request.Customizations.Kernel != nil {
kernel := &blueprint.KernelCustomization{}
if request.Customizations.Kernel.Name != nil {
kernel.Name = *request.Customizations.Kernel.Name
}
if request.Customizations.Kernel.Append != nil {
kernel.Append = *request.Customizations.Kernel.Append
}
bp.Customizations.Kernel = kernel
}
if request.Customizations.Groups != nil {
groups := []blueprint.GroupCustomization{}
for _, group := range *request.Customizations.Groups {
groups = append(groups, blueprint.GroupCustomization{
Name: group.Name,
GID: group.Gid,
})
}
bp.Customizations.Group = groups
}
if request.Customizations.Timezone != nil {
tz := &blueprint.TimezoneCustomization{
Timezone: request.Customizations.Timezone.Timezone,
}
if request.Customizations.Timezone.Ntpservers != nil {
tz.NTPServers = append(tz.NTPServers, *request.Customizations.Timezone.Ntpservers...)
}
bp.Customizations.Timezone = tz
}
if request.Customizations.Locale != nil {
locale := &blueprint.LocaleCustomization{
Keyboard: request.Customizations.Locale.Keyboard,
}
if request.Customizations.Locale.Languages != nil {
locale.Languages = append(locale.Languages, *request.Customizations.Locale.Languages...)
}
bp.Customizations.Locale = locale
}
if request.Customizations.Firewall != nil {
firewall := &blueprint.FirewallCustomization{}
if request.Customizations.Firewall.Ports != nil {
firewall.Ports = append(firewall.Ports, *request.Customizations.Firewall.Ports...)
}
if request.Customizations.Firewall.Services != nil {
enabled := []string{}
if request.Customizations.Firewall.Services.Enabled != nil {
enabled = append(enabled, *request.Customizations.Firewall.Services.Enabled...)
}
disabled := []string{}
if request.Customizations.Firewall.Services.Disabled != nil {
disabled = append(disabled, *request.Customizations.Firewall.Services.Disabled...)
}
firewall.Services = &blueprint.FirewallServicesCustomization{
Enabled: enabled,
Disabled: disabled,
}
}
bp.Customizations.Firewall = firewall
}
if request.Customizations.InstallationDevice != nil {
if bp.Customizations == nil {
bp.Customizations = &blueprint.Customizations{
InstallationDevice: *request.Customizations.InstallationDevice,
}
} else {
bp.Customizations.InstallationDevice = *request.Customizations.InstallationDevice
}
}
if request.Customizations.Fdo != nil {
fdo := &blueprint.FDOCustomization{}
if request.Customizations.Fdo.DiunPubKeyHash != nil {
fdo.DiunPubKeyHash = *request.Customizations.Fdo.DiunPubKeyHash
}
if request.Customizations.Fdo.DiunPubKeyInsecure != nil {
fdo.DiunPubKeyInsecure = *request.Customizations.Fdo.DiunPubKeyInsecure
}
if request.Customizations.Fdo.DiunPubKeyRootCerts != nil {
fdo.DiunPubKeyRootCerts = *request.Customizations.Fdo.DiunPubKeyRootCerts
}
if request.Customizations.Fdo.ManufacturingServerUrl != nil {
fdo.ManufacturingServerURL = *request.Customizations.Fdo.ManufacturingServerUrl
}
if request.Customizations.Fdo.DiMfgStringTypeMacIface != nil {
fdo.DiMfgStringTypeMacIface = *request.Customizations.Fdo.DiMfgStringTypeMacIface
}
bp.Customizations.FDO = fdo
}
if request.Customizations.Ignition != nil {
ignition := &blueprint.IgnitionCustomization{}
if request.Customizations.Ignition.Embedded != nil {
ignition.Embedded = &blueprint.EmbeddedIgnitionCustomization{
Config: request.Customizations.Ignition.Embedded.Config,
}
}
if request.Customizations.Ignition.Firstboot != nil {
ignition.FirstBoot = &blueprint.FirstBootIgnitionCustomization{
ProvisioningURL: request.Customizations.Ignition.Firstboot.Url,
}
}
bp.Customizations.Ignition = ignition
}
if request.Customizations.Fips != nil {
bp.Customizations.FIPS = request.Customizations.Fips.Enabled
}
if request.Customizations.Installer != nil {
installer := &blueprint.InstallerCustomization{}
if request.Customizations.Installer.Unattended != nil {
installer.Unattended = *request.Customizations.Installer.Unattended
}
if request.Customizations.Installer.SudoNopasswd != nil {
installer.SudoNopasswd = *request.Customizations.Installer.SudoNopasswd
}
bp.Customizations.Installer = installer
}
if request.Customizations.Rpm != nil && request.Customizations.Rpm.ImportKeys != nil {
bp.Customizations.RPM = &blueprint.RPMCustomization{
ImportKeys: &blueprint.RPMImportKeys{
Files: *request.Customizations.Rpm.ImportKeys.Files,
},
}
}
if rhsm := request.Customizations.Rhsm; rhsm != nil && rhsm.Config != nil {
bpRhsm := &blueprint.RHSMCustomization{
Config: &blueprint.RHSMConfig{},
}
conf := rhsm.Config
if conf.DnfPlugins != nil {
bpRhsm.Config.DNFPlugins = &blueprint.SubManDNFPluginsConfig{}
if conf.DnfPlugins.ProductId != nil && conf.DnfPlugins.ProductId.Enabled != nil {
bpRhsm.Config.DNFPlugins.ProductID = &blueprint.DNFPluginConfig{
Enabled: common.ToPtr(*conf.DnfPlugins.ProductId.Enabled),
}
}
if conf.DnfPlugins.SubscriptionManager != nil && conf.DnfPlugins.SubscriptionManager.Enabled != nil {
bpRhsm.Config.DNFPlugins.SubscriptionManager = &blueprint.DNFPluginConfig{
Enabled: common.ToPtr(*conf.DnfPlugins.SubscriptionManager.Enabled),
}
}
}
if subMan := conf.SubscriptionManager; subMan != nil {
bpSubMan := &blueprint.SubManConfig{}
if subMan.Rhsm != nil {
bpSubMan.RHSMConfig = &blueprint.SubManRHSMConfig{}
if subMan.Rhsm.ManageRepos != nil {
bpSubMan.RHSMConfig.ManageRepos = common.ToPtr(*subMan.Rhsm.ManageRepos)
}
if subMan.Rhsm.AutoEnableYumPlugins != nil {
bpSubMan.RHSMConfig.AutoEnableYumPlugins = common.ToPtr(*subMan.Rhsm.AutoEnableYumPlugins)
}
}
if subMan.Rhsmcertd != nil && subMan.Rhsmcertd.AutoRegistration != nil {
bpSubMan.RHSMCertdConfig = &blueprint.SubManRHSMCertdConfig{
AutoRegistration: common.ToPtr(*subMan.Rhsmcertd.AutoRegistration),
}
}
bpRhsm.Config.SubscriptionManager = bpSubMan
}
bp.Customizations.RHSM = bpRhsm
}
bp.Customizations.Disk, err = convertDiskCustomizations(request.Customizations.Disk)
if err != nil {
return bp, err
}
if cacerts := request.Customizations.Cacerts; cacerts != nil {
bp.Customizations.CACerts = &blueprint.CACustomization{
PEMCerts: cacerts.PemCerts,
}
}
// Did bp.Customizations get set at all? If not, set it back to nil
if reflect.DeepEqual(*bp.Customizations, blueprint.Customizations{}) {
bp.Customizations = nil
}
err = bp.CryptPasswords()
if err != nil {
return bp, fmt.Errorf("Error hashing passwords: %s", err.Error())
}
return bp, nil
}
// GetBlueprint returns a blueprint
// If the compose request includes a blueprint, return it, otherwise if it has
// customizations create a blueprint with those customizations. If it has neither
// return an empty blueprint.
func (request *ComposeRequest) GetBlueprint() (blueprint.Blueprint, error) {
if request.Blueprint != nil {
return request.GetBlueprintFromCompose()
}
return request.GetBlueprintFromCustomizations()
}
// GetPayloadRepositories returns the custom repos
// If there are none it returns a nil slice
func (request *ComposeRequest) GetPayloadRepositories() (repos []Repository) {
if request.Customizations != nil && request.Customizations.PayloadRepositories != nil {
repos = *request.Customizations.PayloadRepositories
}
return
}
// GetSubscription returns an ImageOptions struct populated by the subscription information
// included in the request, or nil if it has not been included.
func (request *ComposeRequest) GetSubscription() (sub *subscription.ImageOptions) {
if request.Customizations != nil && request.Customizations.Subscription != nil {
// Rhc is optional, default to false if not included
var rhc bool
if request.Customizations.Subscription.Rhc != nil {
rhc = *request.Customizations.Subscription.Rhc
}
var insightsClientProxy string
if request.Customizations.Subscription.InsightsClientProxy != nil {
insightsClientProxy = *request.Customizations.Subscription.InsightsClientProxy
}
var templateName string
if request.Customizations.Subscription.TemplateName != nil {
templateName = *request.Customizations.Subscription.TemplateName
}
var templateUUID string
if request.Customizations.Subscription.TemplateUuid != nil {
templateUUID = *request.Customizations.Subscription.TemplateUuid
}
sub = &subscription.ImageOptions{
Organization: request.Customizations.Subscription.Organization,
ActivationKey: request.Customizations.Subscription.ActivationKey,
ServerUrl: request.Customizations.Subscription.ServerUrl,
BaseUrl: request.Customizations.Subscription.BaseUrl,
Insights: request.Customizations.Subscription.Insights,
Rhc: rhc,
Proxy: insightsClientProxy,
TemplateName: templateName,
TemplateUUID: templateUUID,
}
}
return
}
// GetPartitioningMode returns the partitioning mode included in the request
// or defaults to AutoLVMPartitioningMode if not included
func (request *ComposeRequest) GetPartitioningMode() (disk.PartitioningMode, error) {
if request.Customizations == nil || request.Customizations.PartitioningMode == nil {
return disk.AutoLVMPartitioningMode, nil
}
switch *request.Customizations.PartitioningMode {
case CustomizationsPartitioningModeRaw:
return disk.RawPartitioningMode, nil
case CustomizationsPartitioningModeLvm:
return disk.LVMPartitioningMode, nil
case CustomizationsPartitioningModeAutoLvm:
return disk.AutoLVMPartitioningMode, nil
}
return disk.AutoLVMPartitioningMode, HTTPError(ErrorInvalidPartitioningMode)
}
// GetImageRequests converts a composeRequest structure from the API to an intermediate imageRequest structure
// that's used for generating manifests and orchestrating worker jobs.
func (request *ComposeRequest) GetImageRequests(distroFactory *distrofactory.Factory, repoRegistry *reporegistry.RepoRegistry) ([]imageRequest, error) {
// OpenAPI enforces blueprint or customization, not both
// but check anyway
if request.Customizations != nil && request.Blueprint != nil {
return nil, HTTPError(ErrorBlueprintOrCustomNotBoth)
}
// Create a blueprint from the request
bp, err := request.GetBlueprint()
if err != nil {
return nil, err
}
// Used when no repositories are included. Must be the original name because it may
// be an alias and you cannot map back from the distrofactory to the original string.
originalDistroName := request.Distribution
// If there is a distribution in the blueprint it overrides the request's distro
if len(bp.Distro) > 0 {
originalDistroName = bp.Distro
}
distribution := distroFactory.GetDistro(originalDistroName)
if distribution == nil {
return nil, HTTPError(ErrorUnsupportedDistribution)
}
// add the user-defined repositories only to the depsolve job for the
// payload (the packages for the final image)
payloadRepositories := request.GetPayloadRepositories()
// use the same seed for all images so we get the same IDs
bigSeed, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
return nil, HTTPError(ErrorFailedToGenerateManifestSeed)
}
manifestSeed := bigSeed.Int64()
// For backwards compatibility, we support both a single image request
// as well as an array of requests in the API. Exactly one must be
// specified.
if request.ImageRequest != nil {
if request.ImageRequests != nil {
// we should really be using oneOf in the spec
return nil, HTTPError(ErrorInvalidNumberOfImageBuilds)
}
request.ImageRequests = &[]ImageRequest{*request.ImageRequest}
}
if request.ImageRequests == nil {
return nil, HTTPError(ErrorInvalidNumberOfImageBuilds)
}
var irs []imageRequest
for _, ir := range *request.ImageRequests {
arch, err := distribution.GetArch(ir.Architecture)
if err != nil {
return nil, HTTPError(ErrorUnsupportedArchitecture)
}
imageType, err := arch.GetImageType(imageTypeFromApiImageType(ir.ImageType, arch))
if err != nil {
return nil, HTTPError(ErrorUnsupportedImageType)
}
repos, err := convertRepos(ir.Repositories, payloadRepositories, imageType.PayloadPackageSets())
if err != nil {
return nil, err
}
// If no repositories are included with the imageRequest use the defaults for
// the distro selected by the blueprint, or the compose request.
if len(ir.Repositories) == 0 {
dr, err := repoRegistry.ReposByImageTypeName(originalDistroName, arch.Name(), imageType.Name())
if err != nil {
return nil, err
}
repos = append(repos, dr...)
}
// Initialise the image options from the image request and
// customizations
imageOptions := distro.ImageOptions{}
if ir.Size != nil {
imageOptions.Size = *ir.Size
}
if request.Koji == nil {
imageOptions.Facts = &facts.ImageOptions{
APIType: facts.CLOUDV2_APITYPE,
}
oscap := bp.Customizations.GetOpenSCAP()
if oscap != nil {
if oscap.ProfileID != "" {
imageOptions.Facts.OpenSCAPProfileID = oscap.ProfileID
}
if oscap.PolicyID != "" {
policyID, err := uuid.Parse(oscap.PolicyID)
if err != nil {
return nil, fmt.Errorf("Unable to parse %s as a uuid: %w", oscap.PolicyID, err)
}
imageOptions.Facts.CompliancePolicyID = policyID
}
}
}
// Set Subscription from the compose request
imageOptions.Subscription = request.GetSubscription()
// Set PartitioningMode from the compose request
imageOptions.PartitioningMode, err = request.GetPartitioningMode()
if err != nil {
return nil, err
}
// Set OSTree options from the image request
imageOptions.OSTree, err = ir.GetOSTreeOptions()
if err != nil {
return nil, err
}
var irTargets []*target.Target
if ir.UploadOptions == nil && (ir.UploadTargets == nil || len(*ir.UploadTargets) == 0) {
// nowhere to put the image, this is a user error
if request.Koji == nil {
return nil, HTTPError(ErrorJSONUnMarshallingError)
}
} else {
// Get the target for the selected image type
irTargets, err = ir.GetTargets(request, imageType)
if err != nil {
return nil, err
}
}
irs = append(irs, imageRequest{
imageType: imageType,
repositories: repos,
imageOptions: imageOptions,
targets: irTargets,
blueprint: bp,
manifestSeed: manifestSeed,
})
}
return irs, nil
}
func convertDiskCustomizations(disk *Disk) (*blueprint.DiskCustomization, error) {
if disk == nil {
return nil, nil
}
diskSize, err := decodeMinsize(disk.Minsize)
if err != nil {
return nil, err
}
bpDisk := &blueprint.DiskCustomization{
MinSize: diskSize,
Type: string(common.DerefOrDefault(disk.Type)),
}
for idx, partition := range disk.Partitions {
// partition successfully converts to all three types, so convert to
// filesystem to sniff the type string
sniffer, err := partition.AsFilesystemTyped()
if err != nil {
return nil, fmt.Errorf("failed to deserialize disk customization partition %d", idx)
}
var bpPartition blueprint.PartitionCustomization
switch partType := common.DerefOrDefault(sniffer.Type); string(partType) {
case string(Plain):
fs, err := partition.AsFilesystemTyped()
if err != nil {
return nil, fmt.Errorf("failed to deserialize disk customization partition %d with type %q", idx, partType)
}
fsSize, err := decodeMinsize(fs.Minsize)
if err != nil {
return nil, err
}
bpPartition = blueprint.PartitionCustomization{
Type: string(common.DerefOrDefault(fs.Type)),
PartType: common.DerefOrDefault(fs.PartType),
MinSize: fsSize,
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
Mountpoint: fs.Mountpoint,
Label: common.DerefOrDefault(fs.Label),
FSType: string(common.DerefOrDefault(fs.FsType)),
},
}
case string(Btrfs):
btrfsVol, err := partition.AsBtrfsVolume()
if err != nil {
return nil, fmt.Errorf("failed to deserialize disk customization partition %d with type %q", idx, partType)
}
volSize, err := decodeMinsize(btrfsVol.Minsize)
if err != nil {
return nil, err
}
bpPartition = blueprint.PartitionCustomization{
Type: string(common.DerefOrDefault(btrfsVol.Type)),
PartType: common.DerefOrDefault(btrfsVol.PartType),
MinSize: volSize,
}
for _, subvol := range btrfsVol.Subvolumes {
bpSubvol := blueprint.BtrfsSubvolumeCustomization{
Name: subvol.Name,
Mountpoint: subvol.Mountpoint,
}
bpPartition.Subvolumes = append(bpPartition.Subvolumes, bpSubvol)
}
case string(Lvm):
vg, err := partition.AsVolumeGroup()
if err != nil {
return nil, fmt.Errorf("failed to deserialize disk customization partition %d with type %q", idx, partType)
}
vgSize, err := decodeMinsize(vg.Minsize)
if err != nil {
return nil, err
}
bpPartition = blueprint.PartitionCustomization{
Type: string(common.DerefOrDefault(vg.Type)),
PartType: common.DerefOrDefault(vg.PartType),
MinSize: vgSize,
VGCustomization: blueprint.VGCustomization{
Name: common.DerefOrDefault(vg.Name),
},
}
for _, lv := range vg.LogicalVolumes {
lvSize, err := decodeMinsize(lv.Minsize)
if err != nil {
return nil, err
}
bpLV := blueprint.LVCustomization{
Name: common.DerefOrDefault(lv.Name),
MinSize: lvSize,
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
Mountpoint: lv.Mountpoint,
Label: common.DerefOrDefault(lv.Label),
FSType: string(common.DerefOrDefault(lv.FsType)),
},
}
bpPartition.LogicalVolumes = append(bpPartition.LogicalVolumes, bpLV)
}
default:
return nil, fmt.Errorf("disk customization partition %d has invalid or unknown type %q", idx, partType)
}
bpDisk.Partitions = append(bpDisk.Partitions, bpPartition)
}
return bpDisk, nil
}
func decodeMinsize(size *Minsize) (uint64, error) {
if size == nil {
return 0, nil
}
return datasizes.Parse(*size)
}