debian-forge-cli/cmd/image-builder/main_test.go
Tomáš Hozza 57e8468390 Test/main: don't rely on EOL RHEL 8.9 in tests
Signed-off-by: Tomáš Hozza <thozza@redhat.com>
2025-07-09 06:52:47 +00:00

1054 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/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)
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 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 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":{"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`)
}