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

manifest,images: add new manifest.NewBuildFromContainerSpec and use in new BootcDiskImage (HMS-3318) #354

Merged
merged 3 commits into from
Jan 10, 2024
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
72 changes: 72 additions & 0 deletions pkg/image/bootc_disk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package image

import (
"fmt"
"math/rand"

"github.com/osbuild/images/pkg/artifact"
"github.com/osbuild/images/pkg/container"
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/images/pkg/platform"
"github.com/osbuild/images/pkg/runner"
)

type BootcDiskImage struct {
*OSTreeDiskImage
}

func NewBootcDiskImage(container container.SourceSpec) *BootcDiskImage {
// XXX: hardcoded for now
ref := "ostree/1/1/0"

return &BootcDiskImage{
&OSTreeDiskImage{
Base: NewBase("bootc-raw-image"),
ContainerSource: &container,
Ref: ref,
OSName: "default",
},
}
}

func (img *BootcDiskImage) InstantiateManifestFromContainers(m *manifest.Manifest,
containers []container.SourceSpec,
runner runner.Runner,
rng *rand.Rand) (*artifact.Artifact, error) {

buildPipeline := manifest.NewBuildFromContainer(m, runner, containers, &manifest.BuildOptions{ContainerBuildable: true})
buildPipeline.Checkpoint()

// don't support compressing non-raw images
imgFormat := img.Platform.GetImageFormat()
if imgFormat == platform.FORMAT_UNSET {
// treat unset as raw for this check
imgFormat = platform.FORMAT_RAW
}
if imgFormat != platform.FORMAT_RAW && img.Compression != "" {
panic(fmt.Sprintf("no compression is allowed with %q format for %q", imgFormat, img.name))
}

baseImage := baseRawOstreeImage(img.OSTreeDiskImage, buildPipeline)
switch imgFormat {
case platform.FORMAT_QCOW2:
// TODO: create new build pipeline here that uses "bib" itself
// as the buildroot to get access to tooling like "qemu-img"
qcow2Pipeline := manifest.NewQCOW2(buildPipeline, baseImage)
qcow2Pipeline.Compat = img.Platform.GetQCOW2Compat()
qcow2Pipeline.SetFilename(img.Filename)
return qcow2Pipeline.Export(), nil
}

switch img.Compression {
case "xz":
compressedImage := manifest.NewXZ(buildPipeline, baseImage)
compressedImage.SetFilename(img.Filename)
return compressedImage.Export(), nil
case "":
baseImage.SetFilename(img.Filename)
return baseImage.Export(), nil
default:
panic(fmt.Sprintf("unsupported compression type %q on %q", img.Compression, img.name))
}
}
22 changes: 22 additions & 0 deletions pkg/image/bootc_disk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package image_test

import (
"testing"

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

"github.com/osbuild/images/pkg/container"
"github.com/osbuild/images/pkg/image"
)

func TestBootcDiskImageNew(t *testing.T) {
containerSource := container.SourceSpec{
Source: "source-spec",
Name: "name",
}

img := image.NewBootcDiskImage(containerSource)
require.NotNil(t, img)
assert.Equal(t, img.OSTreeDiskImage.Base.Name(), "bootc-raw-image")
}
2 changes: 1 addition & 1 deletion pkg/image/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/osbuild/images/pkg/runner"
)

func MockManifestNewBuild(new func(m *manifest.Manifest, runner runner.Runner, repos []rpmmd.RepoConfig, opts *manifest.BuildOptions) *manifest.Build) (restore func()) {
func MockManifestNewBuild(new func(m *manifest.Manifest, runner runner.Runner, repos []rpmmd.RepoConfig, opts *manifest.BuildOptions) manifest.Build) (restore func()) {
saved := manifestNewBuild
manifestNewBuild = new
return func() {
Expand Down
9 changes: 5 additions & 4 deletions pkg/image/ostree_disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func NewOSTreeDiskImageFromContainer(container container.SourceSpec, ref string)
}
}

func baseRawOstreeImage(img *OSTreeDiskImage, buildPipeline *manifest.Build) *manifest.RawOSTreeImage {
func baseRawOstreeImage(img *OSTreeDiskImage, buildPipeline manifest.Build) *manifest.RawOSTreeImage {
var osPipeline *manifest.OSTreeDeployment
switch {
case img.CommitSource != nil:
Expand All @@ -100,9 +100,10 @@ func baseRawOstreeImage(img *OSTreeDiskImage, buildPipeline *manifest.Build) *ma
osPipeline.LockRoot = img.LockRoot

// other image types (e.g. live) pass the workload to the pipeline.
osPipeline.EnabledServices = img.Workload.GetServices()
osPipeline.DisabledServices = img.Workload.GetDisabledServices()

if img.Workload != nil {
osPipeline.EnabledServices = img.Workload.GetServices()
osPipeline.DisabledServices = img.Workload.GetDisabledServices()
}
return manifest.NewRawOStreeImage(buildPipeline, osPipeline, img.Platform)
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/image/ostree_disk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestOSTreeDiskImageManifestSetsContainerBuildable(t *testing.T) {
}

var buildOpts []*manifest.BuildOptions
restore := image.MockManifestNewBuild(func(m *manifest.Manifest, r runner.Runner, repos []rpmmd.RepoConfig, opts *manifest.BuildOptions) *manifest.Build {
restore := image.MockManifestNewBuild(func(m *manifest.Manifest, r runner.Runner, repos []rpmmd.RepoConfig, opts *manifest.BuildOptions) manifest.Build {
buildOpts = append(buildOpts, opts)
return manifest.NewBuild(m, r, repos, opts)
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/manifest/anaconda_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ type AnacondaInstaller struct {
}

func NewAnacondaInstaller(installerType AnacondaInstallerType,
buildPipeline *Build,
buildPipeline Build,
platform platform.Platform,
repos []rpmmd.RepoConfig,
kernelName,
Expand Down
2 changes: 1 addition & 1 deletion pkg/manifest/anaconda_installer_iso_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ type AnacondaInstallerISOTree struct {
Files []*fsnode.File
}

func NewAnacondaInstallerISOTree(buildPipeline *Build, anacondaPipeline *AnacondaInstaller, rootfsPipeline *ISORootfsImg, bootTreePipeline *EFIBootTree) *AnacondaInstallerISOTree {
func NewAnacondaInstallerISOTree(buildPipeline Build, anacondaPipeline *AnacondaInstaller, rootfsPipeline *ISORootfsImg, bootTreePipeline *EFIBootTree) *AnacondaInstallerISOTree {

// the three pipelines should all belong to the same manifest
if anacondaPipeline.Manifest() != rootfsPipeline.Manifest() ||
Expand Down
112 changes: 102 additions & 10 deletions pkg/manifest/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,16 @@ import (
// is not predictable nor reproducible. For the purposes of building the
// build pipeline, we do use the build host's filesystem, this means we should
// make minimal assumptions about what's available there.
type Build struct {

type Build interface {
Name() string
Checkpoint()
Manifest() *Manifest

addDependent(dep Pipeline)
}

type BuildrootFromPackages struct {
Base

runner runner.Runner
Expand All @@ -34,13 +43,13 @@ type BuildOptions struct {

// NewBuild creates a new build pipeline from the repositories in repos
// and the specified packages.
func NewBuild(m *Manifest, runner runner.Runner, repos []rpmmd.RepoConfig, opts *BuildOptions) *Build {
func NewBuild(m *Manifest, runner runner.Runner, repos []rpmmd.RepoConfig, opts *BuildOptions) Build {
if opts == nil {
opts = &BuildOptions{}
}

name := "build"
pipeline := &Build{
pipeline := &BuildrootFromPackages{
Base: NewBase(name, nil),
runner: runner,
dependents: make([]Pipeline, 0),
Expand All @@ -51,7 +60,7 @@ func NewBuild(m *Manifest, runner runner.Runner, repos []rpmmd.RepoConfig, opts
return pipeline
}

func (p *Build) addDependent(dep Pipeline) {
func (p *BuildrootFromPackages) addDependent(dep Pipeline) {
p.dependents = append(p.dependents, dep)
man := p.Manifest()
if man == nil {
Expand All @@ -60,7 +69,7 @@ func (p *Build) addDependent(dep Pipeline) {
man.addPipeline(dep)
}

func (p *Build) getPackageSetChain(distro Distro) []rpmmd.PackageSet {
func (p *BuildrootFromPackages) getPackageSetChain(distro Distro) []rpmmd.PackageSet {
// TODO: make the /usr/bin/cp dependency conditional
// TODO: make the /usr/bin/xz dependency conditional
packages := []string{
Expand All @@ -84,25 +93,25 @@ func (p *Build) getPackageSetChain(distro Distro) []rpmmd.PackageSet {
}
}

func (p *Build) getPackageSpecs() []rpmmd.PackageSpec {
func (p *BuildrootFromPackages) getPackageSpecs() []rpmmd.PackageSpec {
return p.packageSpecs
}

func (p *Build) serializeStart(packages []rpmmd.PackageSpec, _ []container.Spec, _ []ostree.CommitSpec) {
func (p *BuildrootFromPackages) serializeStart(packages []rpmmd.PackageSpec, _ []container.Spec, _ []ostree.CommitSpec) {
if len(p.packageSpecs) > 0 {
panic("double call to serializeStart()")
}
p.packageSpecs = packages
}

func (p *Build) serializeEnd() {
func (p *BuildrootFromPackages) serializeEnd() {
if len(p.packageSpecs) == 0 {
panic("serializeEnd() call when serialization not in progress")
}
p.packageSpecs = nil
}

func (p *Build) serialize() osbuild.Pipeline {
func (p *BuildrootFromPackages) serialize() osbuild.Pipeline {
if len(p.packageSpecs) == 0 {
panic("serialization not started")
}
Expand All @@ -121,7 +130,7 @@ func (p *Build) serialize() osbuild.Pipeline {

// Returns a map of paths to labels for the SELinux stage based on specific
// packages found in the pipeline.
func (p *Build) getSELinuxLabels() map[string]string {
func (p *BuildrootFromPackages) getSELinuxLabels() map[string]string {
labels := make(map[string]string)
for _, pkg := range p.getPackageSpecs() {
switch pkg.Name {
Expand All @@ -137,3 +146,86 @@ func (p *Build) getSELinuxLabels() map[string]string {
}
return labels
}

type BuildrootFromContainer struct {
Base

runner runner.Runner
dependents []Pipeline

containers []container.SourceSpec
containerSpecs []container.Spec

containerBuildable bool
}

// NewBuildFromContainer creates a new build pipeline from the given
// containers specs
func NewBuildFromContainer(m *Manifest, runner runner.Runner, containerSources []container.SourceSpec, opts *BuildOptions) Build {
if opts == nil {
opts = &BuildOptions{}
}

name := "build"
pipeline := &BuildrootFromContainer{
Base: NewBase(name, nil),
runner: runner,
dependents: make([]Pipeline, 0),
containers: containerSources,

containerBuildable: opts.ContainerBuildable,
}
m.addPipeline(pipeline)
return pipeline
}

func (p *BuildrootFromContainer) addDependent(dep Pipeline) {
p.dependents = append(p.dependents, dep)
man := p.Manifest()
if man == nil {
panic("cannot add build dependent without a manifest")
}
man.addPipeline(dep)
}

func (p *BuildrootFromContainer) getContainerSources() []container.SourceSpec {
return p.containers
}

func (p *BuildrootFromContainer) serializeStart(_ []rpmmd.PackageSpec, containerSpecs []container.Spec, _ []ostree.CommitSpec) {
if len(p.containerSpecs) > 0 {
panic("double call to serializeStart()")
}
p.containerSpecs = containerSpecs
}

func (p *BuildrootFromContainer) serializeEnd() {
if len(p.containerSpecs) == 0 {
panic("serializeEnd() call when serialization not in progress")
}
p.containerSpecs = nil
}

func (p *BuildrootFromContainer) serialize() osbuild.Pipeline {
if len(p.containerSpecs) == 0 {
panic("serialization not started")
}
pipeline := p.Base.serialize()
pipeline.Runner = p.runner.String()

stage, err := osbuild.NewContainerDeployStage(osbuild.NewContainersInputForSources(p.containerSpecs))
if err != nil {
panic(err)
}
pipeline.AddStage(stage)
pipeline.AddStage(osbuild.NewSELinuxStage(
&osbuild.SELinuxStageOptions{
FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
Labels: map[string]string{
"/usr/bin/ostree": "system_u:object_r:install_exec_t:s0",
},
},
))

return pipeline
}
38 changes: 37 additions & 1 deletion pkg/manifest/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package manifest
import (
"testing"

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

"github.com/osbuild/images/pkg/container"
"github.com/osbuild/images/pkg/osbuild"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/runner"
)
Expand All @@ -14,7 +17,8 @@ func TestBuildContainerBuildableNo(t *testing.T) {
mf := New()
runner := &runner.Fedora{Version: 39}

build := NewBuild(&mf, runner, repos, nil)
buildIf := NewBuild(&mf, runner, repos, nil)
build := buildIf.(*BuildrootFromPackages)
require.NotNil(t, build)

for _, tc := range []struct {
Expand Down Expand Up @@ -82,3 +86,35 @@ func TestBuildContainerBuildableNo(t *testing.T) {
require.Equal(t, labels, tc.expectedSELinuxLabels)
}
}

func TestNewBuildFromContainerSpecs(t *testing.T) {
containers := []container.SourceSpec{
{
Name: "Build container",
Source: "ghcr.io/ondrejbudai/booc:fedora",
},
}
mf := New()
runner := &runner.Fedora{Version: 39}

buildIf := NewBuildFromContainer(&mf, runner, containers, nil)
require.NotNil(t, buildIf)
build := buildIf.(*BuildrootFromContainer)

fakeContainerSpecs := []container.Spec{
{
ImageID: "id-0",
Source: "registry.example.org/reg/img",
},
}
build.serializeStart(nil, fakeContainerSpecs, nil)
osbuildPipeline := build.serialize()
require.Len(t, osbuildPipeline.Stages, 2)
assert.Equal(t, osbuildPipeline.Stages[0].Type, "org.osbuild.container-deploy")
// one container src input is added
assert.Equal(t, len(osbuildPipeline.Stages[0].Inputs.(osbuild.ContainerDeployInputs).Images.References), 1)

assert.Equal(t, osbuildPipeline.Stages[1].Type, "org.osbuild.selinux")
assert.Equal(t, len(osbuildPipeline.Stages[1].Options.(*osbuild.SELinuxStageOptions).Labels), 1)
assert.Equal(t, osbuildPipeline.Stages[1].Options.(*osbuild.SELinuxStageOptions).Labels["/usr/bin/ostree"], "system_u:object_r:install_exec_t:s0")
}
2 changes: 1 addition & 1 deletion pkg/manifest/coi_iso_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type CoreOSISOTree struct {
}

func NewCoreOSISOTree(
buildPipeline *Build,
buildPipeline Build,
payloadPipeline *XZ,
coiPipeline *CoreOSInstaller,
bootTreePipeline *EFIBootTree) *CoreOSISOTree {
Expand Down
Loading
Loading