debian-forge-cli/cmd/image-builder/main_test.go
Michael Vogt aecbe5928a 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".
2025-04-17 13:17:24 +00:00

1032 lines
28 KiB
Go

package main_test
import (
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/sirupsen/logrus"
"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"
)
func init() {
// silence logrus by default, it is quite verbose
logrus.SetLevel(logrus.WarnLevel)
}
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.9 comes before 8.10
assert.Regexp(t, `(?ms)rhel-8.9.*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-image",
"--arch=x86_64",
"--distro=fedora-40",
"--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-40",
}
for _, tc := range []struct {
extraArgs []string
expectedErr string
}{
{
[]string{"iot-raw-image"},
`iot-raw-image: ostree commit URL required`,
},
{
[]string{"qcow2", "--ostree-url=http://example.com/"},
`OSTree is not supported for "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)
defer fakeOsbuildCmd.Restore()
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.Calls()))
osbuildCall := fakeOsbuildCmd.Calls()[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)
defer fakeOsbuildCmd.Restore()
err := main.Run()
require.NoError(t, err)
// ensure output dir override works
osbuildCall := fakeOsbuildCmd.Calls()[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()
fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", failingOsbuild)
defer fakeOsbuildCmd.Restore()
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
`
fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", failingOsbuild)
defer fakeOsbuildCmd.Restore()
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()
fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", failingOsbuild)
defer fakeOsbuildCmd.Restore()
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 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 TestManifestExtraRepo(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"
`
restore := main.MockOsArgs([]string{
"manifest",
"qcow2",
"--distro=centos-9",
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)
defer fakeOsbuildCmd.Restore()
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()
fakeOsbuildCmd := testutil.MockCommand(t, "osbuild", script)
defer fakeOsbuildCmd.Restore()
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":{"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`)
}