Add a unit test for testing that providing only the required command line arguments produces expected output. Signed-off-by: Tomáš Hozza <thozza@redhat.com>
1060 lines
28 KiB
Go
1060 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"
|
|
"github.com/osbuild/images/pkg/arch"
|
|
)
|
|
|
|
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 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":{"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`)
|
|
}
|