Skip to content

Commit

Permalink
bib: reoslve container content from the external dnf again
Browse files Browse the repository at this point in the history
This commit goes back to the apparoch we had in PR#565 to resolve using
the solver from outside of the container. The reason is that:
a) we don't need to run code from the container for this
b) some containers (like f41) do not have the python dnf available

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 9a71708
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 9a71708

Please sign in to comment.