debian-forge-composer/cmd/osbuild-composer-cli-tests/main_test.go
Brian C. Lane 7a4bb863dd Update deprecated io/ioutil functions
ioutil has been deprecated since go 1.16, this fixes all of the
deprecated functions we are using:

ioutil.ReadFile -> os.ReadFile
ioutil.ReadAll -> io.ReadAll
ioutil.WriteFile -> os.WriteFile
ioutil.TempFile -> os.CreateTemp
ioutil.TempDir -> os.MkdirTemp

All of the above are a simple name change, the function arguments and
results are exactly the same as before.

ioutil.ReadDir -> os.ReadDir

now returns a os.DirEntry but the IsDir and Name functions work the
same. The difference is that the FileInfo must be retrieved with the
Info() function which can also return an error.

These were identified by running:
golangci-lint run --build-tags=integration ./...
2023-03-07 09:22:23 -08:00

485 lines
14 KiB
Go

//go:build integration
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"
"testing"
"time"
"github.com/BurntSushi/toml"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/weldr"
)
func TestComposeCommands(t *testing.T) {
// common setup
tmpdir := NewTemporaryWorkDir(t, "osbuild-tests-")
defer tmpdir.Close(t)
bp := blueprint.Blueprint{
Name: "empty",
Description: "Test blueprint in toml format",
Packages: []blueprint.Package{{Name: "bash", Version: "*"}},
}
pushBlueprint(t, &bp)
defer deleteBlueprint(t, &bp)
runComposer(t, "blueprints", "save", "empty")
_, err := os.Stat("empty.toml")
require.NoError(t, err, "Error accessing 'empty.toml: %v'", err)
runComposer(t, "compose", "types")
runComposer(t, "compose", "status")
runComposer(t, "compose", "list")
runComposer(t, "compose", "list", "waiting")
runComposer(t, "compose", "list", "running")
runComposer(t, "compose", "list", "finished")
runComposer(t, "compose", "list", "failed")
// Full integration tests
uuid := buildCompose(t, "empty", "qcow2")
defer deleteCompose(t, uuid)
runComposer(t, "compose", "info", uuid.String())
runComposer(t, "compose", "metadata", uuid.String())
_, err = os.Stat(uuid.String() + "-metadata.tar")
require.NoError(t, err, "'%s-metadata.tar' not found", uuid.String())
defer os.Remove(uuid.String() + "-metadata.tar")
runComposer(t, "compose", "results", uuid.String())
_, err = os.Stat(uuid.String() + ".tar")
require.NoError(t, err, "'%s.tar' not found", uuid.String())
defer os.Remove(uuid.String() + ".tar")
// Just assert that result wasn't empty
result := runComposer(t, "compose", "log", uuid.String())
require.NotNil(t, result)
result = runComposer(t, "compose", "log", uuid.String(), "1024")
require.NotNil(t, result)
runComposer(t, "compose", "logs", uuid.String())
_, err = os.Stat(uuid.String() + "-logs.tar")
require.NoError(t, err, "'%s-logs.tar' not found", uuid.String())
defer os.Remove(uuid.String() + "-logs.tar")
runComposer(t, "compose", "image", uuid.String())
_, err = os.Stat(uuid.String() + "-disk.qcow2")
require.NoError(t, err, "'%s-disk.qcow2' not found", uuid.String())
defer os.Remove(uuid.String() + "-disk.qcow2")
// workers ask the composer every 15 seconds if a compose was canceled.
// Use 20 seconds here to be sure this is hit.
uuid = startCompose(t, "empty", "qcow2")
defer deleteCompose(t, uuid)
runComposer(t, "compose", "cancel", uuid.String())
time.Sleep(20 * time.Second)
status := waitForCompose(t, uuid)
assert.Equal(t, "FAILED", status)
// Check that reusing the cache works ok
uuid = buildCompose(t, "empty", "qcow2")
defer deleteCompose(t, uuid)
}
func TestBlueprintCommands(t *testing.T) {
// common setup
tmpdir := NewTemporaryWorkDir(t, "osbuild-tests-")
defer tmpdir.Close(t)
bp := blueprint.Blueprint{
Name: "empty",
Description: "Test empty blueprint in toml format",
}
pushBlueprint(t, &bp)
defer deleteBlueprint(t, &bp)
runComposer(t, "blueprints", "list")
runComposer(t, "blueprints", "show", "empty")
runComposer(t, "blueprints", "changes", "empty")
runComposer(t, "blueprints", "diff", "empty", "NEWEST", "WORKSPACE")
runComposer(t, "blueprints", "save", "empty")
_, err := os.Stat("empty.toml")
require.NoError(t, err, "'empty.toml' not found")
defer os.Remove("empty.toml")
runComposer(t, "blueprints", "depsolve", "empty")
runComposer(t, "blueprints", "freeze", "empty")
runComposer(t, "blueprints", "freeze", "show", "empty")
runComposer(t, "blueprints", "freeze", "save", "empty")
_, err = os.Stat("empty.frozen.toml")
require.NoError(t, err, "'empty.frozen.toml' not found")
defer os.Remove("empty.frozen.toml")
runComposer(t, "blueprints", "tag", "empty")
// undo the latest commit we can find
var changesWeldr []weldr.BlueprintsChangesV0Weldr
rawReply := runComposerJSON(t, "blueprints", "changes", "empty")
if isWeldrClientInstalled() {
err = json.Unmarshal(rawReply, &changesWeldr)
if err != nil {
changesWeldr = make([]weldr.BlueprintsChangesV0Weldr, 1)
err = json.Unmarshal(rawReply, &changesWeldr[0])
}
} else {
changesWeldr = make([]weldr.BlueprintsChangesV0Weldr, 1)
err = json.Unmarshal(rawReply, &changesWeldr[0].Body)
}
require.Nilf(t, err, "Error searching for commits to undo: %v", err)
runComposer(t, "blueprints", "undo", "empty", changesWeldr[0].Body.BlueprintsChanges[0].Changes[0].Commit)
runComposer(t, "blueprints", "workspace", "empty.toml")
}
func TestModulesCommands(t *testing.T) {
runComposer(t, "modules", "list")
}
func TestProjectsCommands(t *testing.T) {
runComposer(t, "projects", "list")
runComposer(t, "projects", "info", "filesystem")
runComposer(t, "projects", "info", "filesystem", "kernel")
}
func TestStatusCommands(t *testing.T) {
runComposer(t, "status", "show")
}
func TestSourcesCommands(t *testing.T) {
sources_toml, err := os.CreateTemp("", "SOURCES-*.TOML")
require.NoErrorf(t, err, "Could not create temporary file: %v", err)
defer os.Remove(sources_toml.Name())
_, err = sources_toml.Write([]byte(`id = "osbuild-test-addon-source"
name = "Testing sources add command"
url = "file://REPO-PATH"
type = "yum-baseurl"
proxy = "https://proxy-url/"
check_ssl = true
check_gpg = true
gpgkeys = ["https://url/path/to/gpg-key"]
`))
require.NoError(t, err)
runComposer(t, "sources", "list")
runComposer(t, "sources", "add", sources_toml.Name())
runComposer(t, "sources", "info", "osbuild-test-addon-source")
runComposer(t, "sources", "change", sources_toml.Name())
runComposer(t, "sources", "delete", "osbuild-test-addon-source")
}
func buildCompose(t *testing.T, bpName string, outputType string) uuid.UUID {
uuid := startCompose(t, bpName, outputType)
status := waitForCompose(t, uuid)
logs := getLogs(t, uuid)
assert.NotEmpty(t, logs, "logs are empty after the build is finished/failed")
// A sanity check for logs - every log for a compose should contain at
// least one mention of org.osbuild.rpm stage.
// This check should prevent bugs where we lose logs for all stages.
assert.Contains(t, logs, "org.osbuild.rpm")
if !assert.Equalf(t, "FINISHED", status, "Unexpected compose result: %s", status) {
log.Print("logs from the build: ", logs)
t.FailNow()
}
return uuid
}
func startCompose(t *testing.T, name, outputType string) uuid.UUID {
rawReply := runComposerJSON(t, "compose", "start", name, outputType)
type reply struct {
BuildID uuid.UUID `json:"build_id"`
Status bool `json:"status"`
}
type replyWithBody struct {
Body reply `json:"body"`
}
var replyWeldr []replyWithBody
var err error
if isWeldrClientInstalled() {
err = json.Unmarshal(rawReply, &replyWeldr)
if err != nil {
replyWeldr = make([]replyWithBody, 1)
err = json.Unmarshal(rawReply, &replyWeldr[0])
}
} else {
replyWeldr = make([]replyWithBody, 1)
err = json.Unmarshal(rawReply, &replyWeldr[0].Body)
}
require.Nilf(t, err, "Unexpected reply: %v", err)
require.Truef(t, replyWeldr[0].Body.Status, "Unexpected status %v", replyWeldr[0].Body.Status)
return replyWeldr[0].Body.BuildID
}
func deleteCompose(t *testing.T, id uuid.UUID) {
rawReply := runComposerJSON(t, "compose", "delete", id.String())
type deleteUUID struct {
ID uuid.UUID `json:"uuid"`
Status bool `json:"status"`
}
type reply struct {
IDs []deleteUUID `json:"uuids"`
Errors []interface{} `json:"errors"`
}
type replyWithBody struct {
Body reply `json:"body"`
}
var replyWeldr []replyWithBody
var err error
if isWeldrClientInstalled() {
err = json.Unmarshal(rawReply, &replyWeldr)
if err != nil {
replyWeldr = make([]replyWithBody, 1)
err = json.Unmarshal(rawReply, &replyWeldr[0])
}
} else {
replyWeldr = make([]replyWithBody, 1)
err = json.Unmarshal(rawReply, &replyWeldr[0].Body)
}
require.Nilf(t, err, "Unexpected reply: %v", err)
require.Zerof(t, len(replyWeldr[0].Body.Errors), "Unexpected errors")
require.Equalf(t, 1, len(replyWeldr[0].Body.IDs), "Unexpected number of UUIDs returned: %d", len(replyWeldr[0].Body.IDs))
require.Truef(t, replyWeldr[0].Body.IDs[0].Status, "Unexpected status %v", replyWeldr[0].Body.IDs[0].Status)
}
func waitForCompose(t *testing.T, uuid uuid.UUID) string {
for {
status := getComposeStatus(t, uuid)
if status == "FINISHED" || status == "FAILED" {
return status
}
time.Sleep(time.Second)
}
}
func getComposeStatus(t *testing.T, uuid uuid.UUID) string {
rawReply := runComposerJSON(t, "compose", "info", uuid.String())
type reply struct {
QueueStatus string `json:"queue_status"`
}
type replyWithBody struct {
Body reply `json:"body"`
}
var replyWeldr []replyWithBody
var err error
if isWeldrClientInstalled() {
err = json.Unmarshal(rawReply, &replyWeldr)
if err != nil {
replyWeldr = make([]replyWithBody, 1)
err = json.Unmarshal(rawReply, &replyWeldr[0])
}
} else {
replyWeldr = make([]replyWithBody, 1)
err = json.Unmarshal(rawReply, &replyWeldr[0].Body)
}
require.Nilf(t, err, "Unexpected reply: %v", err)
return replyWeldr[0].Body.QueueStatus
}
func getLogs(t *testing.T, uuid uuid.UUID) string {
// There's no potential command injection vector here
/* #nosec G204 */
cmd := exec.Command("composer-cli", "compose", "log", uuid.String())
cmd.Stderr = os.Stderr
stdoutReader, err := cmd.StdoutPipe()
require.NoError(t, err)
err = cmd.Start()
require.NoError(t, err)
var buffer bytes.Buffer
_, err = buffer.ReadFrom(stdoutReader)
require.NoError(t, err)
err = cmd.Wait()
require.NoError(t, err)
return buffer.String()
}
func pushBlueprint(t *testing.T, bp *blueprint.Blueprint) {
tmpfile, err := os.CreateTemp("", "osbuild-test-")
require.Nilf(t, err, "Could not create temporary file: %v", err)
defer os.Remove(tmpfile.Name())
err = toml.NewEncoder(tmpfile).Encode(bp)
require.Nilf(t, err, "Could not marshal blueprint TOML: %v", err)
err = tmpfile.Close()
require.Nilf(t, err, "Could not close toml file: %v", err)
rawReply := runComposerJSON(t, "blueprints", "push", tmpfile.Name())
type reply struct {
Status bool `json:"status"`
}
type replyWithBody struct {
Body reply `json:"body"`
}
var replyWeldr []replyWithBody
if isWeldrClientInstalled() {
err = json.Unmarshal(rawReply, &replyWeldr)
if err != nil {
replyWeldr = make([]replyWithBody, 1)
err = json.Unmarshal(rawReply, &replyWeldr[0])
}
} else {
replyWeldr = make([]replyWithBody, 1)
err = json.Unmarshal(rawReply, &replyWeldr[0].Body)
}
require.Nilf(t, err, "Unexpected reply: %v", err)
log.Printf("Mesasge: %v", replyWeldr[0].Body.Status)
require.Truef(t, replyWeldr[0].Body.Status, "Unexpected status %v", replyWeldr[0].Body.Status)
}
func deleteBlueprint(t *testing.T, bp *blueprint.Blueprint) {
rawReply := runComposerJSON(t, "blueprints", "delete", bp.Name)
type reply struct {
Status bool `json:"status"`
}
type replyWithBody struct {
Body reply `json:"body"`
}
var replyWeldr []replyWithBody
var err error
if isWeldrClientInstalled() {
err = json.Unmarshal(rawReply, &replyWeldr)
if err != nil {
replyWeldr = make([]replyWithBody, 1)
err = json.Unmarshal(rawReply, &replyWeldr[0])
}
} else {
replyWeldr = make([]replyWithBody, 1)
err = json.Unmarshal(rawReply, &replyWeldr[0].Body)
}
require.Nilf(t, err, "Unexpected reply: %v", err)
require.Truef(t, replyWeldr[0].Body.Status, "Unexpected status %v", replyWeldr[0].Body.Status)
}
func runComposer(t *testing.T, command ...string) []byte {
fmt.Printf("Running composer-cli %s\n", strings.Join(command, " "))
cmd := exec.Command("composer-cli", command...)
stdout, err := cmd.StdoutPipe()
require.Nilf(t, err, "Could not create command: %v", err)
stderr, err := cmd.StderrPipe()
require.Nilf(t, err, "Could not create command: %v", err)
err = cmd.Start()
require.Nilf(t, err, "Could not start command: %v", err)
contents, err := io.ReadAll(stdout)
require.NoError(t, err, "Could not read stdout from command")
errcontents, err := io.ReadAll(stderr)
require.NoError(t, err, "Could not read stderr from command")
err = cmd.Wait()
require.NoErrorf(t, err, "Command failed (%v): %v", err, string(errcontents))
return contents
}
func runComposerJSON(t *testing.T, command ...string) json.RawMessage {
command = append([]string{"--json"}, command...)
contents := runComposer(t, command...)
var result json.RawMessage
if len(contents) != 0 {
err := json.Unmarshal(contents, &result)
if err != nil {
// We probably got two jsons
tmp := bytes.SplitN(contents, []byte("}\n{"), 2)
tmp2 := [][]byte{tmp[0], []byte("}")}
joined := bytes.Join(tmp2, nil)
err := json.Unmarshal(joined, &result)
if err != nil {
// We did not get JSON, try interpreting it as TOML
var data interface{}
err = toml.Unmarshal(contents, &data)
require.Nilf(t, err, "Could not parse output: %v", err)
buffer := bytes.Buffer{}
err = json.NewEncoder(&buffer).Encode(data)
require.Nilf(t, err, "Could not remarshal TOML to JSON: %v", err)
err = json.NewDecoder(&buffer).Decode(&result)
require.Nilf(t, err, "Could not decode the remarshalled JSON: %v", err)
}
}
}
buffer := bytes.Buffer{}
encoder := json.NewEncoder(&buffer)
encoder.SetIndent("", " ")
err := encoder.Encode(result)
require.Nilf(t, err, "Could not remarshal the recevied JSON: %v", err)
return result
}
type TemporaryWorkDir struct {
OldWorkDir string
Path string
}
// Creates a new temporary directory based on pattern and changes the current
// working directory to it.
//
// Example:
// d := NewTemporaryWorkDir(t, "foo-*")
// defer d.Close(t)
func NewTemporaryWorkDir(t *testing.T, pattern string) TemporaryWorkDir {
var d TemporaryWorkDir
var err error
d.OldWorkDir, err = os.Getwd()
require.Nilf(t, err, "os.GetWd: %v", err)
d.Path, err = os.MkdirTemp("", pattern)
require.Nilf(t, err, "os.MkdirTemp: %v", err)
err = os.Chdir(d.Path)
require.Nilf(t, err, "os.ChDir: %v", err)
return d
}
// Change back to the previous working directory and removes the temporary one.
func (d *TemporaryWorkDir) Close(t *testing.T) {
var err error
err = os.Chdir(d.OldWorkDir)
require.Nilf(t, err, "os.ChDir: %v", err)
err = os.RemoveAll(d.Path)
require.Nilf(t, err, "os.RemoveAll: %v", err)
}
// This checks wheter weldr-client is installed or not
func isWeldrClientInstalled() bool {
cmd := exec.Command("rpm", "-q", "weldr-client")
err := cmd.Run()
if err != nil {
return false
} else {
return true
}
}