Skip to content

Commit

Permalink
ibcli: add new --extra-artifacts option with sbom support
Browse files Browse the repository at this point in the history
This commit adds an option --extra-artifacts that can be
used to generate extra artifacts during the build or manifest
generation. Initially supported is `sbom` (but `manifest` is
planned too).

To use it run `--extra-artifacts=sbom` and it will generate
files like `centos-9-qcow2-x86_64.image-os.spdx.json` in
the output directory next to the generate runable artifact.

Closes: osbuild#46
  • Loading branch information
mvo5 committed Jan 16, 2025
1 parent 030ce9b commit a5d4ae4
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 16 deletions.
7 changes: 1 addition & 6 deletions cmd/image-builder/build.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package main

import (
"fmt"
"path/filepath"

"github.com/osbuild/images/pkg/imagefilter"
"github.com/osbuild/images/pkg/osbuild"
)
Expand All @@ -12,9 +9,7 @@ func buildImage(res *imagefilter.Result, osbuildManifest []byte, osbuildStoreDir
// XXX: support output dir via commandline
// XXX2: support output filename via commandline (c.f.
// https://github.com/osbuild/images/pull/1039)
outputDir := "."
buildName := fmt.Sprintf("%s-%s-%s", res.Distro.Name(), res.ImgType.Name(), res.Arch.Name())
jobOutputDir := filepath.Join(outputDir, buildName)
jobOutputDir := outputDirFor(res)

// XXX: support stremaing via images/pkg/osbuild/monitor.go
_, err := osbuild.RunOSBuild(osbuildManifest, osbuildStoreDir, jobOutputDir, res.ImgType.Exports(), nil, nil, false, osStderr)
Expand Down
15 changes: 13 additions & 2 deletions cmd/image-builder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ var (
osStderr io.Writer = os.Stderr
)

// generate the default output directory name for the given image
func outputDirFor(img *imagefilter.Result) string {
return fmt.Sprintf("%s-%s-%s", img.Distro.Name(), img.ImgType.Name(), img.Arch.Name())
}

func cmdListImages(cmd *cobra.Command, args []string) error {
filter, err := cmd.Flags().GetStringArray("filter")
if err != nil {
Expand Down Expand Up @@ -79,6 +84,10 @@ func cmdManifestWrapper(cmd *cobra.Command, args []string, w io.Writer, archChec
if err != nil {
return nil, err
}
extraArtifacts, err := cmd.Flags().GetStringArray("extra-artifacts")
if err != nil {
return nil, err
}
ostreeImgOpts, err := ostreeImageOptions(cmd)
if err != nil {
return nil, err
Expand Down Expand Up @@ -109,8 +118,9 @@ func cmdManifestWrapper(cmd *cobra.Command, args []string, w io.Writer, archChec
}

opts := &manifestOptions{
BlueprintPath: blueprintPath,
Ostree: ostreeImgOpts,
BlueprintPath: blueprintPath,
Ostree: ostreeImgOpts,
ExtraArtifacts: extraArtifacts,
}
err = generateManifest(dataDir, res, w, opts)
return res, err
Expand Down Expand Up @@ -158,6 +168,7 @@ operating sytsems like centos and RHEL with easy customizations support.`,
SilenceErrors: true,
}
rootCmd.PersistentFlags().String("datadir", "", `Override the default data direcotry for e.g. custom repositories/*.json data`)
rootCmd.PersistentFlags().StringArray("extra-artifacts", nil, `Export extra artifacts to the output dir (e.g. "sbom")`)
rootCmd.SetOut(osStdout)
rootCmd.SetErr(osStderr)

Expand Down
37 changes: 33 additions & 4 deletions cmd/image-builder/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package main

import (
"io"
"os"
"path/filepath"
"slices"

"github.com/osbuild/images/pkg/distro"
"github.com/osbuild/images/pkg/imagefilter"
Expand All @@ -12,8 +15,22 @@ import (
)

type manifestOptions struct {
BlueprintPath string
Ostree *ostree.ImageOptions
BlueprintPath string
Ostree *ostree.ImageOptions
ExtraArtifacts []string
}

func sbomWriter(outputDir, filename string, content io.Reader) error {
p := filepath.Join(outputDir, filename)
f, err := os.Create(p)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(f, content); err != nil {
return err
}
return nil
}

func generateManifest(dataDir string, res *imagefilter.Result, output io.Writer, opts *manifestOptions) error {
Expand All @@ -22,12 +39,24 @@ func generateManifest(dataDir string, res *imagefilter.Result, output io.Writer,
return err
}
// XXX: add --rpmmd/cachedir option like bib
mg, err := manifestgen.New(repos, &manifestgen.Options{
manifestGenOpts := &manifestgen.Options{
Output: output,
})
}
if slices.Contains(opts.ExtraArtifacts, "sbom") {
outputDir := outputDirFor(res)
if err := os.MkdirAll(outputDir, 0755); err != nil {
return err
}
manifestGenOpts.SBOMWriter = func(filename string, content io.Reader) error {
return sbomWriter(outputDir, filename, content)
}
}

mg, err := manifestgen.New(repos, manifestGenOpts)
if err != nil {
return err
}

bp, err := blueprintload.Load(opts.BlueprintPath)
if err != nil {
return err
Expand Down
53 changes: 50 additions & 3 deletions internal/manifestgen/manifestgen.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package manifestgen

import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"slices"
"strings"

"github.com/osbuild/images/pkg/blueprint"
Expand Down Expand Up @@ -33,7 +36,9 @@ func defaultDepsolver(cacheDir string, packageSets map[string][]rpmmd.PackageSet
solver := dnfjson.NewSolver(d.ModulePlatformID(), d.Releasever(), arch, d.Name(), cacheDir)
depsolvedSets := make(map[string]dnfjson.DepsolveResult)
for name, pkgSet := range packageSets {
res, err := solver.Depsolve(pkgSet, sbom.StandardTypeNone)
// XXX: is there harm in always generating an sbom?
// (expect for slightly longer runtime?)
res, err := solver.Depsolve(pkgSet, sbom.StandardTypeSpdx)
if err != nil {
return nil, fmt.Errorf("error depsolving: %w", err)
}
Expand Down Expand Up @@ -86,16 +91,29 @@ type (
ContainerResolverFunc func(containerSources map[string][]container.SourceSpec, archName string) (map[string][]container.Spec, error)

CommitResolverFunc func(commitSources map[string][]ostree.SourceSpec) (map[string][]ostree.CommitSpec, error)

SBOMWriterFunc func(filename string, content io.Reader) error
)

// Options contains the optional settings for the manifest generation.
// For unset values defaults will be used.
type Options struct {
Cachedir string
Output io.Writer
Cachedir string
Output io.Writer

// There are two types of sbom outputs, one for the "payload"
// and one for the "buildroot", we allow exporting both here
SbomImageOutput io.Writer
SbomBuildrootOutput io.Writer

Depsolver DepsolveFunc
ContainerResolver ContainerResolverFunc
CommitResolver CommitResolverFunc

// Will be called for each generated SBOM the filename
// contains the suggest filename string and the content
// can be read
SBOMWriter SBOMWriterFunc
}

// Generator can generate an osbuild manifest from a given repository
Expand All @@ -107,6 +125,7 @@ type Generator struct {
depsolver DepsolveFunc
containerResolver ContainerResolverFunc
commitResolver CommitResolverFunc
sbomWriter SBOMWriterFunc

reporegistry *reporegistry.RepoRegistry
}
Expand All @@ -124,6 +143,7 @@ func New(reporegistry *reporegistry.RepoRegistry, opts *Options) (*Generator, er
depsolver: opts.Depsolver,
containerResolver: opts.ContainerResolver,
commitResolver: opts.CommitResolver,
sbomWriter: opts.SBOMWriter,
}
if mg.out == nil {
mg.out = os.Stdout
Expand Down Expand Up @@ -186,5 +206,32 @@ func (mg *Generator) Generate(bp *blueprint.Blueprint, dist distro.Distro, imgTy
}
fmt.Fprintf(mg.out, "%s\n", mf)

if mg.sbomWriter != nil {
// XXX: this is very similar to
// osbuild-composer:jobimpl-osbuild.go, see if code
// can be shared
for plName, depsolvedPipeline := range depsolved {
pipelinePurpose := "unknown"
switch {
case slices.Contains(imgType.PayloadPipelines(), plName):
pipelinePurpose = "image"
case slices.Contains(imgType.BuildPipelines(), plName):
pipelinePurpose = "buildroot"
}
// XXX: sync with image-builder-cli:build.go name generation - can we have a shared helper?
imageName := fmt.Sprintf("%s-%s-%s", dist.Name(), imgType.Name(), a.Name())
sbomDocOutputFilename := fmt.Sprintf("%s.%s-%s.spdx.json", imageName, pipelinePurpose, plName)

var buf bytes.Buffer
enc := json.NewEncoder(&buf)
if err := enc.Encode(depsolvedPipeline.SBOM); err != nil {
return err
}
if err := mg.sbomWriter(sbomDocOutputFilename, &buf); err != nil {
return err
}
}
}

return nil
}
22 changes: 21 additions & 1 deletion test/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,30 @@ def test_container_builds_image(tmp_path, build_container):
build_container,
"build",
"minimal-raw",
"--distro", "centos-9"
"--distro", "centos-9",
])
arch = "x86_64"
assert (output_dir / f"centos-9-minimal-raw-{arch}/xz/disk.raw.xz").exists()
# XXX: ensure no other leftover dirs
dents = os.listdir(output_dir)
assert len(dents) == 1, f"too many dentries in output dir: {dents}"


@pytest.mark.skipif(os.getuid() != 0, reason="needs root")
def test_container_manifest_generates_sbom(tmp_path, build_container):
output_dir = tmp_path / "output"
output_dir.mkdir()
subprocess.check_call([
"podman", "run",
"--privileged",
"-v", f"{output_dir}:/output",
build_container,
"manifest",
"minimal-raw",
"--distro", "centos-9",
"--export-sbom=/output/sbom.json",
])
sbom_json_path = output_dir / "sbom.json"
sbom_json = sbom_json_path.read_text()
# XXX: how to meaningfully inspect this? check for libc?
assert len(sbom_json) > 0

0 comments on commit a5d4ae4

Please sign in to comment.