-
Notifications
You must be signed in to change notification settings - Fork 94
manifest/raw_bootc: support subscription registration on first boot (HMS-10897) #2528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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 | ||
|
|
@@ -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 { | ||
|
|
||
|
|
@@ -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 | ||
| 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/`. | ||
|
|
@@ -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()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd drop the |
||
| } | ||
| p.inlineData = append(p.inlineData, string(file.Data())) | ||
| } | ||
|
|
||
| return inlineData | ||
| return osbuild.GenFileNodesStages(files), nil | ||
| } | ||
|
|
||
| // XXX: copied from raw.go | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package manifest_test | |
| import ( | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
|
|
@@ -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" | ||
|
|
@@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
There was a problem hiding this comment.
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
subscriptionEnabledServicesand all this does is copy it.