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
22 changes: 22 additions & 0 deletions pkg/distro/generic/bootc_image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/osbuild/blueprint/pkg/blueprint"
"github.com/osbuild/image-builder/internal/common"
"github.com/osbuild/image-builder/pkg/customizations/subscription"
"github.com/osbuild/image-builder/pkg/datasizes"
"github.com/osbuild/image-builder/pkg/disk"
"github.com/osbuild/image-builder/pkg/disk/partition"
Expand Down Expand Up @@ -670,3 +671,24 @@ func TestGenPartitionTableFromOSInfo(t *testing.T) {
assert.NoError(t, err)
assert.Contains(t, string(manifestJson), "01010101-01011-01011-01011-01010101")
}

// Each bootc manifestFor* variant hand-copies customizations, so pin that the
// disk variant does not forget to pass Subscription through.
func TestManifestSubscriptionCustomization(t *testing.T) {
imgType := NewTestBootcImageType(t, "qcow2")
imgOpts := distro.ImageOptions{
Subscription: &subscription.ImageOptions{
Organization: "2040324",
ActivationKey: "my-secret-key",
},
}

mf, _, err := imgType.Manifest(&blueprint.Blueprint{}, imgOpts, nil, common.ToPtr(int64(0)))
assert.NoError(t, err)
manifestJson, err := mf.Serialize(nil, diskContainers, nil, nil, nil)
assert.NoError(t, err)

// assert on names, not stage types: mount units share them
assert.Contains(t, string(manifestJson), "osbuild-subscription-register.service")
assert.Contains(t, string(manifestJson), "/etc/osbuild-subscription-register.env")
}
1 change: 1 addition & 0 deletions pkg/distro/generic/bootc_imagetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ func (t *bootcImageType) manifestForDisk(bp *blueprint.Blueprint, options distro
img.Bootloader = bd.bootloader
img.UnifiedKernel = bd.unifiedKernel

img.OSCustomizations.Subscription = options.Subscription
img.OSCustomizations.Users = users.UsersFromBP(customizations.GetUsers())

groups, err := customizations.GetGroups()
Expand Down
74 changes: 67 additions & 7 deletions pkg/manifest/raw_bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type RawBootcImage struct {

// DiskCustomizations can influence things in the base OS tree
DiskCustomizations DiskCustomizations

inlineData []string
}

func (p RawBootcImage) Filename() string {
Expand Down Expand Up @@ -191,6 +193,8 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) {

postStages := []*osbuild.Stage{}

var subscriptionEnabledServices []string

fsCfgStages, err := filesystemConfigStages(pt, p.DiskCustomizations.MountConfiguration)
if err != nil {
return osbuild.Pipeline{}, err
Expand Down Expand Up @@ -261,6 +265,37 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) {
postStages = append(postStages, stages...)
}

if p.OSCustomizations.Subscription != nil {
subStage, subDirs, subFiles, subServices, err := subscriptionService(
*p.OSCustomizations.Subscription,
&subscriptionServiceOptions{
// ostree based: unit in /etc (not /usr commit content),
// insights on boot
InsightsOnBoot: true,
UnitPath: osbuild.EtcUnitPath,
// no-op for now: manifestForDisk never sets OSCustomizations.PermissiveRHC
PermissiveRHC: common.ValueOrEmpty(p.OSCustomizations.PermissiveRHC),
})
if err != nil {
return osbuild.Pipeline{}, err
}

subFileStages, err := p.genFileStagesAndRecordInlineData(subFiles)
if err != nil {
return osbuild.Pipeline{}, err
}
stages := []*osbuild.Stage{subStage}
stages = append(stages, osbuild.GenDirectoryNodesStages(subDirs)...)
stages = append(stages, subFileStages...)
for _, stage := range stages {
stage.Mounts = mounts
stage.Devices = devices
}
postStages = append(postStages, stages...)

subscriptionEnabledServices = subServices
}

// First create custom directories, because some of the custom files may depend on them
if len(p.OSCustomizations.Directories) > 0 {

Expand All @@ -273,14 +308,31 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) {
}

if len(p.OSCustomizations.Files) > 0 {
stages := osbuild.GenFileNodesStages(p.OSCustomizations.Files)
stages, err := p.genFileStagesAndRecordInlineData(p.OSCustomizations.Files)
if err != nil {
return osbuild.Pipeline{}, err
}
for _, stage := range stages {
stage.Mounts = mounts
stage.Devices = devices
}
postStages = append(postStages, stages...)
}

// single point where enabled services are collected, as in the OS
// pipeline; OSCustomizations.EnabledServices is not honoured yet
var enabledServices []string

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.

Why have 2 lists for enabled services? You have already populated subscriptionEnabledServices and all this does is copy it.

enabledServices = append(enabledServices, subscriptionEnabledServices...)

if len(enabledServices) > 0 {
systemdStage := osbuild.NewSystemdStage(&osbuild.SystemdStageOptions{
EnabledServices: enabledServices,
})
systemdStage.Mounts = mounts
systemdStage.Devices = devices
postStages = append(postStages, systemdStage)
}

// The ignition stamp must be created after bootc install, otherwise bootc will error out
// because the boot partition is not empty.
// That's why we have to pass `mount://boot/` and can't write to `tree://boot/`.
Expand Down Expand Up @@ -333,16 +385,24 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) {
return pipeline, nil
}

// XXX: duplicated from os.go
func (p *RawBootcImage) getInline() []string {
inlineData := []string{}
return p.inlineData
}

// inline data for custom files
for _, file := range p.OSCustomizations.Files {
inlineData = append(inlineData, string(file.Data()))
// genFileStagesAndRecordInlineData returns the stages that create the given
// files and records their content as inline data for getInline().
func (p *RawBootcImage) genFileStagesAndRecordInlineData(files []*fsnode.File) ([]*osbuild.Stage, error) {
for _, file := range files {
// files that come via an URI are not inline data, they
// would need to be added to the manifest sources via a
// fileRefs() implementation like the one in the OS pipeline
if file.URI() != "" {
return nil, fmt.Errorf("cannot create file %q from %q: files from an URI are not supported for bootc disk images yet", file.Path(), file.URI())

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.

I'd drop the yet, no need to speculate in error messages and get people's hopes up :)

}
p.inlineData = append(p.inlineData, string(file.Data()))
}

return inlineData
return osbuild.GenFileNodesStages(files), nil
}

// XXX: copied from raw.go
Expand Down
162 changes: 162 additions & 0 deletions pkg/manifest/raw_bootc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package manifest_test
import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -13,6 +14,7 @@ import (
"github.com/osbuild/image-builder/pkg/arch"
"github.com/osbuild/image-builder/pkg/container"
"github.com/osbuild/image-builder/pkg/customizations/fsnode"
"github.com/osbuild/image-builder/pkg/customizations/subscription"
"github.com/osbuild/image-builder/pkg/customizations/users"
"github.com/osbuild/image-builder/pkg/manifest"
"github.com/osbuild/image-builder/pkg/osbuild"
Expand Down Expand Up @@ -409,3 +411,163 @@ func TestRawBootcPXE(t *testing.T) {
assert.Contains(t, mkdirPaths, "/usr")
assert.Contains(t, mkdirPaths, "/proc")
}

func TestRawBootcImageSerializeSubscriptionManagerCommands(t *testing.T) {
rawBootcPipeline := makeFakeRawBootcPipeline()
rawBootcPipeline.OSCustomizations.Subscription = &subscription.ImageOptions{
Organization: "2040324",
ActivationKey: "my-secret-key",
ServerUrl: "subscription.rhsm.redhat.com",
BaseUrl: "http://cdn.redhat.com/",
}

pipeline, err := rawBootcPipeline.Serialize()
require.NoError(t, err)
CheckSystemdStageOptions(t, pipeline.Stages, []string{
"/usr/sbin/subscription-manager config --server.hostname 'subscription.rhsm.redhat.com'",
`/usr/sbin/subscription-manager register --org="${ORG_ID}" --activationkey="${ACTIVATION_KEY}" --baseurl 'http://cdn.redhat.com/'`,
})

// registration unit in /etc, not /usr (ostree commit content)
assert.Equal(t, osbuild.EtcUnitPath, registrationUnitPath(t, pipeline.Stages))
}

func TestRawBootcImageSerializeSubscriptionManagerInsightsCommands(t *testing.T) {
rawBootcPipeline := makeFakeRawBootcPipeline()
rawBootcPipeline.OSCustomizations.Subscription = &subscription.ImageOptions{
Organization: "2040324",
ActivationKey: "my-secret-key",
ServerUrl: "subscription.rhsm.redhat.com",
BaseUrl: "http://cdn.redhat.com/",
Insights: true,
}

pipeline, err := rawBootcPipeline.Serialize()
require.NoError(t, err)
CheckSystemdStageOptions(t, pipeline.Stages, []string{
"/usr/sbin/subscription-manager config --server.hostname 'subscription.rhsm.redhat.com'",
`/usr/sbin/subscription-manager register --org="${ORG_ID}" --activationkey="${ACTIVATION_KEY}" --baseurl 'http://cdn.redhat.com/'`,
"/usr/bin/insights-client --register",
})

// InsightsOnBoot also materializes the insights-client drop-in
mkdirPaths := collectMkdirPaths(pipeline.Stages)
assert.Contains(t, mkdirPaths, "/etc/systemd/system/insights-client-boot.service.d")
destinationPaths := collectCopyDestinationPaths(pipeline.Stages)
assert.Contains(t, destinationPaths, "tree:///etc/systemd/system/insights-client-boot.service.d/override.conf")
}

func TestRawBootcImageSerializeRhcInsightsCommands(t *testing.T) {
rawBootcPipeline := makeFakeRawBootcPipeline()
rawBootcPipeline.OSCustomizations.Subscription = &subscription.ImageOptions{
Organization: "2040324",
ActivationKey: "my-secret-key",
ServerUrl: "subscription.rhsm.redhat.com",
BaseUrl: "http://cdn.redhat.com/",
Insights: false,
Rhc: true,
}
rawBootcPipeline.OSCustomizations.PermissiveRHC = common.ToPtr(true)

pipeline, err := rawBootcPipeline.Serialize()
require.NoError(t, err)
CheckSystemdStageOptions(t, pipeline.Stages, []string{
"/usr/sbin/subscription-manager config --server.hostname 'subscription.rhsm.redhat.com'",
`/usr/bin/rhc connect --organization="${ORG_ID}" --activation-key="${ACTIVATION_KEY}"`,
"/usr/sbin/semanage permissive --add rhcd_t",
})
}

func TestRawBootcImageSerializeSubscriptionEnablesService(t *testing.T) {
rawBootcPipeline := makeFakeRawBootcPipeline()
rawBootcPipeline.OSCustomizations.Subscription = &subscription.ImageOptions{
Organization: "2040324",
ActivationKey: "my-secret-key",
}

pipeline, err := rawBootcPipeline.Serialize()
require.NoError(t, err)

stage := findStage("org.osbuild.systemd", pipeline.Stages)
require.NotNil(t, stage)
opts := stage.Options.(*osbuild.SystemdStageOptions)
assert.Equal(t, []string{"osbuild-subscription-register.service"}, opts.EnabledServices)
}

// Mirrors TestAddInlineOS: the env file must be both a copy destination and an
// inline source, and is written before the blueprint's own files.
func TestRawBootcImageSerializeSubscriptionEnvFile(t *testing.T) {
rawBootcPipeline := makeFakeRawBootcPipeline()

require := require.New(t)

rawBootcPipeline.OSCustomizations.Files = createTestFilesForPipeline()
rawBootcPipeline.OSCustomizations.Subscription = &subscription.ImageOptions{
Organization: "000",
ActivationKey: "111",
}

expectedPaths := []string{
"tree:///etc/osbuild-subscription-register.env", // from the subscription options
"tree:///etc/test/one", // directly from the OS customizations
"tree:///etc/test/two",
}

pipeline, err := rawBootcPipeline.Serialize()
require.NoError(err)

destinationPaths := collectCopyDestinationPaths(pipeline.Stages)

// The order is significant. Do not use ElementsMatch() or similar.
require.Equal(expectedPaths, destinationPaths)

expectedContents := []string{
"ORG_ID=000\nACTIVATION_KEY=111",
"test 1",
"test 2",
}

fileContents := manifest.GetInline(rawBootcPipeline)
// These are used to define the 'sources' part of the manifest, so the
// order doesn't matter
require.ElementsMatch(expectedContents, fileContents)
}

func TestRawBootcImageSerializeURIFilesError(t *testing.T) {
rawBootcPipeline := makeFakeRawBootcPipeline()

localFile := filepath.Join(t.TempDir(), "local-file")
require.NoError(t, os.WriteFile(localFile, []byte("some content"), 0644))
uriFile := common.Must(fsnode.NewFileForURI("/etc/test/from-uri", nil, nil, nil, localFile))
rawBootcPipeline.OSCustomizations.Files = []*fsnode.File{uriFile}

_, err := rawBootcPipeline.Serialize()
assert.EqualError(t, err, fmt.Sprintf(
"cannot create file %q from %q: files from an URI are not supported for bootc disk images yet",
"/etc/test/from-uri", localFile))
}

// registrationUnitPath returns the UnitPath of the registration unit, found by
// filename because mount units share the systemd.unit.create stage type.
func registrationUnitPath(t *testing.T, stages []*osbuild.Stage) osbuild.SystemdUnitPath {
t.Helper()
for _, s := range findStages("org.osbuild.systemd.unit.create", stages) {
opts := s.Options.(*osbuild.SystemdUnitCreateStageOptions)
if opts.Filename == "osbuild-subscription-register.service" {
return opts.UnitPath
}
}
require.Fail(t, "no osbuild-subscription-register.service unit.create stage found")
return ""
}

func collectMkdirPaths(stages []*osbuild.Stage) []string {

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.

May as well use this in the TestRawBootcPXE function, probably in a new commit just to keep things clean.

mkdirPaths := make([]string, 0)
for _, mkdirStage := range findStages("org.osbuild.mkdir", stages) {
mkdirStageOptions := mkdirStage.Options.(*osbuild.MkdirStageOptions)
for _, path := range mkdirStageOptions.Paths {
mkdirPaths = append(mkdirPaths, path.Path)
}
}
return mkdirPaths
}
Loading