From 2f4dcdac6fe09417be7ec79c50506638bb4c3677 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 12 Mar 2024 10:05:53 +0100 Subject: [PATCH] image,manifest: add support for user customization (well, not really) This adds support for being able to add user customization. In practise we can only handle adding root user key(s) for now until we have more discussion about how to support adding users in a bootc supported way. This support for keys is essential to allow testing the images without play gustfish or similar tricks (which is hard on a bootc deploy because bootc will bind mount the deploy `etc` over the `sysroot/etc` on first boot so anything we do on the root of the disk will not work for /etc (/root/.authorized_keys might work actually maybe?). This also adds support for kernel-args to the bootc install-to-fs stage. --- pkg/image/bootc_disk.go | 12 ++++- pkg/manifest/raw_bootc.go | 24 +++++++-- pkg/manifest/raw_bootc_test.go | 50 ++++++++++++++++++- .../bootc_install_to_filesystem_stage.go | 12 ++++- .../bootc_install_to_filesystem_stage_test.go | 12 +++-- 5 files changed, 97 insertions(+), 13 deletions(-) diff --git a/pkg/image/bootc_disk.go b/pkg/image/bootc_disk.go index 44ce306ee2..2d9a8776f1 100644 --- a/pkg/image/bootc_disk.go +++ b/pkg/image/bootc_disk.go @@ -5,6 +5,7 @@ import ( "math/rand" "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/customizations/users" "github.com/osbuild/images/pkg/disk" "github.com/osbuild/images/pkg/manifest" "github.com/osbuild/images/pkg/osbuild" @@ -21,6 +22,12 @@ type BootcDiskImage struct { Filename string ContainerSource *container.SourceSpec + + // "Users" is a bit misleading as only root and its ssh key is supported + // right now because that is all that bootc gives us by default but that + // will most likely change over time. + // See https://github.com/containers/bootc/pull/267 + Users []users.User } func NewBootcDiskImage(container container.SourceSpec) *BootcDiskImage { @@ -42,10 +49,11 @@ func (img *BootcDiskImage) InstantiateManifestFromContainers(m *manifest.Manifes // this is signified by passing nil to the below pipelines. var hostPipeline manifest.Build - // XXX: no support for customization right now, at least /etc/fstab - // and very basic user (root only?) should be supported + // TODO: no support for customization right now but minimal support + // for root ssh keys is supported baseImage := manifest.NewRawBootcImage(buildPipeline, containers, img.Platform) baseImage.PartitionTable = img.PartitionTable + baseImage.Users = img.Users // In BIB, we export multiple images from the same pipeline so we use the // filename as the basename for each export and set the extensions based on diff --git a/pkg/manifest/raw_bootc.go b/pkg/manifest/raw_bootc.go index c16d056aa3..5491e706fb 100644 --- a/pkg/manifest/raw_bootc.go +++ b/pkg/manifest/raw_bootc.go @@ -5,6 +5,7 @@ import ( "github.com/osbuild/images/pkg/artifact" "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/customizations/users" "github.com/osbuild/images/pkg/disk" "github.com/osbuild/images/pkg/osbuild" "github.com/osbuild/images/pkg/ostree" @@ -27,6 +28,12 @@ type RawBootcImage struct { // tree, with `bootc install to-filesystem` we can only work // with the image itself PartitionTable *disk.PartitionTable + + // "Users" is a bit misleading as only root and its ssh key is supported + // right now because that is all that bootc gives us by default but that + // will most likely change over time. + // See https://github.com/containers/bootc/pull/267 + Users []users.User } func (p RawBootcImage) Filename() string { @@ -76,7 +83,14 @@ func (p *RawBootcImage) serialize() osbuild.Pipeline { pt := p.PartitionTable if pt == nil { - panic("no partition table in live image") + panic(fmt.Errorf("no partition table in live image")) + } + + if len(p.Users) > 1 { + panic(fmt.Errorf("raw bootc image only supports a single root key for user customization, got %v", p.Users)) + } + if len(p.Users) == 1 && p.Users[0].Name != "root" { + panic(fmt.Errorf("raw bootc image only supports the root user, got %v", p.Users)) } for _, stage := range osbuild.GenImagePrepareStages(pt, p.filename, osbuild.PTSfdisk) { @@ -84,7 +98,11 @@ func (p *RawBootcImage) serialize() osbuild.Pipeline { } if len(p.containerSpecs) != 1 { - panic(fmt.Sprintf("expected a single container input got %v", p.containerSpecs)) + panic(fmt.Errorf("expected a single container input got %v", p.containerSpecs)) + } + opts := &osbuild.BootcInstallToFilesystemOptions{} + if len(p.Users) == 1 && p.Users[0].Key != nil { + opts.RootSSHAuthorizedKeys = []string{*p.Users[0].Key} } inputs := osbuild.ContainerDeployInputs{ Images: osbuild.NewContainersInputForSingleSource(p.containerSpecs[0]), @@ -93,7 +111,7 @@ func (p *RawBootcImage) serialize() osbuild.Pipeline { if err != nil { panic(err) } - st, err := osbuild.NewBootcInstallToFilesystemStage(inputs, devices, mounts) + st, err := osbuild.NewBootcInstallToFilesystemStage(opts, inputs, devices, mounts) if err != nil { panic(err) } diff --git a/pkg/manifest/raw_bootc_test.go b/pkg/manifest/raw_bootc_test.go index b562359bb1..c69cd8f15a 100644 --- a/pkg/manifest/raw_bootc_test.go +++ b/pkg/manifest/raw_bootc_test.go @@ -1,14 +1,19 @@ package manifest_test import ( + "regexp" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/osbuild/images/internal/assertx" + "github.com/osbuild/images/internal/common" "github.com/osbuild/images/internal/testdisk" "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/customizations/users" "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/osbuild" "github.com/osbuild/images/pkg/runner" ) @@ -36,18 +41,23 @@ func TestNewRawBootcImage(t *testing.T) { assert.Equal(t, "disk.img", rawBootcPipeline.Filename()) } -func TestRawBootcImageSerializeHasInstallToFilesystem(t *testing.T) { +func TestRawBootcImageSerialize(t *testing.T) { mani := manifest.New() runner := &runner.Linux{} build := manifest.NewBuildFromContainer(&mani, runner, nil, nil) rawBootcPipeline := manifest.NewRawBootcImage(build, nil, nil) rawBootcPipeline.PartitionTable = testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi") + rawBootcPipeline.Users = []users.User{{Name: "root", Key: common.ToPtr("some-ssh-key")}} + rawBootcPipeline.SerializeStart(nil, []container.Spec{{Source: "foo"}}, nil) imagePipeline := rawBootcPipeline.Serialize() assert.Equal(t, "image", imagePipeline.Name) - require.NotNil(t, manifest.FindStage("org.osbuild.bootc.install-to-filesystem", imagePipeline.Stages)) + bootcInst := manifest.FindStage("org.osbuild.bootc.install-to-filesystem", imagePipeline.Stages) + require.NotNil(t, bootcInst) + opts := bootcInst.Options.(*osbuild.BootcInstallToFilesystemOptions) + assert.Equal(t, []string{"some-ssh-key"}, opts.RootSSHAuthorizedKeys) } func TestRawBootcImageSerializeMountsValidated(t *testing.T) { @@ -63,3 +73,39 @@ func TestRawBootcImageSerializeMountsValidated(t *testing.T) { rawBootcPipeline.Serialize() }) } + +func TestRawBootcImageSerializeValidatesUsers(t *testing.T) { + mani := manifest.New() + runner := &runner.Linux{} + build := manifest.NewBuildFromContainer(&mani, runner, nil, nil) + + rawBootcPipeline := manifest.NewRawBootcImage(build, nil, nil) + rawBootcPipeline.PartitionTable = testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi") + rawBootcPipeline.SerializeStart(nil, []container.Spec{{Source: "foo"}}, nil) + + for _, tc := range []struct { + users []users.User + expectedErr string + }{ + // good + {nil, ""}, + {[]users.User{{Name: "root"}}, ""}, + {[]users.User{{Name: "root", Key: common.ToPtr("some-key")}}, ""}, + // bad + {[]users.User{{Name: "foo"}}, + "raw bootc image only supports the root user, got.*"}, + {[]users.User{{Name: "root"}, {Name: "foo"}}, + "raw bootc image only supports a single root key for user customization, got.*"}, + } { + rawBootcPipeline.Users = tc.users + + if tc.expectedErr == "" { + rawBootcPipeline.Serialize() + } else { + expectedErr := regexp.MustCompile(tc.expectedErr) + assertx.PanicsWithErrorRegexp(t, expectedErr, func() { + rawBootcPipeline.Serialize() + }) + } + } +} diff --git a/pkg/osbuild/bootc_install_to_filesystem_stage.go b/pkg/osbuild/bootc_install_to_filesystem_stage.go index d3a4876d85..06be586016 100644 --- a/pkg/osbuild/bootc_install_to_filesystem_stage.go +++ b/pkg/osbuild/bootc_install_to_filesystem_stage.go @@ -4,6 +4,15 @@ import ( "fmt" ) +type BootcInstallToFilesystemOptions struct { + // options for --root-ssh-authorized-keys + RootSSHAuthorizedKeys []string `json:"root-ssh-authorized-keys,omitempty"` + // options for --karg + Kargs []string `json:"kernel-args,omitempty"` +} + +func (BootcInstallToFilesystemOptions) isStageOptions() {} + // NewBootcInstallToFilesystem creates a new stage for the // org.osbuild.bootc.install-to-filesystem stage. // @@ -12,7 +21,7 @@ import ( // bootc/bootupd find and install all required bootloader bits. // // The mounts input should be generated with GenBootupdDevicesMounts. -func NewBootcInstallToFilesystemStage(inputs ContainerDeployInputs, devices map[string]Device, mounts []Mount) (*Stage, error) { +func NewBootcInstallToFilesystemStage(options *BootcInstallToFilesystemOptions, inputs ContainerDeployInputs, devices map[string]Device, mounts []Mount) (*Stage, error) { if err := validateBootupdMounts(mounts); err != nil { return nil, err } @@ -23,6 +32,7 @@ func NewBootcInstallToFilesystemStage(inputs ContainerDeployInputs, devices map[ return &Stage{ Type: "org.osbuild.bootc.install-to-filesystem", + Options: options, Inputs: inputs, Devices: devices, Mounts: mounts, diff --git a/pkg/osbuild/bootc_install_to_filesystem_stage_test.go b/pkg/osbuild/bootc_install_to_filesystem_stage_test.go index 8c036d9104..3f6abf1a19 100644 --- a/pkg/osbuild/bootc_install_to_filesystem_stage_test.go +++ b/pkg/osbuild/bootc_install_to_filesystem_stage_test.go @@ -31,11 +31,12 @@ func TestBootcInstallToFilesystemStageNewHappy(t *testing.T) { expectedStage := &osbuild.Stage{ Type: "org.osbuild.bootc.install-to-filesystem", + Options: (*osbuild.BootcInstallToFilesystemOptions)(nil), Inputs: inputs, Devices: devices, Mounts: mounts, } - stage, err := osbuild.NewBootcInstallToFilesystemStage(inputs, devices, mounts) + stage, err := osbuild.NewBootcInstallToFilesystemStage(nil, inputs, devices, mounts) require.Nil(t, err) assert.Equal(t, stage, expectedStage) } @@ -45,7 +46,7 @@ func TestBootcInstallToFilesystemStageNewNoContainers(t *testing.T) { mounts := makeOsbuildMounts("/", "/boot", "/boot/efi") inputs := osbuild.ContainerDeployInputs{} - _, err := osbuild.NewBootcInstallToFilesystemStage(inputs, devices, mounts) + _, err := osbuild.NewBootcInstallToFilesystemStage(nil, inputs, devices, mounts) assert.EqualError(t, err, "expected exactly one container input but got: 0 (map[])") } @@ -61,7 +62,7 @@ func TestBootcInstallToFilesystemStageNewTwoContainers(t *testing.T) { }, } - _, err := osbuild.NewBootcInstallToFilesystemStage(inputs, devices, mounts) + _, err := osbuild.NewBootcInstallToFilesystemStage(nil, inputs, devices, mounts) assert.EqualError(t, err, "expected exactly one container input but got: 2 (map[1:{} 2:{}])") } @@ -70,7 +71,7 @@ func TestBootcInstallToFilesystemStageMissingMounts(t *testing.T) { mounts := makeOsbuildMounts("/") inputs := makeFakeContainerInputs() - stage, err := osbuild.NewBootcInstallToFilesystemStage(inputs, devices, mounts) + stage, err := osbuild.NewBootcInstallToFilesystemStage(nil, inputs, devices, mounts) // XXX: rename error assert.ErrorContains(t, err, "required mounts for bootupd stage [/boot /boot/efi] missing") require.Nil(t, stage) @@ -81,7 +82,7 @@ func TestBootcInstallToFilesystemStageJsonHappy(t *testing.T) { mounts := makeOsbuildMounts("/", "/boot", "/boot/efi") inputs := makeFakeContainerInputs() - stage, err := osbuild.NewBootcInstallToFilesystemStage(inputs, devices, mounts) + stage, err := osbuild.NewBootcInstallToFilesystemStage(nil, inputs, devices, mounts) require.Nil(t, err) stageJson, err := json.MarshalIndent(stage, "", " ") require.Nil(t, err) @@ -98,6 +99,7 @@ func TestBootcInstallToFilesystemStageJsonHappy(t *testing.T) { } } }, + "options": null, "devices": { "dev-for-/": { "type": "org.osbuild.loopback"