debian-forge-composer/internal/manifest/os.go
Achilleas Koutsou 99952afbd4 Separate ostree ref from parent spec in Pipelines and ImageKinds
In the OS pipeline, the parent configuration was used to detect if the
pipeline's setup was meant for an ostree commit or not.  Also, the
pipeline used a new type to specify the ostree parameters.

- Use the ostree.CommitSpec for the parent configuration.
- Add a new attribute, OSTreeRef, that defines the ref for the ostree
  commit being built.  An empty string indicates that the tree is not
  for an ostree commit.

Additionally, in the ImageKind configurations for the ostree archive and
container, separate the ostree ref from the parent spec, make the parent
spec optional (pointer) and the ostree ref mandatory, by requiring it in
the constructor of the ImageKind.
2022-10-11 10:00:22 +02:00

494 lines
15 KiB
Go

package manifest
import (
"fmt"
"path/filepath"
"strings"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/disk"
"github.com/osbuild/osbuild-composer/internal/environment"
"github.com/osbuild/osbuild-composer/internal/osbuild"
"github.com/osbuild/osbuild-composer/internal/ostree"
"github.com/osbuild/osbuild-composer/internal/platform"
"github.com/osbuild/osbuild-composer/internal/rpmmd"
"github.com/osbuild/osbuild-composer/internal/users"
"github.com/osbuild/osbuild-composer/internal/workload"
)
// OSCustomizations encapsulates all configuration applied to the base
// operating system independently of where and how it is integrated and what
// workload it is running.
// TODO: move out kernel/bootloader/cloud-init/... to other
// abstractions, this should ideally only contain things that
// can always be applied.
type OSCustomizations struct {
// Packages to install in addition to the ones required by the
// pipeline.
ExtraBasePackages []string
// Packages to exclude from the base package set. This is useful in
// case of weak dependencies, comps groups, or where multiple packages
// can satisfy a dependency. Must not conflict with the included base
// package set.
ExcludeBasePackages []string
// Additional repos to install the base packages from.
ExtraBaseRepos []rpmmd.RepoConfig
// KernelName indicates that a kernel is installed, and names the kernel
// package.
KernelName string
// KernelOptionsAppend are appended to the kernel commandline
KernelOptionsAppend []string
// UEFIVendor indicates whether or not the image should support UEFI and
// if set namespaces the UEFI binaries with this string.
GPGKeyFiles []string
Language string
Keyboard *string
Hostname string
Timezone string
NTPServers []string
EnabledServices []string
DisabledServices []string
DefaultTarget string
// SELinux policy, when set it enables the labeling of the tree with the
// selected profile
SElinux string
// Do not install documentation
ExcludeDocs bool
Groups []users.Group
Users []users.User
// TODO: drop blueprint types from the API
Firewall *blueprint.FirewallCustomization
// TODO: drop osbuild types from the API
Grub2Config *osbuild.GRUB2Config
Sysconfig []*osbuild.SysconfigStageOptions
SystemdLogind []*osbuild.SystemdLogindStageOptions
CloudInit []*osbuild.CloudInitStageOptions
Modprobe []*osbuild.ModprobeStageOptions
DracutConf []*osbuild.DracutConfStageOptions
SystemdUnit []*osbuild.SystemdUnitStageOptions
Authselect *osbuild.AuthselectStageOptions
SELinuxConfig *osbuild.SELinuxConfigStageOptions
Tuned *osbuild.TunedStageOptions
Tmpfilesd []*osbuild.TmpfilesdStageOptions
PamLimitsConf []*osbuild.PamLimitsConfStageOptions
Sysctld []*osbuild.SysctldStageOptions
DNFConfig []*osbuild.DNFConfigStageOptions
SshdConfig *osbuild.SshdConfigStageOptions
AuthConfig *osbuild.AuthconfigStageOptions
PwQuality *osbuild.PwqualityConfStageOptions
OpenSCAPConfig *osbuild.OscapRemediationStageOptions
}
// OS represents the filesystem tree of the target image. This roughly
// corresponds to the root filesystem once an instance of the image is running.
type OS struct {
Base
// Customizations to apply to the base OS
OSCustomizations
// Environment the system will run in
Environment environment.Environment
// Workload to install on top of the base system
Workload workload.Workload
// Ref of ostree commit, if empty the tree cannot be in an ostree commit
OSTreeRef string
// OSTree parent spec, if nil the new commit (if applicable) will have no parent
OSTreeParent *ostree.CommitSpec
// Partition table, if nil the tree cannot be put on a partitioned disk
PartitionTable *disk.PartitionTable
repos []rpmmd.RepoConfig
packageSpecs []rpmmd.PackageSpec
platform platform.Platform
kernelVer string
}
// NewOS creates a new OS pipeline. build is the build pipeline to use for
// building the OS pipeline. platform is the target platform for the final
// image. repos are the repositories to install RPMs from.
func NewOS(m *Manifest,
buildPipeline *Build,
platform platform.Platform,
repos []rpmmd.RepoConfig) *OS {
p := &OS{
Base: NewBase(m, "os", buildPipeline),
repos: repos,
platform: platform,
}
buildPipeline.addDependent(p)
m.addPipeline(p)
return p
}
func (p *OS) getPackageSetChain() []rpmmd.PackageSet {
packages := p.platform.GetPackages()
if p.KernelName != "" {
packages = append(packages, p.KernelName)
}
// If we have a logical volume we need to include the lvm2 package.
// OSTree-based images (commit and container) aren't bootable images and
// don't have partition tables.
if p.PartitionTable != nil && p.OSTreeRef == "" {
packages = append(packages, p.PartitionTable.GetBuildPackages()...)
}
if p.Environment != nil {
packages = append(packages, p.Environment.GetPackages()...)
}
if len(p.NTPServers) > 0 {
packages = append(packages, "chrony")
}
if p.SElinux != "" {
packages = append(packages, fmt.Sprintf("selinux-policy-%s", p.SElinux))
}
if p.OpenSCAPConfig != nil {
packages = append(packages, "openscap-scanner", "scap-security-guide")
}
chain := []rpmmd.PackageSet{
{
Include: append(packages, p.ExtraBasePackages...),
Exclude: p.ExcludeBasePackages,
Repositories: append(p.repos, p.ExtraBaseRepos...),
},
}
if p.Workload != nil {
workloadPackages := p.Workload.GetPackages()
if len(workloadPackages) > 0 {
chain = append(chain, rpmmd.PackageSet{
Include: workloadPackages,
Repositories: append(p.repos, p.Workload.GetRepos()...),
})
}
}
return chain
}
func (p *OS) getBuildPackages() []string {
packages := p.platform.GetBuildPackages()
packages = append(packages, "rpm")
if p.OSTreeRef != "" {
packages = append(packages, "rpm-ostree")
}
if p.SElinux != "" {
packages = append(packages, "policycoreutils")
packages = append(packages, fmt.Sprintf("selinux-policy-%s", p.SElinux))
}
return packages
}
func (p *OS) getOSTreeCommits() []osTreeCommit {
commits := []osTreeCommit{}
if p.OSTreeRef != "" && p.OSTreeParent != nil {
commits = append(commits, osTreeCommit{
checksum: p.OSTreeParent.Checksum,
url: p.OSTreeParent.URL,
})
}
return commits
}
func (p *OS) getPackageSpecs() []rpmmd.PackageSpec {
return p.packageSpecs
}
func (p *OS) serializeStart(packages []rpmmd.PackageSpec) {
if len(p.packageSpecs) > 0 {
panic("double call to serializeStart()")
}
p.packageSpecs = packages
if p.KernelName != "" {
p.kernelVer = rpmmd.GetVerStrFromPackageSpecListPanic(p.packageSpecs, p.KernelName)
}
}
func (p *OS) serializeEnd() {
if len(p.packageSpecs) == 0 {
panic("serializeEnd() call when serialization not in progress")
}
p.kernelVer = ""
p.packageSpecs = nil
}
func (p *OS) serialize() osbuild.Pipeline {
if len(p.packageSpecs) == 0 {
panic("serialization not started")
}
pipeline := p.Base.serialize()
if p.OSTreeRef != "" && p.OSTreeParent != nil {
pipeline.AddStage(osbuild.NewOSTreePasswdStage("org.osbuild.source", p.OSTreeParent.Checksum))
}
rpmOptions := osbuild.NewRPMStageOptions(p.repos)
if p.ExcludeDocs {
if rpmOptions.Exclude == nil {
rpmOptions.Exclude = &osbuild.Exclude{}
}
rpmOptions.Exclude.Docs = true
}
rpmOptions.GPGKeysFromTree = p.GPGKeyFiles
if p.OSTreeRef != "" {
rpmOptions.OSTreeBooted = common.BoolToPtr(true)
rpmOptions.DBPath = "/usr/share/rpm"
}
pipeline.AddStage(osbuild.NewRPMStage(rpmOptions, osbuild.NewRpmStageSourceFilesInputs(p.packageSpecs)))
// If the /boot is on a separate partition, the prefix for the BLS stage must be ""
if p.PartitionTable == nil || p.PartitionTable.FindMountable("/boot") == nil {
pipeline.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{}))
} else {
pipeline.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{Prefix: common.StringToPtr("")}))
}
pipeline.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: p.Language}))
if p.Keyboard != nil {
pipeline.AddStage(osbuild.NewKeymapStage(&osbuild.KeymapStageOptions{Keymap: *p.Keyboard}))
}
pipeline.AddStage(osbuild.NewHostnameStage(&osbuild.HostnameStageOptions{Hostname: p.Hostname}))
pipeline.AddStage(osbuild.NewTimezoneStage(&osbuild.TimezoneStageOptions{Zone: p.Timezone}))
if len(p.NTPServers) > 0 {
pipeline.AddStage(osbuild.NewChronyStage(&osbuild.ChronyStageOptions{Timeservers: p.NTPServers}))
}
if len(p.Groups) > 0 {
pipeline.AddStage(osbuild.GenGroupsStage(p.Groups))
}
if len(p.Users) > 0 {
if p.OSTreeRef != "" {
// for ostree, writing the key during user creation is
// redundant and can cause issues so create users without keys
// and write them on first boot
usersStageSansKeys, err := osbuild.GenUsersStage(p.Users, true)
if err != nil {
// TODO: move encryption into weldr
panic("password encryption failed")
}
pipeline.AddStage(usersStageSansKeys)
pipeline.AddStage(osbuild.NewFirstBootStage(usersFirstBootOptions(p.Users)))
} else {
usersStage, err := osbuild.GenUsersStage(p.Users, false)
if err != nil {
// TODO: move encryption into weldr
panic("password encryption failed")
}
pipeline.AddStage(usersStage)
}
}
enabledServices := []string{}
disabledServices := []string{}
enabledServices = append(enabledServices, p.EnabledServices...)
disabledServices = append(disabledServices, p.DisabledServices...)
if p.Environment != nil {
enabledServices = append(enabledServices, p.Environment.GetServices()...)
}
if p.Workload != nil {
enabledServices = append(enabledServices, p.Workload.GetServices()...)
disabledServices = append(disabledServices, p.Workload.GetDisabledServices()...)
}
if len(enabledServices) != 0 ||
len(disabledServices) != 0 || p.DefaultTarget != "" {
pipeline.AddStage(osbuild.NewSystemdStage(&osbuild.SystemdStageOptions{
EnabledServices: enabledServices,
DisabledServices: disabledServices,
DefaultTarget: p.DefaultTarget,
}))
}
if p.Firewall != nil {
options := osbuild.FirewallStageOptions{
Ports: p.Firewall.Ports,
}
if p.Firewall.Services != nil {
options.EnabledServices = p.Firewall.Services.Enabled
options.DisabledServices = p.Firewall.Services.Disabled
}
pipeline.AddStage(osbuild.NewFirewallStage(&options))
}
for _, sysconfigConfig := range p.Sysconfig {
pipeline.AddStage(osbuild.NewSysconfigStage(sysconfigConfig))
}
for _, systemdLogindConfig := range p.SystemdLogind {
pipeline.AddStage(osbuild.NewSystemdLogindStage(systemdLogindConfig))
}
for _, cloudInitConfig := range p.CloudInit {
pipeline.AddStage(osbuild.NewCloudInitStage(cloudInitConfig))
}
for _, modprobeConfig := range p.Modprobe {
pipeline.AddStage(osbuild.NewModprobeStage(modprobeConfig))
}
for _, dracutConfConfig := range p.DracutConf {
pipeline.AddStage(osbuild.NewDracutConfStage(dracutConfConfig))
}
for _, systemdUnitConfig := range p.SystemdUnit {
pipeline.AddStage(osbuild.NewSystemdUnitStage(systemdUnitConfig))
}
if p.Authselect != nil {
pipeline.AddStage(osbuild.NewAuthselectStage(p.Authselect))
}
if p.SELinuxConfig != nil {
pipeline.AddStage(osbuild.NewSELinuxConfigStage(p.SELinuxConfig))
}
if p.Tuned != nil {
pipeline.AddStage(osbuild.NewTunedStage(p.Tuned))
}
for _, tmpfilesdConfig := range p.Tmpfilesd {
pipeline.AddStage(osbuild.NewTmpfilesdStage(tmpfilesdConfig))
}
for _, pamLimitsConfConfig := range p.PamLimitsConf {
pipeline.AddStage(osbuild.NewPamLimitsConfStage(pamLimitsConfConfig))
}
for _, sysctldConfig := range p.Sysctld {
pipeline.AddStage(osbuild.NewSysctldStage(sysctldConfig))
}
for _, dnfConfig := range p.DNFConfig {
pipeline.AddStage(osbuild.NewDNFConfigStage(dnfConfig))
}
if p.SshdConfig != nil {
pipeline.AddStage((osbuild.NewSshdConfigStage(p.SshdConfig)))
}
if p.AuthConfig != nil {
pipeline.AddStage(osbuild.NewAuthconfigStage(p.AuthConfig))
}
if p.PwQuality != nil {
pipeline.AddStage(osbuild.NewPwqualityConfStage(p.PwQuality))
}
if pt := p.PartitionTable; pt != nil {
kernelOptions := osbuild.GenImageKernelOptions(p.PartitionTable)
kernelOptions = append(kernelOptions, p.KernelOptionsAppend...)
pipeline = prependKernelCmdlineStage(pipeline, strings.Join(kernelOptions, " "), pt)
pipeline.AddStage(osbuild.NewFSTabStage(osbuild.NewFSTabStageOptions(pt)))
var bootloader *osbuild.Stage
switch p.platform.GetArch() {
case platform.ARCH_S390X:
bootloader = osbuild.NewZiplStage(new(osbuild.ZiplStageOptions))
default:
options := osbuild.NewGrub2StageOptionsUnified(pt,
p.kernelVer,
p.platform.GetUEFIVendor() != "",
p.platform.GetBIOSPlatform(),
p.platform.GetUEFIVendor(), false)
if cfg := p.Grub2Config; cfg != nil {
// TODO: don't store Grub2Config in OSPipeline, making the overrides unnecessary
// grub2.Config.Default is owned and set by `NewGrub2StageOptionsUnified`
// and thus we need to preserve it
if options.Config != nil {
cfg.Default = options.Config.Default
}
options.Config = cfg
}
bootloader = osbuild.NewGRUB2Stage(options)
}
pipeline.AddStage(bootloader)
}
if p.OpenSCAPConfig != nil {
pipeline.AddStage(osbuild.NewOscapRemediationStage(p.OpenSCAPConfig))
}
if p.SElinux != "" {
pipeline.AddStage(osbuild.NewSELinuxStage(&osbuild.SELinuxStageOptions{
FileContexts: fmt.Sprintf("etc/selinux/%s/contexts/files/file_contexts", p.SElinux),
}))
}
if p.OSTreeRef != "" {
pipeline.AddStage(osbuild.NewOSTreePrepTreeStage(&osbuild.OSTreePrepTreeStageOptions{
EtcGroupMembers: []string{
// NOTE: We may want to make this configurable.
"wheel", "docker",
},
}))
}
return pipeline
}
func prependKernelCmdlineStage(pipeline osbuild.Pipeline, kernelOptions string, pt *disk.PartitionTable) osbuild.Pipeline {
rootFs := pt.FindMountable("/")
if rootFs == nil {
panic("root filesystem must be defined for kernel-cmdline stage, this is a programming error")
}
rootFsUUID := rootFs.GetFSSpec().UUID
kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootFsUUID, kernelOptions))
pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...)
return pipeline
}
func usersFirstBootOptions(users []users.User) *osbuild.FirstBootStageOptions {
cmds := make([]string, 0, 3*len(users)+2)
// workaround for creating authorized_keys file for user
// need to special case the root user, which has its home in a different place
varhome := filepath.Join("/var", "home")
roothome := filepath.Join("/var", "roothome")
for _, user := range users {
if user.Key != nil {
var home string
if user.Name == "root" {
home = roothome
} else {
home = filepath.Join(varhome, user.Name)
}
sshdir := filepath.Join(home, ".ssh")
cmds = append(cmds, fmt.Sprintf("mkdir -p %s", sshdir))
cmds = append(cmds, fmt.Sprintf("sh -c 'echo %q >> %q'", *user.Key, filepath.Join(sshdir, "authorized_keys")))
cmds = append(cmds, fmt.Sprintf("chown %s:%s -Rc %s", user.Name, user.Name, sshdir))
}
}
cmds = append(cmds, fmt.Sprintf("restorecon -rvF %s", varhome))
cmds = append(cmds, fmt.Sprintf("restorecon -rvF %s", roothome))
options := &osbuild.FirstBootStageOptions{
Commands: cmds,
WaitForNetwork: false,
}
return options
}
func (p *OS) GetPlatform() platform.Platform {
return p.platform
}