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, } }