Skip to content
Open
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
2 changes: 2 additions & 0 deletions pkg/image/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func (img *DiskImage) InstantiateManifest(m *manifest.Manifest,

for _, sysext := range img.Sysexts {
var depsolveRef manifest.Pipeline
// Standalone extensions resolve and install their full package set
// into a fresh tree, without copying the OS tree or its RPM database.
if !sysext.Standalone {
depsolveRef = osPipeline
}
Expand Down
94 changes: 94 additions & 0 deletions pkg/image/disk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package image_test

import (
"encoding/json"
"testing"

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

"github.com/osbuild/image-builder/pkg/arch"
"github.com/osbuild/image-builder/pkg/depsolvednf"
"github.com/osbuild/image-builder/pkg/disk"
"github.com/osbuild/image-builder/pkg/image"
"github.com/osbuild/image-builder/pkg/manifest"
"github.com/osbuild/image-builder/pkg/osbuild"
"github.com/osbuild/image-builder/pkg/platform"
"github.com/osbuild/image-builder/pkg/rpmmd"
"github.com/osbuild/image-builder/pkg/runner"
)

func TestDiskImageSysextStandalone(t *testing.T) {
for _, standalone := range []bool{false, true} {
name := "reuse OS"
if standalone {
name = "standalone"
}
t.Run(name, func(t *testing.T) {
img := image.NewDiskImage(&platform.Data{Arch: arch.ARCH_X86_64, ImageFormat: platform.FORMAT_RAW}, "disk.raw")
img.DiskCustomizations = manifest.NewDiskCustomizations()
img.PartitionTable = &disk.PartitionTable{
Type: disk.PT_GPT, Size: 1024 * 1024 * 1024,
Partitions: []disk.Partition{{
Start: 1024 * 1024, Size: 1023 * 1024 * 1024,
Payload: &disk.Filesystem{Type: "ext4", Mountpoint: "/", UUID: "11111111-1111-1111-1111-111111111111"},
}},
}
img.OSCustomizations.BasePackages = []string{"kernel"}
img.OSCustomizations.BaseRPMOptions = osbuild.RPMStageOptions{GPGKeysFromTree: []string{"/base-key"}}
img.Sysexts = []image.SysextConfig{{
Name: "test", Format: "erofs", Standalone: standalone,
PackageSet: rpmmd.PackageSet{Include: []string{"extension"}},
}}
mf := manifest.New()
_, err := img.InstantiateManifest(&mf, nil, &runner.Fedora{}, nil)
require.NoError(t, err)
chains, err := mf.GetPackageSetChains()
require.NoError(t, err)
if standalone {
require.Len(t, chains["sysext-test-tree"], 1)
assert.Equal(t, []string{"extension"}, chains["sysext-test-tree"][0].Include)
} else {
require.Len(t, chains["sysext-test-tree"], len(chains["os"])+1)
assert.Equal(t, chains["os"], chains["sysext-test-tree"][:len(chains["os"])])
assert.Equal(t, []string{"extension"}, chains["sysext-test-tree"][len(chains["os"])].Include)
}

depsolved := mockPackageSets()
installed := depsolved["os"].Transactions[0][0]
installed.Files = []string{"/base-key"}
depsolved["os"] = depsolvednf.DepsolveResult{Transactions: depsolvednf.TransactionList{{installed}}}
added := installed
added.Name = "extension"
added.Checksum.Value = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
added.Files = nil
transactions := depsolvednf.TransactionList{{installed}, {added}}
if standalone {
transactions = depsolvednf.TransactionList{{installed, added}}
}
depsolved["sysext-test-tree"] = depsolvednf.DepsolveResult{Transactions: transactions}
data, err := mf.Serialize(depsolved, nil, nil, nil, nil)
require.NoError(t, err)
pipeline := findPipelineFromOsbuildManifest(t, data, "sysext-test-tree")
require.NotNil(t, pipeline)
var want []*osbuild.Stage
packages := rpmmd.PackageList{added}
opts := &osbuild.RPMStageOptions{}
if standalone {
packages = rpmmd.PackageList{installed, added}
opts.GPGKeysFromTree = []string{"/base-key"}
} else {
want = append(want, osbuild.NewCopyStageSimple(
&osbuild.CopyStageOptions{Paths: []osbuild.CopyStagePath{{From: "input://tree/", To: "tree:///"}}},
osbuild.NewPipelineTreeInputs("tree", "os"),
))
}
want = append(want, osbuild.NewRPMStage(opts, osbuild.NewRpmStageSourceFilesInputs(packages)))
wantJSON, err := json.Marshal(want)
require.NoError(t, err)
gotJSON, err := json.Marshal(pipeline["stages"])
require.NoError(t, err)
assert.JSONEq(t, string(wantJSON), string(gotJSON))
})
}
}
56 changes: 42 additions & 14 deletions pkg/manifest/sysext.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package manifest
import (
"errors"
"fmt"
"slices"

"github.com/osbuild/image-builder/pkg/depsolvednf"
"github.com/osbuild/image-builder/pkg/osbuild"
Expand Down Expand Up @@ -32,9 +33,8 @@ type SysextTree struct {

platform platform.Platform

// When non-nil, this pipeline's package sets are merged with the
// reference's sets so that shared dependencies resolve to the same
// versions as the base OS.
// When non-nil, copy this tree and install only packages not already
// present in it. Its package set chain precedes the extension transaction.
depsolveReferencePipeline Pipeline

Customizations SysextCustomizations
Expand Down Expand Up @@ -78,8 +78,34 @@ func (p *SysextTree) serialize() (osbuild.Pipeline, error) {
return osbuild.Pipeline{}, err
}

// Do not copy the reference pipeline tree here; it causes issues with the RPM database and package selections.
rpmStages, err := osbuild.GenRPMStagesFromTransactions(p.depsolveResult.Transactions, p.Customizations.BaseRPMOptions)
transactions := p.depsolveResult.Transactions
baseRPMOptions := p.Customizations.BaseRPMOptions.Clone()
if ref := p.depsolveReferencePipeline; ref != nil {
// The reference transactions are already installed in the copied tree.
// The final transaction contains only the extension's additional packages.
if len(transactions) > 0 {
transactions = transactions[len(transactions)-1:]
}
pipeline.AddStage(osbuild.NewCopyStageSimple(
&osbuild.CopyStageOptions{Paths: []osbuild.CopyStagePath{{
From: "input://tree/",
To: "tree:///",
}}},
osbuild.NewPipelineTreeInputs("tree", ref.Name()),
))

// Keys provided by the reference packages have already been imported
// into the copied RPM database. Only import keys from new packages.
if baseRPMOptions != nil {
installed := ref.getPackageSpecs()
inheritedKeys := (depsolvednf.TransactionList{installed}).GetFilesTransactionInfo(baseRPMOptions.GPGKeysFromTree)
baseRPMOptions.GPGKeysFromTree = slices.DeleteFunc(baseRPMOptions.GPGKeysFromTree, func(path string) bool {
_, inherited := inheritedKeys[path]
return inherited
})
}
}
rpmStages, err := osbuild.GenRPMStagesFromTransactions(transactions, baseRPMOptions)
if err != nil {
return osbuild.Pipeline{}, err
}
Expand All @@ -94,20 +120,22 @@ func (p *SysextTree) getPackageSetChain(d Distro) ([]rpmmd.PackageSet, error) {
return nil, nil
}

pkgSet.Repositories = slices.Concat(pkgSet.Repositories, p.depsolveRepos)
pkgSet.InstallWeakDeps = false
var chain []rpmmd.PackageSet
if p.depsolveReferencePipeline != nil {
refChain, err := p.depsolveReferencePipeline.getPackageSetChain(d)
var err error
chain, err = p.depsolveReferencePipeline.getPackageSetChain(d)
if err != nil {
return nil, fmt.Errorf("getting reference package set chain: %w", err)
}
for _, refSet := range refChain {
pkgSet = pkgSet.Append(refSet)
pkgSet.Repositories = append(pkgSet.Repositories, refSet.Repositories...)
if len(chain) > 0 {
// Each transaction must retain its predecessor's repositories in order.
pkgSet.Repositories = slices.Concat(chain[len(chain)-1].Repositories, pkgSet.Repositories)
}
}

pkgSet.Repositories = append(pkgSet.Repositories, p.depsolveRepos...)
pkgSet.InstallWeakDeps = false
return []rpmmd.PackageSet{pkgSet}, nil
return append(chain, pkgSet), nil
}

// SysextPrep computes the file-level difference between a reference tree
Expand Down Expand Up @@ -186,8 +214,8 @@ type SysextPipelines struct {
// NewSysextPipelines creates the full Tree -> Prep chain for a named sysext.
// filesystemReferencePipeline is used by the Prep stage to compute the
// tree-delta and is always required. depsolveReferencePipeline is optional:
// when non-nil, its package sets are merged into the tree's depsolve so that
// shared dependencies resolve to the same versions as the base OS.
// when non-nil, its tree is copied and its package set chain precedes the
// extension's own transaction. Only the final transaction is installed.
func NewSysextPipelines(buildPipeline Build, platform platform.Platform, repos []rpmmd.RepoConfig, filesystemReferencePipeline Pipeline, depsolveReferencePipeline Pipeline, name string) SysextPipelines {
tree := NewSysextTree(buildPipeline, platform, repos, name)
tree.depsolveReferencePipeline = depsolveReferencePipeline
Expand Down
Loading
Loading