package main_test import ( "bytes" "crypto/sha256" "encoding/json" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "slices" "strings" "testing" "github.com/spf13/cobra" "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" "github.com/osbuild/image-builder-cli/internal/manifesttest" "github.com/osbuild/image-builder-cli/internal/testutil" "github.com/osbuild/images/pkg/arch" ) func TestListImagesNoArguments(t *testing.T) { restore := main.MockNewRepoRegistry(testrepos.New) defer restore() for _, args := range [][]string{nil, []string{"--format=text"}} { restore = main.MockOsArgs(append([]string{"list"}, args...)) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err := main.Run() assert.NoError(t, err) // we expect at least this canary assert.Contains(t, fakeStdout.String(), "rhel-10.0 type:qcow2 arch:x86_64\n") // output is sorted, i.e. 8.8 comes before 8.10 assert.Regexp(t, `(?ms)rhel-8.8.*rhel-8.10`, fakeStdout.String()) } } func TestListImagesNoArgsOutputJSON(t *testing.T) { restore := main.MockNewRepoRegistry(testrepos.New) defer restore() restore = main.MockOsArgs([]string{"list", "--format=json"}) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err := main.Run() assert.NoError(t, err) // smoke test only, we expect valid json and at least the // distro/arch/image_type keys in the json var jo []map[string]interface{} err = json.Unmarshal(fakeStdout.Bytes(), &jo) assert.NoError(t, err) res := jo[0] for _, key := range []string{"distro", "arch", "image_type"} { assert.NotNil(t, res[key]) } } func TestListImagesFilteringSmoke(t *testing.T) { restore := main.MockNewRepoRegistry(testrepos.New) defer restore() restore = main.MockOsArgs([]string{"list", "--filter=centos*"}) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err := main.Run() assert.NoError(t, err) // we have centos assert.Contains(t, fakeStdout.String(), "centos-9 type:qcow2 arch:x86_64\n") // but not rhel assert.NotContains(t, fakeStdout.String(), "rhel") } func TestBadCmdErrorsNoExtraCobraNoise(t *testing.T) { var fakeStderr bytes.Buffer restore := main.MockOsStderr(&fakeStderr) defer restore() restore = main.MockOsArgs([]string{"bad-command"}) defer restore() err := main.Run() assert.EqualError(t, err, `unknown command "bad-command" for "image-builder"`) // no extra output from cobra assert.Equal(t, "", fakeStderr.String()) } func TestListImagesErrorsOnExtraArgs(t *testing.T) { restore := main.MockNewRepoRegistry(testrepos.New) defer restore() restore = main.MockOsArgs(append([]string{"list"}, "extra-arg")) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err := main.Run() assert.EqualError(t, err, `unknown command "extra-arg" for "image-builder list"`) } func hasDepsolveDnf() bool { // XXX: expose images/pkg/depsolve:findDepsolveDnf() _, err := os.Stat("/usr/libexec/osbuild-depsolve-dnf") return err == nil } var testBlueprint = ` [[containers]] source = "registry.gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/fedora-minimal" [[customizations.user]] name = "alice" [[customizations.disk.partitions]] type = "lvm" name = "mainvg" minsize = "20 GiB" ` func makeTestBlueprint(t *testing.T, testBlueprint string) string { tmpdir := t.TempDir() blueprintPath := filepath.Join(tmpdir, "blueprint.toml") err := os.WriteFile(blueprintPath, []byte(testBlueprint), 0644) assert.NoError(t, err) return blueprintPath } // XXX: move to pytest like bib maybe? func TestManifestIntegrationSmoke(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() for _, useLibrepo := range []bool{false, true} { t.Run(fmt.Sprintf("use-librepo: %v", useLibrepo), func(t *testing.T) { restore = main.MockOsArgs([]string{ "manifest", "qcow2", "--arch=x86_64", "--distro=centos-9", fmt.Sprintf("--blueprint=%s", makeTestBlueprint(t, testBlueprint)), fmt.Sprintf("--use-librepo=%v", useLibrepo), }) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err := main.Run() assert.NoError(t, err) pipelineNames, err := manifesttest.PipelineNamesFrom(fakeStdout.Bytes()) assert.NoError(t, err) assert.Contains(t, pipelineNames, "qcow2") // XXX: provide helpers in manifesttest to extract this in a nicer way assert.Contains(t, fakeStdout.String(), `{"type":"org.osbuild.users","options":{"users":{"alice":{}}}}`) assert.Contains(t, fakeStdout.String(), `"image":{"name":"registry.gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/fedora-minimal"`) assert.Equal(t, strings.Contains(fakeStdout.String(), "org.osbuild.librepo"), useLibrepo) }) } } func TestManifestIntegrationCrossArch(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() restore = main.MockOsArgs([]string{ "manifest", "tar", "--distro", "centos-9", "--arch", "s390x", }) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err := main.Run() assert.NoError(t, err) pipelineNames, err := manifesttest.PipelineNamesFrom(fakeStdout.Bytes()) assert.NoError(t, err) assert.Contains(t, pipelineNames, "archive") // XXX: provide helpers in manifesttest to extract this in a nicer way assert.Contains(t, fakeStdout.String(), `.el9.s390x.rpm`) } func TestManifestIntegrationOstreeSmoke(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() // we cannot hit ostree.f.o directly, we need to go via the mirrorlist resp, err := http.Get("https://ostree.fedoraproject.org/iot/mirrorlist") assert.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusOK, resp.StatusCode) body, err := io.ReadAll(resp.Body) assert.NoError(t, err) restore = main.MockOsArgs([]string{ "manifest", "iot-raw-xz", "--arch=x86_64", "--distro=fedora-42", "--ostree-url=" + strings.SplitN(string(body), "\n", 2)[0], "--ostree-ref=fedora/stable/x86_64/iot", }) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err = main.Run() assert.NoError(t, err) pipelineNames, err := manifesttest.PipelineNamesFrom(fakeStdout.Bytes()) assert.NoError(t, err) assert.Contains(t, pipelineNames, "ostree-deployment") // XXX: provide helpers in manifesttest to extract this in a nicer way assert.Contains(t, fakeStdout.String(), `{"type":"org.osbuild.ostree.init-fs"`) } func TestManifestIntegrationOstreeSmokeErrors(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() baseArgs := []string{ "manifest", "--arch=x86_64", "--distro=fedora-42", } for _, tc := range []struct { extraArgs []string expectedErr string }{ { []string{"iot-raw-xz"}, `iot-raw-xz: ostree commit URL required`, }, { []string{"server-qcow2", "--ostree-url=http://example.com/"}, `OSTree is not supported for "server-qcow2"`, }, } { args := append(baseArgs, tc.extraArgs...) restore = main.MockOsArgs(args) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err := main.Run() assert.EqualError(t, err, tc.expectedErr) } } // this is needed because images currently hardcodes the artifact filenames // so we need to faithfully reproduce this in our tests. see images PR#1039 // for an alternative way that would make this unneeded. func makeFakeOsbuildScript() string { return ` cat - > "$0".stdin output_dir="" export="" while [[ $# -gt 0 ]]; do key="$1" case $key in --output-directory) output_dir="$2" shift 2 ;; --export) export="$2" shift 2 ;; *) shift 1 esac done mkdir -p "$output_dir/$export" case $export in qcow2) echo "fake-img-qcow2" > "$output_dir/$export/disk.qcow2" ;; image) echo "fake-img-raw" > "$output_dir/$export/image.raw" ;; *) echo "Unknown export: $1 - add to testscript" exit 1 ;; esac ` } func TestBuildIntegrationHappy(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() tmpdir := t.TempDir() restore = main.MockOsArgs([]string{ "build", "qcow2", fmt.Sprintf("--blueprint=%s", makeTestBlueprint(t, testBlueprint)), "--distro", "centos-9", "--cache", tmpdir, }) defer restore() script := makeFakeOsbuildScript() fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", script) var err error // run inside the tmpdir to validate that the default output dir // creation works testutil.Chdir(t, tmpdir, func() { err = main.Run() }) assert.NoError(t, err) assert.Contains(t, fakeStdout.String(), `Image build successful, results: centos-9-qcow2-x86_64/centos-9-qcow2-x86_64.qcow2 `) // ensure osbuild was run exactly one require.Equal(t, 1, len(fakeOsbuildCmd.CallArgsList())) osbuildCall := fakeOsbuildCmd.CallArgsList()[0] // --cache is passed correctly to osbuild storePos := slices.Index(osbuildCall, "--store") assert.True(t, storePos > -1) assert.Equal(t, tmpdir, osbuildCall[storePos+1]) // and we passed the output dir outputDirPos := slices.Index(osbuildCall, "--output-directory") assert.True(t, outputDirPos > -1) assert.Equal(t, "centos-9-qcow2-x86_64", osbuildCall[outputDirPos+1]) // ... and that the manifest passed to osbuild manifest, err := os.ReadFile(fakeOsbuildCmd.Path() + ".stdin") assert.NoError(t, err) // XXX: provide helpers in manifesttest to extract this in a nicer way assert.Contains(t, string(manifest), `{"type":"org.osbuild.users","options":{"users":{"alice":{}}}}`) assert.Contains(t, string(manifest), `"image":{"name":"registry.gitlab.com/redhat/services/products/image-builder/ci/osbuild-composer/fedora-minimal"`) } func TestBuildIntegrationArgs(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() cacheDir := t.TempDir() for _, tc := range []struct { args []string expectedFiles []string }{ { nil, nil, }, { []string{"--with-manifest"}, []string{"centos-9-qcow2-x86_64.osbuild-manifest.json"}, }, { []string{"--with-buildlog"}, []string{"centos-9-qcow2-x86_64.buildlog"}, }, { []string{"--with-sbom"}, []string{"centos-9-qcow2-x86_64.buildroot-build.spdx.json", "centos-9-qcow2-x86_64.image-os.spdx.json", }, }, { []string{"--with-manifest", "--with-sbom"}, []string{"centos-9-qcow2-x86_64.buildroot-build.spdx.json", "centos-9-qcow2-x86_64.image-os.spdx.json", "centos-9-qcow2-x86_64.osbuild-manifest.json", }, }, } { t.Run(strings.Join(tc.args, ","), func(t *testing.T) { outputDir := filepath.Join(t.TempDir(), "output") cmd := []string{ "build", "qcow2", "--distro", "centos-9", "--cache", cacheDir, "--output-dir", outputDir, } cmd = append(cmd, tc.args...) restore = main.MockOsArgs(cmd) defer restore() script := makeFakeOsbuildScript() fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", script) err := main.Run() require.NoError(t, err) // ensure output dir override works osbuildCall := fakeOsbuildCmd.CallArgsList()[0] outputDirPos := slices.Index(osbuildCall, "--output-directory") assert.True(t, outputDirPos > -1) assert.Equal(t, outputDir, osbuildCall[outputDirPos+1]) // ensure we get exactly the expected files files, err := filepath.Glob(outputDir + "/*") assert.NoError(t, err) // we always have the qcow2 dir expectedFiles := append(tc.expectedFiles, "qcow") assert.Equal(t, len(expectedFiles), len(files), files) for _, expected := range tc.expectedFiles { _, err = os.Stat(filepath.Join(outputDir, expected)) assert.NoError(t, err, fmt.Sprintf("file %q missing", expected)) } }) } } var failingOsbuild = ` cat - > "$0".stdin echo "error on stdout" >&2 echo "error on stderr" sleep 0.1 >&3 echo '{"message": "osbuild-stage-output"}' exit 1 ` func TestBuildIntegrationErrorsProgressVerbose(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() outputDir := t.TempDir() restore = main.MockOsArgs([]string{ "build", "qcow2", "--distro", "centos-9", "--progress=verbose", "--output-dir", outputDir, }) defer restore() testutil.MockCommand(t, "osbuild", failingOsbuild) var err error stdout, stderr := testutil.CaptureStdio(t, func() { err = main.Run() }) assert.EqualError(t, err, "error running osbuild: exit status 1") assert.Contains(t, stdout, "error on stdout\n") assert.Contains(t, stderr, "error on stderr\n") } func TestBuildIntegrationErrorsProgressVerboseWithBuildlog(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() outputDir := t.TempDir() restore = main.MockOsArgs([]string{ "build", "qcow2", "--distro", "centos-9", "--progress=verbose", "--with-buildlog", "--output-dir", outputDir, }) defer restore() failingOsbuild := `#!/bin/sh cat - > "$0".stdin echo "error on stdout" >&2 echo "error on stderr" exit 1 ` testutil.MockCommand(t, "osbuild", failingOsbuild) var err error stdout, _ := testutil.CaptureStdio(t, func() { err = main.Run() }) assert.EqualError(t, err, "error running osbuild: exit status 1") // when the buildlog is used we do not get the direct output of // osbuild on stderr, to avoid races everything goes via stdout assert.Contains(t, stdout, "error on stdout\n") assert.Contains(t, stdout, "error on stderr\n") buildLog, err := os.ReadFile(filepath.Join(outputDir, "centos-9-qcow2-x86_64.buildlog")) assert.NoError(t, err) assert.Equal(t, string(buildLog), `error on stdout error on stderr `) } func TestBuildIntegrationErrorsProgressTerm(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() for _, withBuildlog := range []bool{false, true} { t.Run(fmt.Sprintf("with buildlog %v", withBuildlog), func(t *testing.T) { outputDir := t.TempDir() cmd := []string{ "build", "qcow2", "--distro", "centos-9", "--progress=term", "--output-dir", outputDir, } if withBuildlog { cmd = append(cmd, "--with-buildlog") } restore = main.MockOsArgs(cmd) defer restore() testutil.MockCommand(t, "osbuild", failingOsbuild) var err error stdout, stderr := testutil.CaptureStdio(t, func() { err = main.Run() }) assert.EqualError(t, err, `error running osbuild: exit status 1 BuildLog: osbuild-stage-output Output: error on stdout error on stderr `) assert.NotContains(t, stdout, "error on stdout") assert.NotContains(t, stderr, "error on stderr") if withBuildlog { buildLog, err := os.ReadFile(filepath.Join(outputDir, "centos-9-qcow2-x86_64.buildlog")) assert.NoError(t, err) assert.Equal(t, string(buildLog), `error on stdout error on stderr osbuild-stage-output `) } else { _, err := os.Stat(filepath.Join(outputDir, "centos-9-qcow2-x86_64.buildlog")) assert.True(t, os.IsNotExist(err)) } }) } } func TestManifestIntegrationWithSBOMWithOutputDir(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } outputDir := filepath.Join(t.TempDir(), "output-dir") restore := main.MockNewRepoRegistry(testrepos.New) defer restore() restore = main.MockOsArgs([]string{ "manifest", "qcow2", "--arch=x86_64", "--distro=centos-9", fmt.Sprintf("--blueprint=%s", makeTestBlueprint(t, testBlueprint)), "--with-sbom", "--output-dir", outputDir, }) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err := main.Run() assert.NoError(t, err) sboms, err := filepath.Glob(filepath.Join(outputDir, "*.spdx.json")) assert.NoError(t, err) assert.Equal(t, len(sboms), 2) assert.Equal(t, filepath.Join(outputDir, "centos-9-qcow2-x86_64.buildroot-build.spdx.json"), sboms[0]) assert.Equal(t, filepath.Join(outputDir, "centos-9-qcow2-x86_64.image-os.spdx.json"), sboms[1]) } func TestDescribeImageSmoke(t *testing.T) { restore := main.MockNewRepoRegistry(testrepos.New) defer restore() restore = main.MockOsArgs([]string{ "describe", "qcow2", "--distro=centos-9", "--arch=x86_64", }) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err := main.Run() assert.NoError(t, err) assert.Contains(t, fakeStdout.String(), `distro: centos-9 type: qcow2 arch: x86_64`) } func TestDescribeImageMinimal(t *testing.T) { restore := main.MockNewRepoRegistry(testrepos.New) defer restore() restore = main.MockDistroGetHostDistroName(func() (string, error) { return "centos-9", nil }) defer restore() restore = main.MockOsArgs([]string{ "describe", "qcow2", }) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err := main.Run() assert.NoError(t, err) assert.Contains(t, fakeStdout.String(), fmt.Sprintf(`distro: centos-9 type: qcow2 arch: %s`, arch.Current().String())) } func TestProgressFromCmd(t *testing.T) { cmd := &cobra.Command{} cmd.Flags().String("progress", "auto", "") cmd.Flags().Bool("verbose", false, "") for _, tc := range []struct { progress string verbose bool // XXX: progress should just export the types, then // this would be a bit nicer expectedProgress string }{ // we cannto test the "auto/false" case because it // depends on if there is a terminal attached or not //{"auto", false, "*progress.terminalProgressBar"}, {"auto", true, "*progress.verboseProgressBar"}, {"term", false, "*progress.terminalProgressBar"}, {"term", true, "*progress.terminalProgressBar"}, } { cmd.Flags().Set("progress", tc.progress) cmd.Flags().Set("verbose", fmt.Sprintf("%v", tc.verbose)) pbar, err := main.ProgressFromCmd(cmd) assert.NoError(t, err) assert.Equal(t, tc.expectedProgress, fmt.Sprintf("%T", pbar)) } } func TestManifestExtraRepos(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } if _, err := exec.LookPath("createrepo_c"); err != nil { t.Skip("need createrepo_c to run this test") } localRepoDir := filepath.Join(t.TempDir(), "repo") err := os.MkdirAll(localRepoDir, 0755) assert.NoError(t, err) err = exec.Command("cp", "-a", "../../test/data/rpm/dummy-1.0.0-0.noarch.rpm", localRepoDir).Run() assert.NoError(t, err) err = exec.Command("createrepo_c", localRepoDir).Run() assert.NoError(t, err) pkgHelloBlueprint := ` [[packages]] name = "dummy" ` nativeArch := arch.Current() // setup test for cross-arch, if we happen to run on the // cross arch already just pick a different arch crossArch := arch.ARCH_AARCH64 if arch.Current() == arch.ARCH_AARCH64 { crossArch = arch.ARCH_X86_64 } for _, arch := range []arch.Arch{nativeArch, crossArch} { t.Run(arch.String(), func(t *testing.T) { restore := main.MockOsArgs([]string{ "manifest", "qcow2", "--distro=centos-9", fmt.Sprintf("--arch=%s", arch), fmt.Sprintf("--extra-repo=file://%s", localRepoDir), "--blueprint", makeTestBlueprint(t, pkgHelloBlueprint), }) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() err = main.Run() require.NoError(t, err) // our local repo got added assert.Contains(t, fakeStdout.String(), `"path":"dummy-1.0.0-0.noarch.rpm"`) assert.Contains(t, fakeStdout.String(), fmt.Sprintf(`"url":"file://%s"`, localRepoDir)) }) } } func TestManifestOverrideRepo(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } var fakeStderr bytes.Buffer restore := main.MockOsStderr(&fakeStderr) defer restore() restore = main.MockOsArgs([]string{ "manifest", "qcow2", "--distro=centos-9", "--arch=x86_64", "--force-repo=http://xxx.abcdefgh-no-such-host.com/repo", }) defer restore() // XXX: dnfjson is very chatty and puts a bunch of output on stderr // we should probably silence this in images as its the job of the // error to catpure this. Use CaptureStdio here to ensure we don't // get noisy and confusing errors when this test runs. var err error testutil.CaptureStdio(t, func() { err = main.Run() }) assert.ErrorContains(t, err, "forced repo#0 xxx.abcdefgh-no-such-host.com/repo: http://xxx.abcdefgh-no-such-host.com/repo]: Cannot download repomd.xml") // XXX: we should probably look into "images" here, there is a bunch // of redundancy in the full error message: // // `error depsolving: running osbuild-depsolve-dnf failed: // DNF error occurred: RepoError: There was a problem reading a repository: Failed to download metadata for repo '9828718901ab404ac1b600157aec1a8b19f4b2139e7756f347fb0ecc06c92929' [forced repo#0 xxx.abcdefgh-no-such-host.com/repo: http://xxx.abcdefgh-no-such-host.com/repo]: Cannot download repomd.xml: Cannot download repodata/repomd.xml: All mirrors were tried` } func TestBuildCrossArchSmoke(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() tmpdir := t.TempDir() for _, withCrossArch := range []bool{false, true} { cmd := []string{ "build", "qcow2", "--distro", "centos-9", "--cache", tmpdir, "--output-dir", tmpdir, } if withCrossArch { cmd = append(cmd, "--arch=aarch64") } restore = main.MockOsArgs(cmd) defer restore() script := makeFakeOsbuildScript() fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", script) var err error _, stderr := testutil.CaptureStdio(t, func() { err = main.Run() }) assert.NoError(t, err) manifest, err := os.ReadFile(fakeOsbuildCmd.Path() + ".stdin") assert.NoError(t, err) pipelines, err := manifesttest.PipelineNamesFrom(manifest) assert.NoError(t, err) crossArchPipeline := "bootstrap-buildroot" crossArchWarning := `WARNING: using experimental cross-architecture building to build "aarch64"` if withCrossArch { assert.Contains(t, pipelines, crossArchPipeline) assert.Contains(t, stderr, crossArchWarning) } else { assert.NotContains(t, pipelines, crossArchPipeline) assert.NotContains(t, stderr, crossArchWarning) } } } func TestBuildIntegrationOutputFilename(t *testing.T) { if testing.Short() { t.Skip("manifest generation takes a while") } if !hasDepsolveDnf() { t.Skip("no osbuild-depsolve-dnf binary found") } restore := main.MockNewRepoRegistry(testrepos.New) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() tmpdir := t.TempDir() outputDir := filepath.Join(tmpdir, "output") restore = main.MockOsArgs([]string{ "build", "qcow2", fmt.Sprintf("--blueprint=%s", makeTestBlueprint(t, testBlueprint)), "--distro", "centos-9", "--cache", tmpdir, "--output-dir", outputDir, // XXX: also test --output-name="foo.n.0" here which should // have exactly the same result (once the depsolving is mocked) "--output-name=foo.n.0.qcow2", "--with-manifest", "--with-sbom", "--with-buildlog", }) defer restore() script := makeFakeOsbuildScript() testutil.MockCommand(t, "osbuild", script) err := main.Run() assert.NoError(t, err) expectedFiles := []string{ "foo.n.0.buildroot-build.spdx.json", "foo.n.0.image-os.spdx.json", "foo.n.0.osbuild-manifest.json", "foo.n.0.buildlog", "foo.n.0.qcow2", } files, err := filepath.Glob(outputDir + "/*") assert.NoError(t, err) assert.Equal(t, len(expectedFiles), len(files), files) for _, expected := range expectedFiles { _, err = os.Stat(filepath.Join(outputDir, expected)) assert.NoError(t, err, fmt.Sprintf("file %q missing from %v", expected, files)) } } func TestBasenameFor(t *testing.T) { restore := main.MockNewRepoRegistry(testrepos.New) defer restore() for _, tc := range []struct { imgTypeName string basename string expected string }{ // no user provided output name {"qcow2", "", "centos-9-qcow2-x86_64"}, {"minimal-raw", "", "centos-9-minimal-raw-x86_64"}, // simple {"qcow2", "foo", "foo"}, {"qcow2", "foo.n.0", "foo.n.0"}, // with extension {"qcow2", "foo.n.0.qcow2", "foo.n.0"}, {"minimal-raw", "foo.n.0.raw.xz", "foo.n.0"}, // with the "wrong" extension, we just ignore that and trust // the user (what else could we do?) {"qcow2", "foo.n.0.raw", "foo.n.0.raw"}, } { res, err := main.GetOneImage("centos-9", tc.imgTypeName, "x86_64", nil) require.NoError(t, err) 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":{"config":{"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`) } func TestManifestIntegrationWarningsHandling(t *testing.T) { restore := main.MockManifestgenDepsolver(fakeDepsolve) defer restore() restore = main.MockNewRepoRegistry(testrepos.New) defer restore() testBlueprint := ` customizations.FIPS = true ` fipsWarning := "The host building this image is not running in FIPS mode. The image will still be FIPS compliant. If you have custom steps that generate keys or perform cryptographic operations, those must be considered non-compliant.\n" for _, tc := range []struct { extraCmdline []string expectedErr string expectedStderr string }{ {nil, fipsWarning, ""}, {[]string{"--ignore-warnings"}, "", fipsWarning}, } { t.Run(fmt.Sprintf("extra-cmdline: %v", tc.extraCmdline), func(t *testing.T) { restore = main.MockOsArgs(append([]string{ "manifest", "qcow2", "--arch=x86_64", "--distro=centos-9", fmt.Sprintf("--blueprint=%s", makeTestBlueprint(t, testBlueprint)), }, tc.extraCmdline...)) defer restore() var fakeStdout bytes.Buffer restore = main.MockOsStdout(&fakeStdout) defer restore() var err error _, stderr := testutil.CaptureStdio(t, func() { err = main.Run() }) if tc.expectedErr != "" { assert.ErrorContains(t, err, tc.expectedErr) } else { assert.NoError(t, err) } assert.Contains(t, stderr, tc.expectedStderr) }) } }