Skip to content

Commit

Permalink
bib: resolve container content from the external dnf again
Browse files Browse the repository at this point in the history
This commit goes back to the approach we had in PR#565 to resolve using
the solver from outside of the container. The reason to back is that
some containers (like f41) do not have the python dnf available so
they break with the "run dnfjson inside" approach (that was meant
as a quick fix only anyway).

Huge kudos to Achilleas for most of the work here.

Co-Authored-By: Achilleas Koutsou <[email protected]>
  • Loading branch information
mvo5 and achilleas-k committed Nov 6, 2024
1 parent 858734f commit ba5a4c9
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 20 deletions.
90 changes: 70 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,80 @@ 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
}
if len(matches) != 1 {
return nil, fmt.Errorf("unexpected number of osbuild python module matches: %v", matches)

// 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 {
// 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 err := cnt.CopyInto(matches[0], "/"); err != nil {
return nil, fmt.Errorf("cannot prepare depsolve python-modules in the container: %w", err)

// 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
}
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
}
5 changes: 5 additions & 0 deletions bib/internal/container/solver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func TestDNFJsonWorks(t *testing.T) {

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

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

Expand Down Expand Up @@ -116,13 +118,16 @@ func TestDNFJsonWorkWithSubscribedContent(t *testing.T) {

cnt, err := container.New(dnfTestingImageRHEL)
require.NoError(t, err)
defer 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 Down

0 comments on commit ba5a4c9

Please sign in to comment.