debian-forge-composer/internal/blueprint/blueprint.go
2025-01-10 10:26:54 +01:00

431 lines
13 KiB
Go

// 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"`
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 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.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
}
// 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 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 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,
Groups: groups,
Containers: containers,
Customizations: customizations,
Distro: bp.Distro,
}
return ibp
}