Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ jobs:
- name: Run unit tests for bib container
run: sudo go test ./pkg/bib/container/... --fail-if-podman-missing

- name: Run tests for bootc container
run: sudo go test ./pkg/distro/bootc/...

unit-tests-cs:
strategy:
matrix:
Expand Down
27 changes: 15 additions & 12 deletions pkg/bib/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package container

import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -67,11 +66,7 @@ func New(ref string) (*Container, error) {
// not all containers set {{.Architecture}} so fallback
c.arch, err = findContainerArchInspect(c.id, ref)
if err != nil {
var err2 error
c.arch, err2 = findContainerArchUname(c.id, ref)
if err2 != nil {
return nil, errors.Join(err, err2)
}
return nil, err
}

/* #nosec G204 */
Expand Down Expand Up @@ -185,9 +180,9 @@ func (c *Container) DefaultRootfsType() (string, error) {
return fsType, nil
}

func findContainerArchInspect(cntId, ref string) (string, error) {
func findImageIdFor(cntId, ref string) (string, error) {
/* #nosec G204 */
output, err := exec.Command("podman", "inspect", "-f", "{{.Architecture}}", cntId).Output()
output, err := exec.Command("podman", "inspect", "-f", "{{.Image}}", cntId).Output()
if err != nil {
if err, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("inspecting container %q failed: %w\nstderr:\n%s", ref, err, err.Stderr)
Expand All @@ -197,14 +192,22 @@ func findContainerArchInspect(cntId, ref string) (string, error) {
return strings.TrimSpace(string(output)), nil
}

func findContainerArchUname(cntId, ref string) (string, error) {
func findContainerArchInspect(cntId, ref string) (string, error) {
// get image id first, then get the arch from the image,
// it seems this is the most reliable way to get the
// architecture
imageId, err := findImageIdFor(cntId, ref)
if err != nil {
return "", err
}

/* #nosec G204 */
output, err := exec.Command("podman", "exec", cntId, "uname", "-m").Output()
output, err := exec.Command("podman", "inspect", "-f", "{{.Architecture}}", imageId).Output()
if err != nil {
if err, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("running 'uname -m' from container %q failed: %w\nstderr:\n%s", cntId, err, err.Stderr)
return "", fmt.Errorf("inspecting container %q failed: %w\nstderr:\n%s", ref, err, err.Stderr)
}
return "", fmt.Errorf("running 'uname -m' from container %q failed with generic error: %w", cntId, err)
return "", fmt.Errorf("inspecting %s container failed with generic error: %w", ref, err)
}
return strings.TrimSpace(string(output)), nil
}
105 changes: 105 additions & 0 deletions pkg/distro/bootc/bootctest/bootctest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package bootctest

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/osbuild/images/internal/randutil"
)

func makeOsRelease(t *testing.T, buildDir string) {
osRelease := `
NAME="bootc-fake-name"
ID="bootc-fake"
VERSION_ID="1"
`

osReleasePath := filepath.Join(buildDir, "etc/os-release")
err := os.MkdirAll(filepath.Dir(osReleasePath), 0755)
require.NoError(t, err)
//nolint:gosec
err = os.WriteFile(osReleasePath, []byte(osRelease), 0644)
require.NoError(t, err)
}

func makeBootcInstallToml(t *testing.T, buildDir string) {
installToml := `
[install]
filesystem = [
{ mountpoint = "/", type = "xfs", size = "10 GiB" },
{ mountpoint = "/boot", type = "ext4", size = "1 GiB" },
]
`

installTomlPath := filepath.Join(buildDir, "usr/lib/bootc/install/99-fedora-install.toml")
err := os.MkdirAll(filepath.Dir(installTomlPath), 0755)
require.NoError(t, err)
//nolint:gosec
err = os.WriteFile(installTomlPath, []byte(installToml), 0644)
require.NoError(t, err)
}

func makeFakeBinaries(t *testing.T, buildDir string) {
_, currentFile, _, ok := runtime.Caller(0)
require.True(t, ok)
currentDir := filepath.Dir(currentFile)

destDir := fmt.Sprintf("DESTDIR=%s", filepath.Join(buildDir, "/usr/bin"))
//nolint:gosec
output, err := exec.Command("make", "-C", filepath.Join(currentDir, "exe"), destDir).CombinedOutput()
require.NoError(t, err, string(output))
}

func makeContainerfile(t *testing.T, buildDir string) {
var fakeBootcCnt = `
FROM scratch
COPY etc /etc
COPY usr/bin /usr/bin
COPY usr/lib/bootc/install /usr/lib/bootc/install
`

cntFilePath := filepath.Join(buildDir, "Containerfile")
//nolint:gosec
err := os.WriteFile(cntFilePath, []byte(fakeBootcCnt), 0644)
require.NoError(t, err)
}

func makeFakeContainerImage(t *testing.T, buildDir, purpose string) string {
imgTag := fmt.Sprintf("image-builder-test-%s-%s", purpose, randutil.String(10, randutil.AsciiLower))
//nolint:gosec
output, err := exec.Command(
"podman", "build",
"-f", filepath.Join(buildDir, "Containerfile"),
"-t", imgTag,
).CombinedOutput()
require.NoError(t, err, string(output))
// add cleanup
t.Cleanup(func() {
output, err := exec.Command("podman", "image", "rm", imgTag).CombinedOutput()
assert.NoError(t, err, string(output))
})

return fmt.Sprintf("localhost/%s", imgTag)
}

func NewFakeContainer(t *testing.T, purpose string) string {
t.Helper()

buildDir := t.TempDir()

// XXX: allow adding test specific content
makeContainerfile(t, buildDir)
makeFakeBinaries(t, buildDir)
// XXX: make os-release content configurable
makeOsRelease(t, buildDir)
makeBootcInstallToml(t, buildDir)

return makeFakeContainerImage(t, buildDir, purpose)
}
16 changes: 16 additions & 0 deletions pkg/distro/bootc/bootctest/exe/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
DESTDIR ?= .

.PHONY: all clean

all: $(DESTDIR)/bootc $(DESTDIR)/sleep

$(DESTDIR)/bootc: *.go
mkdir -p $(DESTDIR)
CGO_ENABLED=0 go build -o $@ $<

$(DESTDIR)/sleep: $(DESTDIR)/bootc
cd $(DESTDIR) && ln -sf bootc sleep

clean:
rm -f $(DESTDIR)/bootc $(DESTDIR)/sleep

40 changes: 40 additions & 0 deletions pkg/distro/bootc/bootctest/exe/bootc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"fmt"
"math"
"os"
"path/filepath"
"time"
)

func fakeBootc() error {
if os.Args[1] != "install" || os.Args[2] != "print-configuration" {
return fmt.Errorf("unexpected bootc arguments %v", os.Args)
}
// print a sensible default configuration
fmt.Println(`{"filesystem": {"root": {"type": "ext4"}}}`)
return nil
}

func fakeSleep() error {
if os.Args[1] != "infinity" {
return fmt.Errorf("unexpected sleep arguments %v", os.Args)
}
time.Sleep(math.MaxInt64)
return nil
}

func main() {
var err error
switch filepath.Base(os.Args[0]) {
case "bootc":
err = fakeBootc()
case "sleep":
err = fakeSleep()
}
if err != nil {
println("error: ", err.Error())
os.Exit(1)
}
}
104 changes: 104 additions & 0 deletions pkg/distro/bootc/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package bootc_test

import (
"bytes"
"fmt"
"os"
"os/exec"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/osbuild/blueprint/pkg/blueprint"

"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/distro/bootc"
"github.com/osbuild/images/pkg/distro/bootc/bootctest"
"github.com/osbuild/images/pkg/manifestgen"
"github.com/osbuild/images/pkg/osbuild/manifesttest"
"github.com/osbuild/images/pkg/rpmmd"
)

func canRunIntegration(t *testing.T) {
if os.Getuid() != 0 {
t.Skip("test needs root")
}
if _, err := exec.LookPath("podman"); err != nil {
t.Skip("test needs installed podman")
}
if _, err := exec.LookPath("systemd-detect-virt"); err != nil {
t.Skip("test needs systemd-detect-virt")
}
// exit code "0" means the container is detected
if err := exec.Command("systemd-detect-virt", "-c", "-q").Run(); err == nil {
t.Skip("test cannot run inside a container")
}
}

func genManifest(t *testing.T, imgType distro.ImageType) string {
var bp blueprint.Blueprint

var manifestJson bytes.Buffer
mg, err := manifestgen.New(nil, &manifestgen.Options{
Output: &manifestJson,
OverrideRepos: []rpmmd.RepoConfig{
{Id: "not-used", BaseURLs: []string{"not-used"}},
},
})
assert.NoError(t, err)
err = mg.Generate(&bp, imgType.Arch().Distro(), imgType, imgType.Arch(), nil)
assert.NoError(t, err)

// XXX: it would be nice to return an *osbuild.Manifest here
// and do all of this more structed, however this is not
// working currently as osbuild.NewManifestsFromBytes() cannot
// unmarshal our manifests because of:
// "unexpected source name: org.osbuild.containers-storage"
return manifestJson.String()
}

func TestBuildContainerHandling(t *testing.T) {
canRunIntegration(t)

imgTag := bootctest.NewFakeContainer(t, "bootc")
buildImgTag := bootctest.NewFakeContainer(t, "build")

for _, withBuildContainer := range []bool{true, false} {
t.Run(fmt.Sprintf("build-cnt:%v", withBuildContainer), func(t *testing.T) {
distri, err := bootc.NewBootcDistro(imgTag)
require.NoError(t, err)
if withBuildContainer {
err = distri.SetBuildContainer(buildImgTag)
require.NoError(t, err)
}

archi, err := distri.GetArch(arch.Current().String())
require.NoError(t, err)
imgType, err := archi.GetImageType("qcow2")
assert.NoError(t, err)

manifestJson := genManifest(t, imgType)
pipelineNames, err := manifesttest.PipelineNamesFrom([]byte(manifestJson))
require.NoError(t, err)
buildStages, err := manifesttest.StagesForPipeline([]byte(manifestJson), "build")
require.NoError(t, err)
// the bootc container is always pulled
assert.Contains(t, manifestJson, imgTag)
if withBuildContainer {
assert.Contains(t, manifestJson, buildImgTag)
// validate that the usr/lib/bootc/install/ dir is copied
assert.Contains(t, manifestJson, "usr/lib/bootc/install/")
assert.Contains(t, buildStages, "org.osbuild.copy")
// validate that we have a "target" pipeline for raw content
assert.Contains(t, pipelineNames, "target")
} else {
assert.NotContains(t, manifestJson, buildImgTag)
assert.NotContains(t, manifestJson, "usr/lib/bootc/install/")
assert.NotContains(t, buildStages, "org.osbuild.copy")
assert.NotContains(t, pipelineNames, "target")
}
})
}
}
Loading