From 96136073a36530c434f925dcc0e1ae62d00517b6 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 22 Feb 2024 09:40:41 +0100 Subject: [PATCH 1/6] osbuild(bootc.install-to-fs): fix inputs type osbuild.ContainersInput->osbuild.ContainerDeployInputs --- .../bootc_install_to_filesystem_stage.go | 6 +-- .../bootc_install_to_filesystem_stage_test.go | 41 +++++++++++-------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/pkg/osbuild/bootc_install_to_filesystem_stage.go b/pkg/osbuild/bootc_install_to_filesystem_stage.go index 26402a2bd5..d3a4876d85 100644 --- a/pkg/osbuild/bootc_install_to_filesystem_stage.go +++ b/pkg/osbuild/bootc_install_to_filesystem_stage.go @@ -12,13 +12,13 @@ import ( // bootc/bootupd find and install all required bootloader bits. // // The mounts input should be generated with GenBootupdDevicesMounts. -func NewBootcInstallToFilesystemStage(inputs ContainersInput, devices map[string]Device, mounts []Mount) (*Stage, error) { +func NewBootcInstallToFilesystemStage(inputs ContainerDeployInputs, devices map[string]Device, mounts []Mount) (*Stage, error) { if err := validateBootupdMounts(mounts); err != nil { return nil, err } - if len(inputs.References) != 1 { - return nil, fmt.Errorf("expected exactly one container input but got: %v (%v)", len(inputs.References), inputs.References) + if len(inputs.Images.References) != 1 { + return nil, fmt.Errorf("expected exactly one container input but got: %v (%v)", len(inputs.Images.References), inputs.Images.References) } return &Stage{ diff --git a/pkg/osbuild/bootc_install_to_filesystem_stage_test.go b/pkg/osbuild/bootc_install_to_filesystem_stage_test.go index cbac772583..8c036d9104 100644 --- a/pkg/osbuild/bootc_install_to_filesystem_stage_test.go +++ b/pkg/osbuild/bootc_install_to_filesystem_stage_test.go @@ -11,14 +11,17 @@ import ( "github.com/osbuild/images/pkg/osbuild" ) -func makeFakeContainerInputs() osbuild.ContainersInput { - return osbuild.NewContainersInputForSources([]container.Spec{ - { - ImageID: "id-0", - Source: "registry.example.org/reg/img", - LocalName: "local-name", +func makeFakeContainerInputs() osbuild.ContainerDeployInputs { + return osbuild.ContainerDeployInputs{ + Images: osbuild.NewContainersInputForSources([]container.Spec{ + { + ImageID: "id-0", + Source: "registry.example.org/reg/img", + LocalName: "local-name", + }, }, - }) + ), + } } func TestBootcInstallToFilesystemStageNewHappy(t *testing.T) { @@ -40,7 +43,7 @@ func TestBootcInstallToFilesystemStageNewHappy(t *testing.T) { func TestBootcInstallToFilesystemStageNewNoContainers(t *testing.T) { devices := makeOsbuildDevices("dev-for-/", "dev-for-/boot", "dev-for-/boot/efi") mounts := makeOsbuildMounts("/", "/boot", "/boot/efi") - inputs := osbuild.ContainersInput{} + inputs := osbuild.ContainerDeployInputs{} _, err := osbuild.NewBootcInstallToFilesystemStage(inputs, devices, mounts) assert.EqualError(t, err, "expected exactly one container input but got: 0 (map[])") @@ -49,10 +52,12 @@ func TestBootcInstallToFilesystemStageNewNoContainers(t *testing.T) { func TestBootcInstallToFilesystemStageNewTwoContainers(t *testing.T) { devices := makeOsbuildDevices("dev-for-/", "dev-for-/boot", "dev-for-/boot/efi") mounts := makeOsbuildMounts("/", "/boot", "/boot/efi") - inputs := osbuild.ContainersInput{ - References: map[string]osbuild.ContainersInputSourceRef{ - "1": {}, - "2": {}, + inputs := osbuild.ContainerDeployInputs{ + Images: osbuild.ContainersInput{ + References: map[string]osbuild.ContainersInputSourceRef{ + "1": {}, + "2": {}, + }, }, } @@ -83,11 +88,13 @@ func TestBootcInstallToFilesystemStageJsonHappy(t *testing.T) { assert.Equal(t, string(stageJson), `{ "type": "org.osbuild.bootc.install-to-filesystem", "inputs": { - "type": "org.osbuild.containers", - "origin": "org.osbuild.source", - "references": { - "id-0": { - "name": "local-name" + "images": { + "type": "org.osbuild.containers", + "origin": "org.osbuild.source", + "references": { + "id-0": { + "name": "local-name" + } } } }, From 70d3cfa736754ce78c9a7fa31a710e540705e00b Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 22 Feb 2024 09:43:39 +0100 Subject: [PATCH 2/6] osbuild(container-deploy): ContainerDeployOptions.Exclude "omitempty" The osbuild stage will fail if we pass `None` here. --- pkg/osbuild/container_deploy_stage.go | 2 +- pkg/osbuild/container_deploy_stage_test.go | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/osbuild/container_deploy_stage.go b/pkg/osbuild/container_deploy_stage.go index 55d510b921..be7606b2f9 100644 --- a/pkg/osbuild/container_deploy_stage.go +++ b/pkg/osbuild/container_deploy_stage.go @@ -9,7 +9,7 @@ type ContainerDeployInputs struct { func (ContainerDeployInputs) isStageInputs() {} type ContainerDeployOptions struct { - Exclude []string `json:"exclude"` + Exclude []string `json:"exclude,omitempty"` } func (ContainerDeployOptions) isStageOptions() {} diff --git a/pkg/osbuild/container_deploy_stage_test.go b/pkg/osbuild/container_deploy_stage_test.go index 08af04ad93..3dc0a0d1f9 100644 --- a/pkg/osbuild/container_deploy_stage_test.go +++ b/pkg/osbuild/container_deploy_stage_test.go @@ -70,6 +70,14 @@ func TestContainersDeployStageOptionsJson(t *testing.T) { assert.Equal(t, string(json), expectedJson) } +func TestContainersDeployStageEmptyOptionsJson(t *testing.T) { + expectedJson := `{}` + cdi := osbuild.ContainerDeployOptions{} + json, err := json.MarshalIndent(cdi, "", " ") + require.Nil(t, err) + assert.Equal(t, string(json), expectedJson) +} + func TestContainersDeployStageInputsValidate(t *testing.T) { type testCase struct { inputs osbuild.ContainerDeployInputs From 6ae0ec210c9bbf0956f8457ce0bc608bdd30102c Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 22 Feb 2024 10:13:03 +0100 Subject: [PATCH 3/6] manifest: add new RawBootcImage type This image type is distinct from the RawOSTreeImage because the way `bootc instal to-filesystem` works is quite different from how our existing ostree deployments work. --- pkg/image/bootc_disk.go | 31 ++++--- pkg/image/bootc_disk_test.go | 49 ++++------- pkg/manifest/export_test.go | 3 + pkg/manifest/pipeline.go | 2 +- pkg/manifest/raw_bootc.go | 120 ++++++++++++++++++++++++++ pkg/manifest/raw_bootc_export_test.go | 20 +++++ pkg/manifest/raw_bootc_test.go | 65 ++++++++++++++ 7 files changed, 244 insertions(+), 46 deletions(-) create mode 100644 pkg/manifest/export_test.go create mode 100644 pkg/manifest/raw_bootc.go create mode 100644 pkg/manifest/raw_bootc_export_test.go create mode 100644 pkg/manifest/raw_bootc_test.go diff --git a/pkg/image/bootc_disk.go b/pkg/image/bootc_disk.go index 08cf81571f..44ce306ee2 100644 --- a/pkg/image/bootc_disk.go +++ b/pkg/image/bootc_disk.go @@ -5,26 +5,28 @@ import ( "math/rand" "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/disk" "github.com/osbuild/images/pkg/manifest" "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/platform" "github.com/osbuild/images/pkg/runner" ) type BootcDiskImage struct { - *OSTreeDiskImage + Base + + Platform platform.Platform + PartitionTable *disk.PartitionTable + + Filename string + + ContainerSource *container.SourceSpec } func NewBootcDiskImage(container container.SourceSpec) *BootcDiskImage { - // XXX: hardcoded for now - ref := "ostree/1/1/0" - return &BootcDiskImage{ - &OSTreeDiskImage{ - Base: NewBase("bootc-raw-image"), - ContainerSource: &container, - Ref: ref, - OSName: "default", - }, + Base: NewBase("bootc-raw-image"), + ContainerSource: &container, } } @@ -40,14 +42,15 @@ func (img *BootcDiskImage) InstantiateManifestFromContainers(m *manifest.Manifes // this is signified by passing nil to the below pipelines. var hostPipeline manifest.Build - opts := &baseRawOstreeImageOpts{useBootupd: true} - - fileBasename := img.Filename + // XXX: no support for customization right now, at least /etc/fstab + // and very basic user (root only?) should be supported + baseImage := manifest.NewRawBootcImage(buildPipeline, containers, img.Platform) + baseImage.PartitionTable = img.PartitionTable // 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 // each file format. - baseImage := baseRawOstreeImage(img.OSTreeDiskImage, buildPipeline, opts) + fileBasename := img.Filename baseImage.SetFilename(fmt.Sprintf("%s.raw", fileBasename)) qcow2Pipeline := manifest.NewQCOW2(hostPipeline, baseImage) diff --git a/pkg/image/bootc_disk_test.go b/pkg/image/bootc_disk_test.go index 9d2bc0903e..92c8a33e68 100644 --- a/pkg/image/bootc_disk_test.go +++ b/pkg/image/bootc_disk_test.go @@ -25,7 +25,7 @@ func TestBootcDiskImageNew(t *testing.T) { img := image.NewBootcDiskImage(containerSource) require.NotNil(t, img) - assert.Equal(t, img.OSTreeDiskImage.Base.Name(), "bootc-raw-image") + assert.Equal(t, img.Base.Name(), "bootc-raw-image") } func makeFakeDigest(t *testing.T) string { @@ -63,6 +63,7 @@ func makeBootcDiskImageOsbuildManifest(t *testing.T, opts *bootcDiskImageTestOpt containers := []container.SourceSpec{containerSource} img := image.NewBootcDiskImage(containerSource) + img.Filename = "fake-disk" require.NotNil(t, img) img.Platform = makeFakePlatform(opts) img.PartitionTable = testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi") @@ -73,8 +74,8 @@ func makeBootcDiskImageOsbuildManifest(t *testing.T, opts *bootcDiskImageTestOpt require.Nil(t, err) fakeSourceSpecs := map[string][]container.Spec{ - "build": []container.Spec{{Source: "some-src", Digest: makeFakeDigest(t), ImageID: makeFakeDigest(t)}}, - "ostree-deployment": []container.Spec{{Source: "other-src", Digest: makeFakeDigest(t), ImageID: makeFakeDigest(t)}}, + "build": []container.Spec{{Source: "some-src", Digest: makeFakeDigest(t), ImageID: makeFakeDigest(t)}}, + "image": []container.Spec{{Source: "other-src", Digest: makeFakeDigest(t), ImageID: makeFakeDigest(t)}}, } osbuildManifest, err := m.Serialize(nil, fakeSourceSpecs, nil) @@ -127,39 +128,25 @@ func TestBootcDiskImageInstantiateVmdk(t *testing.T) { require.NotNil(t, pipeline) } -func TestBootcDiskImageUsesBootupd(t *testing.T) { +func TestBootcDiskImageUsesBootcInstallToFs(t *testing.T) { osbuildManifest := makeBootcDiskImageOsbuildManifest(t, nil) - // check that bootupd is part of the "image" pipeline + // check that bootc.install-to-filesystem is part of the "image" pipeline imagePipeline := findPipelineFromOsbuildManifest(t, osbuildManifest, "image") require.NotNil(t, imagePipeline) - bootupdStage := findStageFromOsbuildPipeline(t, imagePipeline, "org.osbuild.bootupd") - require.NotNil(t, bootupdStage) - - // ensure that "grub2" is not part of the ostree pipeline - ostreeDeployPipeline := findPipelineFromOsbuildManifest(t, osbuildManifest, "ostree-deployment") - require.NotNil(t, ostreeDeployPipeline) - grubStage := findStageFromOsbuildPipeline(t, ostreeDeployPipeline, "org.osbuild.grub2") - require.Nil(t, grubStage) -} - -func TestBootcDiskImageBootupdBiosSupport(t *testing.T) { - for _, withBios := range []bool{false, true} { - osbuildManifest := makeBootcDiskImageOsbuildManifest(t, &bootcDiskImageTestOpts{BIOS: withBios, ImageFormat: platform.FORMAT_QCOW2}) - - imagePipeline := findPipelineFromOsbuildManifest(t, osbuildManifest, "image") - require.NotNil(t, imagePipeline) - bootupdStage := findStageFromOsbuildPipeline(t, imagePipeline, "org.osbuild.bootupd") - require.NotNil(t, bootupdStage) - - opts := bootupdStage["options"].(map[string]interface{}) - if withBios { - biosOpts := opts["bios"].(map[string]interface{}) - assert.Equal(t, biosOpts["device"], "disk") - } else { - require.Nil(t, opts["bios"]) - } + bootcStage := findStageFromOsbuildPipeline(t, imagePipeline, "org.osbuild.bootc.install-to-filesystem") + require.NotNil(t, bootcStage) + + // ensure loopback for the entire disk with partscan is used or install + // to-filesystem will fail + devicesDisk := bootcStage["devices"].(map[string]interface{})["disk"].(map[string]interface{}) + assert.Equal(t, "org.osbuild.loopback", devicesDisk["type"]) + devicesDiskOpts := devicesDisk["options"].(map[string]interface{}) + expectedDiskOpts := map[string]interface{}{ + "partscan": true, + "filename": "fake-disk.raw", } + assert.Equal(t, expectedDiskOpts, devicesDiskOpts) } func TestBootcDiskImageExportPipelines(t *testing.T) { diff --git a/pkg/manifest/export_test.go b/pkg/manifest/export_test.go new file mode 100644 index 0000000000..eff4489738 --- /dev/null +++ b/pkg/manifest/export_test.go @@ -0,0 +1,3 @@ +package manifest + +var FindStage = findStage diff --git a/pkg/manifest/pipeline.go b/pkg/manifest/pipeline.go index 4a92e6b561..ed1d67e79b 100644 --- a/pkg/manifest/pipeline.go +++ b/pkg/manifest/pipeline.go @@ -97,7 +97,7 @@ func (p Base) getCheckpoint() bool { } func (p *Base) Export() *artifact.Artifact { - panic("can't export pipeline") + panic("can't export pipeline directly from pipeline.Base") } func (p Base) getExport() bool { diff --git a/pkg/manifest/raw_bootc.go b/pkg/manifest/raw_bootc.go new file mode 100644 index 0000000000..c16d056aa3 --- /dev/null +++ b/pkg/manifest/raw_bootc.go @@ -0,0 +1,120 @@ +package manifest + +import ( + "fmt" + + "github.com/osbuild/images/pkg/artifact" + "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/disk" + "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/ostree" + "github.com/osbuild/images/pkg/platform" + "github.com/osbuild/images/pkg/rpmmd" +) + +// A RawBootcImage represents a raw bootc image file which can be booted in a +// hypervisor. +type RawBootcImage struct { + Base + + filename string + platform platform.Platform + + containers []container.SourceSpec + containerSpecs []container.Spec + + // customizations go here because there is no intermediate + // tree, with `bootc install to-filesystem` we can only work + // with the image itself + PartitionTable *disk.PartitionTable +} + +func (p RawBootcImage) Filename() string { + return p.filename +} + +func (p *RawBootcImage) SetFilename(filename string) { + p.filename = filename +} + +func NewRawBootcImage(buildPipeline Build, containers []container.SourceSpec, platform platform.Platform) *RawBootcImage { + p := &RawBootcImage{ + Base: NewBase("image", buildPipeline), + filename: "disk.img", + platform: platform, + + containers: containers, + } + buildPipeline.addDependent(p) + return p +} + +func (p *RawBootcImage) getContainerSources() []container.SourceSpec { + return p.containers +} + +func (p *RawBootcImage) getContainerSpecs() []container.Spec { + return p.containerSpecs +} + +func (p *RawBootcImage) serializeStart(_ []rpmmd.PackageSpec, containerSpecs []container.Spec, _ []ostree.CommitSpec) { + if len(p.containerSpecs) > 0 { + panic("double call to serializeStart()") + } + p.containerSpecs = containerSpecs +} + +func (p *RawBootcImage) serializeEnd() { + if len(p.containerSpecs) == 0 { + panic("serializeEnd() call when serialization not in progress") + } + p.containerSpecs = nil +} + +func (p *RawBootcImage) serialize() osbuild.Pipeline { + pipeline := p.Base.serialize() + + pt := p.PartitionTable + if pt == nil { + panic("no partition table in live image") + } + + for _, stage := range osbuild.GenImagePrepareStages(pt, p.filename, osbuild.PTSfdisk) { + pipeline.AddStage(stage) + } + + if len(p.containerSpecs) != 1 { + panic(fmt.Sprintf("expected a single container input got %v", p.containerSpecs)) + } + inputs := osbuild.ContainerDeployInputs{ + Images: osbuild.NewContainersInputForSingleSource(p.containerSpecs[0]), + } + devices, mounts, err := osbuild.GenBootupdDevicesMounts(p.filename, p.PartitionTable) + if err != nil { + panic(err) + } + st, err := osbuild.NewBootcInstallToFilesystemStage(inputs, devices, mounts) + if err != nil { + panic(err) + } + pipeline.AddStage(st) + + // XXX: there is no way right now to support any customizations, + // we cannot touch the filesystem after bootc installed it or + // we risk messing with it's selinux labels or future fsverity + // magic. Once we have a mechanism like --copy-etc from + // https://github.com/containers/bootc/pull/267 things should + // be a bit better + + for _, stage := range osbuild.GenImageFinishStages(pt, p.filename) { + pipeline.AddStage(stage) + } + + return pipeline +} + +// XXX: copied from raw.go +func (p *RawBootcImage) Export() *artifact.Artifact { + p.Base.export = true + return artifact.New(p.Name(), p.Filename(), nil) +} diff --git a/pkg/manifest/raw_bootc_export_test.go b/pkg/manifest/raw_bootc_export_test.go new file mode 100644 index 0000000000..450bedd34d --- /dev/null +++ b/pkg/manifest/raw_bootc_export_test.go @@ -0,0 +1,20 @@ +package manifest + +import ( + "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/ostree" + "github.com/osbuild/images/pkg/rpmmd" +) + +func (br *BuildrootFromContainer) Dependents() []Pipeline { + return br.dependents +} + +func (rbc *RawBootcImage) Serialize() osbuild.Pipeline { + return rbc.serialize() +} + +func (rbc *RawBootcImage) SerializeStart(a []rpmmd.PackageSpec, b []container.Spec, c []ostree.CommitSpec) { + rbc.serializeStart(a, b, c) +} diff --git a/pkg/manifest/raw_bootc_test.go b/pkg/manifest/raw_bootc_test.go new file mode 100644 index 0000000000..b562359bb1 --- /dev/null +++ b/pkg/manifest/raw_bootc_test.go @@ -0,0 +1,65 @@ +package manifest_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/osbuild/images/internal/testdisk" + "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/runner" +) + +func hasPipeline(haystack []manifest.Pipeline, needle manifest.Pipeline) bool { + for _, p := range haystack { + if p == needle { + return true + } + } + return false +} + +func TestNewRawBootcImage(t *testing.T) { + mani := manifest.New() + runner := &runner.Linux{} + buildIf := manifest.NewBuildFromContainer(&mani, runner, nil, nil) + build := buildIf.(*manifest.BuildrootFromContainer) + + rawBootcPipeline := manifest.NewRawBootcImage(build, nil, nil) + require.NotNil(t, rawBootcPipeline) + + assert.True(t, hasPipeline(build.Dependents(), rawBootcPipeline)) + + // disk.img is hardcoded for filename + assert.Equal(t, "disk.img", rawBootcPipeline.Filename()) +} + +func TestRawBootcImageSerializeHasInstallToFilesystem(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) + imagePipeline := rawBootcPipeline.Serialize() + assert.Equal(t, "image", imagePipeline.Name) + + require.NotNil(t, manifest.FindStage("org.osbuild.bootc.install-to-filesystem", imagePipeline.Stages)) +} + +func TestRawBootcImageSerializeMountsValidated(t *testing.T) { + mani := manifest.New() + runner := &runner.Linux{} + build := manifest.NewBuildFromContainer(&mani, runner, nil, nil) + + rawBootcPipeline := manifest.NewRawBootcImage(build, nil, nil) + // note that we create a partition table without /boot here + rawBootcPipeline.PartitionTable = testdisk.MakeFakePartitionTable("/", "/missing-boot") + rawBootcPipeline.SerializeStart(nil, []container.Spec{{Source: "foo"}}, nil) + assert.PanicsWithError(t, `required mounts for bootupd stage [/boot /boot/efi] missing`, func() { + rawBootcPipeline.Serialize() + }) +} From fbfd4bb674939d5daa9b607c400959979c1d948b Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 19 Mar 2024 11:23:16 +0100 Subject: [PATCH 4/6] manifest,osbuild: exclude /sysroot from selinux labeling This enusre that the buildroot container is setup without selinux warnings. --- pkg/manifest/build.go | 7 ++----- pkg/osbuild/selinux_stage.go | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/manifest/build.go b/pkg/manifest/build.go index acc7cc189a..ef82d47823 100644 --- a/pkg/manifest/build.go +++ b/pkg/manifest/build.go @@ -235,11 +235,7 @@ func (p *BuildrootFromContainer) serialize() osbuild.Pipeline { pipeline.Runner = p.runner.String() image := osbuild.NewContainersInputForSingleSource(p.containerSpecs[0]) - options := &osbuild.ContainerDeployOptions{ - Exclude: []string{"/sysroot"}, - } - - stage, err := osbuild.NewContainerDeployStage(image, options) + stage, err := osbuild.NewContainerDeployStage(image, &osbuild.ContainerDeployOptions{}) if err != nil { panic(err) } @@ -247,6 +243,7 @@ func (p *BuildrootFromContainer) serialize() osbuild.Pipeline { pipeline.AddStage(osbuild.NewSELinuxStage( &osbuild.SELinuxStageOptions{ FileContexts: "etc/selinux/targeted/contexts/files/file_contexts", + ExcludePaths: []string{"/sysroot"}, Labels: p.getSELinuxLabels(), }, )) diff --git a/pkg/osbuild/selinux_stage.go b/pkg/osbuild/selinux_stage.go index d5b0512cef..196f0fe0ce 100644 --- a/pkg/osbuild/selinux_stage.go +++ b/pkg/osbuild/selinux_stage.go @@ -6,6 +6,7 @@ package osbuild // the filesystem labels to apply to the image. type SELinuxStageOptions struct { FileContexts string `json:"file_contexts"` + ExcludePaths []string `json:"exclude_paths,omitempty"` Labels map[string]string `json:"labels,omitempty"` ForceAutorelabel *bool `json:"force_autorelabel,omitempty"` } From 2f4dcdac6fe09417be7ec79c50506638bb4c3677 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 12 Mar 2024 10:05:53 +0100 Subject: [PATCH 5/6] 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" From 1204d70075aef2fcb71cf28a2768feb244171f5f Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 22 Mar 2024 09:01:54 +0100 Subject: [PATCH 6/6] image,manifest: add support for `KernelOptionsAppend` in bootc This commit adds support to include KernelOptionsAppend to a BootcDiskImage. This is important for cloud support. --- pkg/image/bootc_disk.go | 4 ++++ pkg/image/bootc_disk_test.go | 12 +++++++++++- pkg/manifest/raw_bootc.go | 6 +++++- pkg/manifest/raw_bootc_test.go | 2 ++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pkg/image/bootc_disk.go b/pkg/image/bootc_disk.go index 2d9a8776f1..a2c27eb9ea 100644 --- a/pkg/image/bootc_disk.go +++ b/pkg/image/bootc_disk.go @@ -23,6 +23,9 @@ type BootcDiskImage struct { ContainerSource *container.SourceSpec + // Customizations + KernelOptionsAppend []string + // "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. @@ -54,6 +57,7 @@ func (img *BootcDiskImage) InstantiateManifestFromContainers(m *manifest.Manifes baseImage := manifest.NewRawBootcImage(buildPipeline, containers, img.Platform) baseImage.PartitionTable = img.PartitionTable baseImage.Users = img.Users + baseImage.KernelOptionsAppend = img.KernelOptionsAppend // 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/image/bootc_disk_test.go b/pkg/image/bootc_disk_test.go index 92c8a33e68..9301d17556 100644 --- a/pkg/image/bootc_disk_test.go +++ b/pkg/image/bootc_disk_test.go @@ -38,6 +38,8 @@ func makeFakeDigest(t *testing.T) string { type bootcDiskImageTestOpts struct { ImageFormat platform.ImageFormat BIOS bool + + KernelOptionsAppend []string } func makeFakePlatform(opts *bootcDiskImageTestOpts) platform.Platform { @@ -67,6 +69,7 @@ func makeBootcDiskImageOsbuildManifest(t *testing.T, opts *bootcDiskImageTestOpt require.NotNil(t, img) img.Platform = makeFakePlatform(opts) img.PartitionTable = testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi") + img.KernelOptionsAppend = opts.KernelOptionsAppend m := &manifest.Manifest{} runi := &runner.Fedora{} @@ -129,7 +132,10 @@ func TestBootcDiskImageInstantiateVmdk(t *testing.T) { } func TestBootcDiskImageUsesBootcInstallToFs(t *testing.T) { - osbuildManifest := makeBootcDiskImageOsbuildManifest(t, nil) + opts := &bootcDiskImageTestOpts{ + KernelOptionsAppend: []string{"karg1", "karg2"}, + } + osbuildManifest := makeBootcDiskImageOsbuildManifest(t, opts) // check that bootc.install-to-filesystem is part of the "image" pipeline imagePipeline := findPipelineFromOsbuildManifest(t, osbuildManifest, "image") @@ -147,6 +153,10 @@ func TestBootcDiskImageUsesBootcInstallToFs(t *testing.T) { "filename": "fake-disk.raw", } assert.Equal(t, expectedDiskOpts, devicesDiskOpts) + + // ensure options got passed + bootcOpts := bootcStage["options"].(map[string]interface{}) + assert.Equal(t, []interface{}{"karg1", "karg2"}, bootcOpts["kernel-args"]) } func TestBootcDiskImageExportPipelines(t *testing.T) { diff --git a/pkg/manifest/raw_bootc.go b/pkg/manifest/raw_bootc.go index 5491e706fb..dfe4c2da75 100644 --- a/pkg/manifest/raw_bootc.go +++ b/pkg/manifest/raw_bootc.go @@ -29,6 +29,8 @@ type RawBootcImage struct { // with the image itself PartitionTable *disk.PartitionTable + KernelOptionsAppend []string + // "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. @@ -100,7 +102,9 @@ func (p *RawBootcImage) serialize() osbuild.Pipeline { if len(p.containerSpecs) != 1 { panic(fmt.Errorf("expected a single container input got %v", p.containerSpecs)) } - opts := &osbuild.BootcInstallToFilesystemOptions{} + opts := &osbuild.BootcInstallToFilesystemOptions{ + Kargs: p.KernelOptionsAppend, + } if len(p.Users) == 1 && p.Users[0].Key != nil { opts.RootSSHAuthorizedKeys = []string{*p.Users[0].Key} } diff --git a/pkg/manifest/raw_bootc_test.go b/pkg/manifest/raw_bootc_test.go index c69cd8f15a..d144654ebb 100644 --- a/pkg/manifest/raw_bootc_test.go +++ b/pkg/manifest/raw_bootc_test.go @@ -49,6 +49,7 @@ func TestRawBootcImageSerialize(t *testing.T) { 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.KernelOptionsAppend = []string{"karg1", "karg2"} rawBootcPipeline.SerializeStart(nil, []container.Spec{{Source: "foo"}}, nil) imagePipeline := rawBootcPipeline.Serialize() @@ -58,6 +59,7 @@ func TestRawBootcImageSerialize(t *testing.T) { require.NotNil(t, bootcInst) opts := bootcInst.Options.(*osbuild.BootcInstallToFilesystemOptions) assert.Equal(t, []string{"some-ssh-key"}, opts.RootSSHAuthorizedKeys) + assert.Equal(t, []string{"karg1", "karg2"}, opts.Kargs) } func TestRawBootcImageSerializeMountsValidated(t *testing.T) {