Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bib: resolve container content from the external dnf again #704

Merged
Merged
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
97 changes: 77 additions & 20 deletions bib/internal/container/solver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package container

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

Expand All @@ -11,6 +12,13 @@ import (
"github.com/osbuild/bootc-image-builder/bib/internal/source"
)

func forceSymlink(symlinkPath, target string) error {
if output, err := exec.Command("ln", "-sf", target, symlinkPath).CombinedOutput(); err != nil {
return fmt.Errorf("cannot run ln: %w, output:\n%s", err, output)
}
return nil
}

// InitDNF initializes dnf in the container. This is necessary when
// the caller wants to read the image's dnf repositories, but they are
// not static, but rather configured by dnf dynamically. The primaru
Expand All @@ -31,38 +39,87 @@ func (c *Container) InitDNF() error {
return nil
}

func (cnt *Container) injectDNFJson() ([]string, error) {
if err := cnt.CopyInto("/usr/libexec/osbuild-depsolve-dnf", "/osbuild-depsolve-dnf"); err != nil {
return nil, fmt.Errorf("cannot prepare depsolve in the container: %w", err)
func (cnt *Container) hasRunSecrets() bool {
_, err := os.Stat(filepath.Join(cnt.root, "/run/secrets/redhat.repo"))
return err == nil
}

// setupRunSecretsBindMount will synthesise a /run/secrets dir
// in the container root
func (cnt *Container) setupRunSecrets() error {
if cnt.hasRunSecrets() {
return nil
}
// copy the python module too
globPath := "/usr/lib/*/site-packages/osbuild"
matches, err := filepath.Glob(globPath)
if err != nil || len(matches) == 0 {
return nil, fmt.Errorf("cannot find osbuild python module in %q: %w", globPath, err)
dst := filepath.Join(cnt.root, "/run/secrets")
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}

// We cannot just bind mount here because
// /usr/share/rhel/secrets contains a bunch of relative symlinks
// that will point to the container root not the host when resolved
// from the outside (via the host container mount).
//
// So instead of bind mounting we create a copy of the
// /run/secrets/ - they are static so that should be fine.
//
// We want to support /usr/share/rhel/secrets too to be able
// to run "bootc-image-builder manifest" directly on the host
// (which is useful for e.g. composer).
for _, src := range []string{"/run/secrets", "/usr/share/rhel/secrets"} {
if st, err := os.Stat(src); err != nil || !st.IsDir() {
continue
}

dents, err := filepath.Glob(src + "/*")
if err != nil {
return err
}
for _, ent := range dents {
// Check if the target file actually exists (i.e. for
// symlinks that they are valid) and only copy if so.
// This covers unsubscribed machines.
if _, err := os.Stat(ent); err != nil {
continue
}

// Note the use of "-L" here to dereference/copy links
if output, err := exec.Command("cp", "-rvL", ent, dst).CombinedOutput(); err != nil {
return fmt.Errorf("failed to setup /run/secrets: %w, output:\n%s", err, string(output))
}
}
}
if len(matches) != 1 {
return nil, fmt.Errorf("unexpected number of osbuild python module matches: %v", matches)

// workaround broken containers (like f41) that use absolute symlinks
// to point to the entitlements-host and rhsm-host, they need to be
// relative so that the "SetRootdir()" from the resolver works, i.e.
// they need to point into the mounted container.
symlink := filepath.Join(cnt.root, "/etc/pki/entitlement-host")
target := "../../run/secrets/etc-pki-entitlement"
if err := forceSymlink(symlink, target); err != nil {
return err
}
if err := cnt.CopyInto(matches[0], "/"); err != nil {
return nil, fmt.Errorf("cannot prepare depsolve python-modules in the container: %w", err)
symlink = filepath.Join(cnt.root, "/etc/rhsm-host")
target = "../run/secrets/rhsm"
if err := forceSymlink(symlink, target); err != nil {
return err
}
return append(cnt.ExecArgv(), "/osbuild-depsolve-dnf"), nil
return nil
}

func (cnt *Container) NewContainerSolver(cacheRoot string, architecture arch.Arch, sourceInfo *source.Info) (*dnfjson.Solver, error) {
depsolverCmd, err := cnt.injectDNFJson()
if err != nil {
return nil, fmt.Errorf("cannot inject depsolve into the container: %w", err)
}

solver := dnfjson.NewSolver(
sourceInfo.OSRelease.PlatformID,
sourceInfo.OSRelease.VersionID,
architecture.String(),
fmt.Sprintf("%s-%s", sourceInfo.OSRelease.ID, sourceInfo.OSRelease.VersionID),
cacheRoot)
solver.SetDNFJSONPath(depsolverCmd[0], depsolverCmd[1:]...)
solver.SetRootDir("/")

// we copy the data directly into the cnt.root, no need to
// cleanup here because podman stop will remove the dir
if err := cnt.setupRunSecrets(); err != nil {
return nil, err
}
solver.SetRootDir(cnt.root)
return solver, nil
}
95 changes: 80 additions & 15 deletions bib/internal/container/solver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -18,21 +19,37 @@ import (
)

const (
dnfTestingImageRHEL = "registry.access.redhat.com/ubi9:latest"
dnfTestingImageCentos = "quay.io/centos/centos:stream9"
dnfTestingImageRHEL = "registry.access.redhat.com/ubi9:latest"
dnfTestingImageCentos = "quay.io/centos/centos:stream9"
dnfTestingImageFedoraLatest = "registry.fedoraproject.org/fedora:latest"
)

func TestDNFJsonWorks(t *testing.T) {
func ensureCanRunDNFJsonTests(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("skipping test; not running as root")
}
if _, err := os.Stat("/usr/libexec/osbuild-depsolve-dnf"); err != nil {
t.Skip("cannot find /usr/libexec/osbuild-depsolve-dnf")
}
}

func ensureAMD64(t *testing.T) {
if runtime.GOARCH != "amd64" {
t.Skip("skipping test; only runs on x86_64")
}
}

func TestDNFJsonWorks(t *testing.T) {
ensureCanRunDNFJsonTests(t)

cacheRoot := t.TempDir()

cnt, err := container.New(dnfTestingImageCentos)
require.NoError(t, err)
defer func() {
assert.NoError(t, cnt.Stop())
}()

err = cnt.InitDNF()
require.NoError(t, err)

Expand Down Expand Up @@ -82,9 +99,7 @@ func TestDNFInitGivesAccessToSubscribedContent(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("skipping test; not running as root")
}
if runtime.GOARCH != "amd64" {
t.Skip("skipping test; only runs on x86_64")
}
ensureAMD64(t)

restore := subscribeMachine(t)
defer restore()
Expand All @@ -100,29 +115,27 @@ func TestDNFInitGivesAccessToSubscribedContent(t *testing.T) {
}

func TestDNFJsonWorkWithSubscribedContent(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("skipping test; not running as root")
}
if runtime.GOARCH != "amd64" {
t.Skip("skipping test; only runs on x86_64")
}
if _, err := os.Stat("/usr/libexec/osbuild-depsolve-dnf"); err != nil {
t.Skip("cannot find /usr/libexec/osbuild-depsolve-dnf")
}
ensureCanRunDNFJsonTests(t)
ensureAMD64(t)
cacheRoot := t.TempDir()

restore := subscribeMachine(t)
defer restore()

cnt, err := container.New(dnfTestingImageRHEL)
require.NoError(t, err)
defer func() {
assert.NoError(t, cnt.Stop())
}()

err = cnt.InitDNF()
require.NoError(t, err)

sourceInfo, err := source.LoadInfo(cnt.Root())
require.NoError(t, err)
solver, err := cnt.NewContainerSolver(cacheRoot, arch.ARCH_X86_64, sourceInfo)
require.NoError(t, err)

res, err := solver.Depsolve([]rpmmd.PackageSet{
{
Include: []string{"coreutils"},
Expand All @@ -131,3 +144,55 @@ func TestDNFJsonWorkWithSubscribedContent(t *testing.T) {
require.NoError(t, err)
assert.True(t, len(res.Packages) > 0)
}

func runCmd(t *testing.T, args ...string) {
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
require.NoError(t, err)
}

func TestDNFJsonWorkWithSubscribedContentNestedContainers(t *testing.T) {
ensureCanRunDNFJsonTests(t)
ensureAMD64(t)
tmpdir := t.TempDir()

restore := subscribeMachine(t)
defer restore()

// build a test binary from the existing
// TestDNFJsonWorkWithSubscribedContent that is then
// transfered and run *inside* the centos container
testBinary := filepath.Join(tmpdir, "dnftest")
runCmd(t, "go", "test",
"-c",
"-o", testBinary,
"-run", "^TestDNFJsonWorkWithSubscribedContent$")

output, err := exec.Command(
"podman", "run", "--rm",
"--privileged",
"--init",
"--detach",
"--entrypoint", "sleep",
// use a fedora container as intermediate so that we
// always have the latest glibc (we cannot fully
// static link the test)
dnfTestingImageFedoraLatest,
"infinity",
).Output()
require.NoError(t, err, string(output))
cntID := strings.TrimSpace(string(output))
defer func() {
err := exec.Command("podman", "stop", cntID).Run()
assert.NoError(t, err)
}()

runCmd(t, "podman", "cp", testBinary, cntID+":/dnftest")
// we need these test dependencies inside the container
runCmd(t, "podman", "exec", cntID, "dnf", "install", "-y",
"gpgme", "podman")
// run the test
runCmd(t, "podman", "exec", cntID, "/dnftest")
}
Loading