diff --git a/pkg/image/bootc_disk.go b/pkg/image/bootc_disk.go index 44ce306ee..2c277a027 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" @@ -18,6 +19,11 @@ type BootcDiskImage struct { Platform platform.Platform PartitionTable *disk.PartitionTable + // This is a bit of a lie, only root and it's ssh key is supported + // today because that is all that bootc gives us by default but + // that will most likely change over time. + Users []users.User + Filename string ContainerSource *container.SourceSpec @@ -42,10 +48,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 c16d056aa..a51970160 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,10 @@ type RawBootcImage struct { // tree, with `bootc install to-filesystem` we can only work // with the image itself PartitionTable *disk.PartitionTable + + // This is a bit of a lie, only root and it's ssh key is supported + // today because that is all that bootc gives us by default. + Users []users.User } func (p RawBootcImage) Filename() string { @@ -76,7 +81,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 +96,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 +109,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 b562359bb..c69cd8f15 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 d3a4876d8..06be58601 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 8c036d910..3f6abf1a1 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"