Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions data/distrodefs/bootc-generic/imagetypes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ image_types:
filename: "installer.iso"
boot_iso: true
image_func: "bootc_legacy_iso"
# Uncomment the following lines to use the bootc verb instead of ostreecontainer
# installer_config:
# bootc_install_verb: bootc
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is under anaconda-iso, should it be under bootc-installer instead?


# XXX: ideally we would use name_aliases but the loader lib
# does not not fully support this yet
Expand Down
5 changes: 5 additions & 0 deletions pkg/distro/bootc/bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,11 @@ func (t *BootcImageType) manifestForISO(bp *blueprint.Blueprint, options distro.
// see https://github.com/osbuild/bootc-image-builder/issues/733
img.InstallerCustomizations.ISORootfsType = manifest.SquashfsRootfs

installerConfig := t.ImageTypeYAML.InstallerConfig(t.arch.distro.id, t.arch.Name())
if installerConfig != nil && installerConfig.BootcInstallVerb != nil {
img.BootcInstallVerb = *installerConfig.BootcInstallVerb
}

installRootfsType, err := disk.NewFSType(t.arch.distro.defaultFs)
if err != nil {
return nil, nil, err
Expand Down
4 changes: 4 additions & 0 deletions pkg/distro/installer_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ type InstallerConfig struct {
LoraxTemplatePackage *string `yaml:"lorax_template_package"`
LoraxLogosPackage *string `yaml:"lorax_logos_package"`
LoraxReleasePackage *string `yaml:"lorax_release_package"`

// BootcInstallVerb controls which directive to use in kickstart files for bootc installer ISOs.
// Valid values are "ostreecontainer" (default) and "bootc"
BootcInstallVerb *string `yaml:"bootc_install_verb,omitempty"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not have to be a pointer, empty string is a good default.

}

// InheritFrom inherits unset values from the provided parent configuration and
Expand Down
5 changes: 5 additions & 0 deletions pkg/image/anaconda_container_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ type AnacondaContainerInstaller struct {
InitramfsPath string
// bootc installer cannot use /root as installer home
InstallerHome string

// BootcInstallVerb controls which directive to use in kickstart files.
// Valid values are "ostreecontainer" (default) and "bootc"
BootcInstallVerb string
}

func NewAnacondaContainerInstaller(platform platform.Platform, filename string, container container.SourceSpec, ref string) *AnacondaContainerInstaller {
Expand Down Expand Up @@ -131,6 +135,7 @@ func (img *AnacondaContainerInstaller) InstantiateManifestFromContainer(m *manif
isoTreePipeline.PartitionTable = efiBootPartitionTable(rng)
isoTreePipeline.Release = img.InstallerCustomizations.Release
isoTreePipeline.Kickstart = img.Kickstart
isoTreePipeline.BootcInstallVerb = img.BootcInstallVerb

isoTreePipeline.RootfsCompression = img.RootfsCompression
isoTreePipeline.RootfsType = img.InstallerCustomizations.ISORootfsType
Expand Down
70 changes: 46 additions & 24 deletions pkg/manifest/anaconda_installer_iso_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ type AnacondaInstallerISOTree struct {
SubscriptionPipeline *Subscription

InstallRootfsType disk.FSType

// BootcInstallVerb controls which directive to use in kickstart files.
// Valid values are "ostreecontainer" (default) and "bootc"
BootcInstallVerb string
}

func NewAnacondaInstallerISOTree(buildPipeline Build, anacondaPipeline *AnacondaInstaller, rootfsPipeline *ISORootfsImg, bootTreePipeline *EFIBootTree) *AnacondaInstallerISOTree {
Expand Down Expand Up @@ -662,31 +666,49 @@ func (p *AnacondaInstallerISOTree) bootcInstallerKickstartStages() ([]*osbuild.S

stages := make([]*osbuild.Stage, 0)

// do what we can in our kickstart stage
kickstartOptions, err := osbuild.NewKickstartStageOptionsWithOSTreeContainer(
p.Kickstart.Path,
p.Kickstart.Users,
p.Kickstart.Groups,
path.Join("/run/install/repo", p.PayloadPath),
"oci",
"",
"")
if err != nil {
return nil, fmt.Errorf("failed to create kickstart stage options: %w", err)
}
var kickstartOptions *osbuild.KickstartStageOptions
var err error

// Workaround for lack of --target-imgref in Anaconda, xref https://github.com/osbuild/images/issues/380
kickstartOptions.Post = append(kickstartOptions.Post, osbuild.PostOptions{
ErrorOnFail: true,
Commands: []string{
fmt.Sprintf("bootc switch --mutate-in-place --transport registry %s", p.containerSpec.LocalName),
"# used during automatic image testing as finished marker",
"if [ -c /dev/ttyS0 ]; then",
" # continue on errors here, because we used to omit --erroronfail",
` echo "Install finished" > /dev/ttyS0 || true`,
"fi",
},
})
switch p.BootcInstallVerb {
case "bootc":
sourceImgRef := fmt.Sprintf("oci:%s", path.Join("/run/install/repo", p.PayloadPath))
kickstartOptions, err = osbuild.NewKickstartStageOptionsWithBootc(
p.Kickstart.Path,
p.Kickstart.Users,
p.Kickstart.Groups,
sourceImgRef,
p.containerSpec.LocalName)
if err != nil {
return nil, fmt.Errorf("failed to create kickstart stage options: %w", err)
}
case "ostreecontainer", "":
kickstartOptions, err = osbuild.NewKickstartStageOptionsWithOSTreeContainer(
p.Kickstart.Path,
p.Kickstart.Users,
p.Kickstart.Groups,
path.Join("/run/install/repo", p.PayloadPath),
"oci",
"",
"")
if err != nil {
return nil, fmt.Errorf("failed to create kickstart stage options: %w", err)
}

// Workaround for lack of --target-imgref in Anaconda, xref https://github.com/osbuild/images/issues/380
kickstartOptions.Post = append(kickstartOptions.Post, osbuild.PostOptions{
ErrorOnFail: true,
Commands: []string{
fmt.Sprintf("bootc switch --mutate-in-place --transport registry %s", p.containerSpec.LocalName),
"# used during automatic image testing as finished marker",
"if [ -c /dev/ttyS0 ]; then",
" # continue on errors here, because we used to omit --erroronfail",
` echo "Install finished" > /dev/ttyS0 || true`,
"fi",
},
})
default:
return nil, fmt.Errorf("invalid bootcInstallVerb: %s", p.BootcInstallVerb)
}

// kickstart.New() already validates the options but they may have been
// modified since then, so validate them before we create the stages
Expand Down
70 changes: 70 additions & 0 deletions pkg/manifest/anaconda_installer_iso_tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -963,3 +963,73 @@ func TestAnacondaISOTreeSerializeInstallRootfsType(t *testing.T) {
assert.Contains(t, inlineData[0], tc.expected)
}
}

func containerBootcSwitchInPost(ksOptions *osbuild.KickstartStageOptions) bool {
for _, post := range ksOptions.Post {
for _, cmd := range post.Commands {
if strings.Contains(cmd, "bootc switch") {
return true
}
}
}
return false
}

func TestAnacondaISOTreeBootcInstallVerb(t *testing.T) {
containerPayload := makeFakeContainerPayload()

t.Run("bootc-install-verb-bootc", func(t *testing.T) {
pipeline := newTestAnacondaISOTree()
pipeline.Kickstart = &kickstart.Options{Path: testKsPath}
pipeline.BootcInstallVerb = "bootc"
pipeline.PayloadPath = "/container"
sp, err := manifest.SerializeWith(pipeline, manifest.Inputs{Containers: []container.Spec{containerPayload}})
assert.NoError(t, err)

ksOptions := getKickstartOptions(sp.Stages)
assert.NotNil(t, ksOptions.Bootc)
assert.Nil(t, ksOptions.OSTreeContainer)
assert.Equal(t, "oci:/run/install/repo/container", ksOptions.Bootc.SourceImgRef)
assert.Equal(t, containerPayload.LocalName, ksOptions.Bootc.TargetImgRef)
assert.False(t, containerBootcSwitchInPost(ksOptions), "bootc switch command should not be present when using bootc install verb")
})

t.Run("bootc-install-verb-ostreecontainer", func(t *testing.T) {
pipeline := newTestAnacondaISOTree()
pipeline.Kickstart = &kickstart.Options{Path: testKsPath}
pipeline.BootcInstallVerb = "ostreecontainer"
pipeline.PayloadPath = "/container"
sp, err := manifest.SerializeWith(pipeline, manifest.Inputs{Containers: []container.Spec{containerPayload}})
assert.NoError(t, err)

ksOptions := getKickstartOptions(sp.Stages)
assert.Nil(t, ksOptions.Bootc)
assert.NotNil(t, ksOptions.OSTreeContainer)
assert.Equal(t, "/run/install/repo/container", ksOptions.OSTreeContainer.URL)
assert.True(t, containerBootcSwitchInPost(ksOptions), "bootc switch command should be present when using ostreecontainer install verb")
})

t.Run("bootc-install-verb-empty-default", func(t *testing.T) {
// Empty string should default to ostreecontainer behavior
pipeline := newTestAnacondaISOTree()
pipeline.Kickstart = &kickstart.Options{Path: testKsPath}
pipeline.BootcInstallVerb = ""
pipeline.PayloadPath = "/container"
sp, err := manifest.SerializeWith(pipeline, manifest.Inputs{Containers: []container.Spec{containerPayload}})
assert.NoError(t, err)

ksOptions := getKickstartOptions(sp.Stages)
assert.Nil(t, ksOptions.Bootc)
assert.NotNil(t, ksOptions.OSTreeContainer)
assert.True(t, containerBootcSwitchInPost(ksOptions), "bootc switch command should be present when using empty install verb (default)")
})

t.Run("bootc-install-verb-invalid", func(t *testing.T) {
pipeline := newTestAnacondaISOTree()
pipeline.Kickstart = &kickstart.Options{Path: testKsPath}
pipeline.BootcInstallVerb = "invalid"
pipeline.PayloadPath = "/container"
_, err := manifest.SerializeWith(pipeline, manifest.Inputs{Containers: []container.Spec{containerPayload}})
assert.EqualError(t, err, "cannot create ostree container stages: cannot generate bootc installer kickstart stages: invalid bootcInstallVerb: invalid")
})
}
31 changes: 31 additions & 0 deletions pkg/osbuild/kickstart_stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type KickstartStageOptions struct {

OSTreeCommit *OSTreeCommitOptions `json:"ostree,omitempty"`
OSTreeContainer *OSTreeContainerOptions `json:"ostreecontainer,omitempty"`
Bootc *BootcOptions `json:"bootc,omitempty"`

LiveIMG *LiveIMGOptions `json:"liveimg,omitempty"`

Expand Down Expand Up @@ -67,6 +68,11 @@ type OSTreeContainerOptions struct {
SignatureVerification bool `json:"signatureverification"`
}

type BootcOptions struct {
SourceImgRef string `json:"source-imgref"`
TargetImgRef string `json:"target-imgref,omitempty"`
}

type RebootOptions struct {
Eject bool `json:"eject,omitempty"`
KExec bool `json:"kexec,omitempty"`
Expand Down Expand Up @@ -308,6 +314,31 @@ func NewKickstartStageOptionsWithOSTreeContainer(
return options, nil
}

func NewKickstartStageOptionsWithBootc(
path string,
userCustomizations []users.User,
groupCustomizations []users.Group,
sourceImgRef string,
targetImgRef string) (*KickstartStageOptions, error) {

options, err := NewKickstartStageOptions(path, userCustomizations, groupCustomizations)

if err != nil {
return nil, err
}

if sourceImgRef != "" {
bootcOptions := &BootcOptions{
SourceImgRef: sourceImgRef,
TargetImgRef: targetImgRef,
}

options.Bootc = bootcOptions
}

return options, nil
}

func NewKickstartStageOptionsWithLiveIMG(
path string,
userCustomizations []users.User,
Expand Down
117 changes: 117 additions & 0 deletions pkg/osbuild/kickstart_stage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,123 @@ func TestNewKicstartStageOptionsWithOSTreeContainer(t *testing.T) {
}
}

func TestNewKickstartStageOptionsWithBootc(t *testing.T) {
type testCase struct {
path string
userCustomizations []users.User
sourceImgRef string
targetImgRef string

expOptions *osbuild.KickstartStageOptions
expErr string
}

testCases := map[string]testCase{
"empty": {
expErr: "org.osbuild.kickstart: kickstart path \"\" is invalid",
},
"user": {
path: "/osbuild-test.ks",
userCustomizations: []users.User{
{
Name: "fisher",
Shell: common.ToPtr("/bin/fish"),
},
},
expOptions: &osbuild.KickstartStageOptions{
Path: "/osbuild-test.ks",
Users: map[string]osbuild.UsersStageOptionsUser{
"fisher": {
Shell: common.ToPtr("/bin/fish"),
},
},
},
},
"bootc": {
path: "/osbuild-test.ks",
sourceImgRef: "docker://quay.io/fedora/fedora-bootc:latest",

expOptions: &osbuild.KickstartStageOptions{
Path: "/osbuild-test.ks",
Bootc: &osbuild.BootcOptions{
SourceImgRef: "docker://quay.io/fedora/fedora-bootc:latest",
TargetImgRef: "",
},
},
},
"bootc-with-target": {
path: "/osbuild-test.ks",
sourceImgRef: "oci:/run/install/repo/container",
targetImgRef: "docker://quay.io/fedora/fedora-bootc:latest",

expOptions: &osbuild.KickstartStageOptions{
Path: "/osbuild-test.ks",
Bootc: &osbuild.BootcOptions{
SourceImgRef: "oci:/run/install/repo/container",
TargetImgRef: "docker://quay.io/fedora/fedora-bootc:latest",
},
},
},
"user+bootc": {
path: "/osbuild-test.ks",
userCustomizations: []users.User{
{
Name: "fisher",
Shell: common.ToPtr("/bin/fish"),
},
},
sourceImgRef: "registry.example.org/my-image:latest",
targetImgRef: "registry.example.org/my-image:stable",

expOptions: &osbuild.KickstartStageOptions{
Path: "/osbuild-test.ks",
Users: map[string]osbuild.UsersStageOptionsUser{
"fisher": {
Shell: common.ToPtr("/bin/fish"),
},
},
Bootc: &osbuild.BootcOptions{
SourceImgRef: "registry.example.org/my-image:latest",
TargetImgRef: "registry.example.org/my-image:stable",
},
},
},
"internal-error": {
path: "/osbuild-test.ks",
// only need to check that an error from NewKickstartStageOptions
// is propagated
userCustomizations: []users.User{
{
Name: "root",
Shell: common.ToPtr("/bin/tsh"),
},
},
expErr: "org.osbuild.kickstart: unsupported options for user \"root\": shell",
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)

ksOptions, err := osbuild.NewKickstartStageOptionsWithBootc(
tc.path,
tc.userCustomizations,
nil,
tc.sourceImgRef,
tc.targetImgRef,
)
if tc.expErr != "" {
assert.EqualError(err, tc.expErr)
return
}

assert.NoError(err)
assert.Equal(tc.expOptions, ksOptions) // new file path must be the original kickstart path
})
}
}

func TestNewKicstartStageOptionsWithLiveIMG(t *testing.T) {
type testCase struct {
path string
Expand Down
Loading