Skip to content

Commit

Permalink
image,manifest: add support for user customization (well, not really)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mvo5 committed Mar 21, 2024
1 parent b79eabf commit 592b14d
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 13 deletions.
11 changes: 9 additions & 2 deletions pkg/image/bootc_disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand Down
22 changes: 19 additions & 3 deletions pkg/manifest/raw_bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -76,15 +81,26 @@ 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) {
pipeline.AddStage(stage)
}

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]),
Expand All @@ -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)
}
Expand Down
50 changes: 48 additions & 2 deletions pkg/manifest/raw_bootc_test.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down Expand Up @@ -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) {
Expand All @@ -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()
})
}
}
}
12 changes: 11 additions & 1 deletion pkg/osbuild/bootc_install_to_filesystem_stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand All @@ -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
}
Expand All @@ -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,
Expand Down
12 changes: 7 additions & 5 deletions pkg/osbuild/bootc_install_to_filesystem_stage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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[])")
}

Expand All @@ -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:{}])")
}

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -98,6 +99,7 @@ func TestBootcInstallToFilesystemStageJsonHappy(t *testing.T) {
}
}
},
"options": null,
"devices": {
"dev-for-/": {
"type": "org.osbuild.loopback"
Expand Down

0 comments on commit 592b14d

Please sign in to comment.