From aecbe5928aca8010ef1d9964e5041385b687e43a Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 10 Apr 2025 10:59:15 +0200 Subject: [PATCH] main: add new `--registrations` options This new flag allows to add a file with registration data. This is meant to eventually hold all sort of registrations like ansible or satelite but initially only contains the redhat subscription. Currently only JSON is supported. It looks like: ```json: { "redhat": { "subscription": { "activation_key": "ak_123", "organization": "org_123", "server_url": "server_url_123", "base_url": "base_url_123", "insights": true, "rhc": true, "proxy": "proxy_123" } } } ``` This is not part of the blueprint (today) because its more ephemeral than the things we usually put into the blueprint. This allows us to build images that are immediately registered. It also keeps our options open in the future. If we move to a new blueprint format where we support multiple blueprints and also ephemeral data like this the "registrations" flag just becomes an alias for "--blueprint". --- cmd/image-builder/export_test.go | 9 ++++ cmd/image-builder/main.go | 41 +++++++++++++++- cmd/image-builder/main_test.go | 82 ++++++++++++++++++++++++++++++++ cmd/image-builder/manifest.go | 11 ++++- 4 files changed, 140 insertions(+), 3 deletions(-) diff --git a/cmd/image-builder/export_test.go b/cmd/image-builder/export_test.go index 7cd28a5..550a59f 100644 --- a/cmd/image-builder/export_test.go +++ b/cmd/image-builder/export_test.go @@ -7,6 +7,7 @@ import ( "github.com/osbuild/images/pkg/cloud" "github.com/osbuild/images/pkg/cloud/awscloud" + "github.com/osbuild/images/pkg/manifestgen" "github.com/osbuild/images/pkg/reporegistry" ) @@ -71,3 +72,11 @@ func MockAwscloudNewUploader(f func(string, string, string, *awscloud.UploaderOp awscloudNewUploader = saved } } + +func MockManifestgenDepsolver(new manifestgen.DepsolveFunc) (restore func()) { + saved := manifestgenDepsolver + manifestgenDepsolver = new + return func() { + manifestgenDepsolver = saved + } +} diff --git a/cmd/image-builder/main.go b/cmd/image-builder/main.go index b00de87..1514281 100644 --- a/cmd/image-builder/main.go +++ b/cmd/image-builder/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/json" "errors" "fmt" "io" @@ -17,6 +18,7 @@ import ( "github.com/osbuild/image-builder-cli/pkg/progress" "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/customizations/subscription" "github.com/osbuild/images/pkg/imagefilter" "github.com/osbuild/images/pkg/osbuild" "github.com/osbuild/images/pkg/ostree" @@ -97,6 +99,37 @@ func ostreeImageOptions(cmd *cobra.Command) (*ostree.ImageOptions, error) { }, nil } +type registrations struct { + Redhat struct { + Subscription *subscription.ImageOptions `json:"subscription,omitempty"` + } `json:"redhat,omitempty"` +} + +func subscriptionImageOptions(cmd *cobra.Command) (*subscription.ImageOptions, error) { + regFilePath, err := cmd.Flags().GetString("registrations") + if err != nil { + return nil, err + } + if regFilePath == "" { + return nil, nil + } + + f, err := os.Open(regFilePath) + if err != nil { + return nil, fmt.Errorf("cannot open registrations file: %w", err) + } + defer f.Close() + + // XXX: support yaml eventually + var regs registrations + dec := json.NewDecoder(f) + dec.DisallowUnknownFields() + if err := dec.Decode(®s); err != nil { + return nil, fmt.Errorf("cannot parse registrations file: %w", err) + } + return regs.Redhat.Subscription, nil +} + type cmdManifestWrapperOptions struct { useBootstrapIfNeeded bool } @@ -160,6 +193,10 @@ func cmdManifestWrapper(pbar progress.ProgressBar, cmd *cobra.Command, args []st } customSeed = &seedFlagVal } + subscription, err := subscriptionImageOptions(cmd) + if err != nil { + return nil, err + } // no error check here as this is (deliberately) not defined on // "manifest" (if "images" learn to set the output filename in // manifests we would change this @@ -199,6 +236,7 @@ func cmdManifestWrapper(pbar progress.ProgressBar, cmd *cobra.Command, args []st RpmDownloader: rpmDownloader, WithSBOM: withSBOM, CustomSeed: customSeed, + Subscription: subscription, ForceRepos: forceRepos, } @@ -407,7 +445,7 @@ operating systems like Fedora, CentOS and RHEL with easy customizations support. RunE: cmdListImages, SilenceUsage: true, Args: cobra.NoArgs, - Aliases: []string{"list-images"}, + Aliases: []string{"list-images"}, } listCmd.Flags().StringArray("filter", nil, `Filter distributions by a specific criteria (e.g. "type:iot*")`) listCmd.Flags().String("format", "", "Output in a specific format (text, json)") @@ -430,6 +468,7 @@ operating systems like Fedora, CentOS and RHEL with easy customizations support. manifestCmd.Flags().String("ostree-url", "", `OSTREE url`) manifestCmd.Flags().Bool("use-librepo", true, `use librepo to download packages (disable if you use old versions of osbuild)`) manifestCmd.Flags().Bool("with-sbom", false, `export SPDX SBOM document`) + manifestCmd.Flags().String("registrations", "", `filename of a registrations file with e.g. subscription details`) rootCmd.AddCommand(manifestCmd) uploadCmd := &cobra.Command{ diff --git a/cmd/image-builder/main_test.go b/cmd/image-builder/main_test.go index bd01893..b3eb469 100644 --- a/cmd/image-builder/main_test.go +++ b/cmd/image-builder/main_test.go @@ -2,6 +2,7 @@ package main_test import ( "bytes" + "crypto/sha256" "encoding/json" "fmt" "io" @@ -18,6 +19,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/osbuild/images/pkg/distro" + "github.com/osbuild/images/pkg/dnfjson" + "github.com/osbuild/images/pkg/rpmmd" testrepos "github.com/osbuild/images/test/data/repositories" main "github.com/osbuild/image-builder-cli/cmd/image-builder" @@ -948,3 +952,81 @@ func TestBasenameFor(t *testing.T) { assert.Equal(t, tc.expected, main.BasenameFor(res, tc.basename)) } } + +// XXX: move into as manifestgen.FakeDepsolve +func fakeDepsolve(cacheDir string, depsolveWarningsOutput io.Writer, packageSets map[string][]rpmmd.PackageSet, d distro.Distro, arch string) (map[string]dnfjson.DepsolveResult, error) { + depsolvedSets := make(map[string]dnfjson.DepsolveResult) + + for name, pkgSetChain := range packageSets { + specSet := make([]rpmmd.PackageSpec, 0) + for _, pkgSet := range pkgSetChain { + include := pkgSet.Include + slices.Sort(include) + for _, pkgName := range include { + checksum := fmt.Sprintf("%x", sha256.Sum256([]byte(pkgName))) + spec := rpmmd.PackageSpec{ + Name: pkgName, + Checksum: "sha256:" + checksum, + } + specSet = append(specSet, spec) + } + + depsolvedSets[name] = dnfjson.DepsolveResult{ + Packages: specSet, + Repos: pkgSet.Repositories, + } + } + } + + return depsolvedSets, nil +} + +func TestManifestIntegrationWithRegistrations(t *testing.T) { + restore := main.MockManifestgenDepsolver(fakeDepsolve) + defer restore() + + restore = main.MockNewRepoRegistry(testrepos.New) + defer restore() + + // XXX: only "proxy_123", "server_url_123" are actually observable + // in the manifest(?) + fakeRegContent := `{ + "redhat": { + "subscription": { + "activation_key": "ak_123", + "organization": "org_123", + "server_url": "server_url_123", + "base_url": "base_url_123", + "insights": true, + "rhc": true, + "proxy": "proxy_123" + } + } +}` + fakeRegistrationsPath := filepath.Join(t.TempDir(), "registrations.json") + err := os.WriteFile(fakeRegistrationsPath, []byte(fakeRegContent), 0644) + assert.NoError(t, err) + + // XXX: fake the depsolving or we will need full support for + // subscripbed hosts in the tests + restore = main.MockOsArgs([]string{ + "manifest", + "qcow2", + "--arch=x86_64", + "--distro=rhel-9.6", + "--registrations", fakeRegistrationsPath, + }) + defer restore() + + var fakeStdout bytes.Buffer + restore = main.MockOsStdout(&fakeStdout) + defer restore() + + err = main.Run() + assert.NoError(t, err) + + // XXX: manifesttest really needs to grow more helpers + assert.Contains(t, fakeStdout.String(), `{"type":"org.osbuild.insights-client.config","options":{"proxy":"proxy_123"}}`) + assert.Contains(t, fakeStdout.String(), `"type":"org.osbuild.systemd.unit.create","options":{"filename":"osbuild-subscription-register.service"`) + assert.Contains(t, fakeStdout.String(), `server_url_123`) +} diff --git a/cmd/image-builder/manifest.go b/cmd/image-builder/manifest.go index 2508bd6..b7562ed 100644 --- a/cmd/image-builder/manifest.go +++ b/cmd/image-builder/manifest.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "github.com/osbuild/images/pkg/customizations/subscription" "github.com/osbuild/images/pkg/distro" "github.com/osbuild/images/pkg/imagefilter" "github.com/osbuild/images/pkg/manifestgen" @@ -22,6 +23,7 @@ type manifestOptions struct { OutputFilename string BlueprintPath string Ostree *ostree.ImageOptions + Subscription *subscription.ImageOptions RpmDownloader osbuild.RpmDownloader WithSBOM bool CustomSeed *int64 @@ -46,6 +48,9 @@ func sbomWriter(outputDir, filename string, content io.Reader) error { return nil } +// used in tests +var manifestgenDepsolver manifestgen.DepsolveFunc + func generateManifest(dataDir string, extraRepos []string, img *imagefilter.Result, output io.Writer, depsolveWarningsOutput io.Writer, opts *manifestOptions) error { repos, err := newRepoRegistry(dataDir, extraRepos) if err != nil { @@ -58,6 +63,7 @@ func generateManifest(dataDir string, extraRepos []string, img *imagefilter.Resu RpmDownloader: opts.RpmDownloader, UseBootstrapContainer: opts.UseBootstrapContainer, CustomSeed: opts.CustomSeed, + Depsolver: manifestgenDepsolver, } if opts.WithSBOM { outputDir := basenameFor(img, opts.OutputDir) @@ -84,9 +90,10 @@ func generateManifest(dataDir string, extraRepos []string, img *imagefilter.Resu return err } var imgOpts *distro.ImageOptions - if opts.Ostree != nil { + if opts.Ostree != nil || opts.Subscription != nil { imgOpts = &distro.ImageOptions{ - OSTree: opts.Ostree, + OSTree: opts.Ostree, + Subscription: opts.Subscription, } }