ibcli: add new --extra-artifacts option with sbom support
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: https://github.com/osbuild/image-builder-cli/issues/46
This commit is contained in:
parent
db7cad2239
commit
d485bc3a44
6 changed files with 178 additions and 17 deletions
|
|
@ -1,9 +1,12 @@
|
|||
package manifestgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/osbuild/images/pkg/blueprint"
|
||||
|
|
@ -35,7 +38,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)
|
||||
}
|
||||
|
|
@ -88,18 +93,31 @@ 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
|
||||
|
||||
RpmDownloader osbuild.RpmDownloader
|
||||
|
||||
// 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
|
||||
|
|
@ -111,6 +129,7 @@ type Generator struct {
|
|||
depsolver DepsolveFunc
|
||||
containerResolver ContainerResolverFunc
|
||||
commitResolver CommitResolverFunc
|
||||
sbomWriter SBOMWriterFunc
|
||||
|
||||
reporegistry *reporegistry.RepoRegistry
|
||||
|
||||
|
|
@ -131,6 +150,7 @@ func New(reporegistry *reporegistry.RepoRegistry, opts *Options) (*Generator, er
|
|||
containerResolver: opts.ContainerResolver,
|
||||
commitResolver: opts.CommitResolver,
|
||||
rpmDownloader: opts.RpmDownloader,
|
||||
sbomWriter: opts.SBOMWriter,
|
||||
}
|
||||
if mg.out == nil {
|
||||
mg.out = os.Stdout
|
||||
|
|
@ -190,5 +210,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ package manifestgen_test
|
|||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -20,6 +23,7 @@ import (
|
|||
"github.com/osbuild/images/pkg/osbuild"
|
||||
"github.com/osbuild/images/pkg/ostree"
|
||||
"github.com/osbuild/images/pkg/rpmmd"
|
||||
"github.com/osbuild/images/pkg/sbom"
|
||||
testrepos "github.com/osbuild/images/test/data/repositories"
|
||||
|
||||
"github.com/osbuild/image-builder-cli/internal/manifestgen"
|
||||
|
|
@ -151,6 +155,11 @@ func fakeDepsolve(cacheDir string, packageSets map[string][]rpmmd.PackageSet, d
|
|||
Id: repoId,
|
||||
Metalink: "https://example.com/metalink",
|
||||
})
|
||||
doc, err := sbom.NewDocument(sbom.StandardTypeSpdx, json.RawMessage(fmt.Sprintf(`{"sbom-for":"%s"}`, name)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolvedSet.SBOM = doc
|
||||
}
|
||||
}
|
||||
depsolvedSets[name] = resolvedSet
|
||||
|
|
@ -238,3 +247,45 @@ func TestManifestGeneratorContainers(t *testing.T) {
|
|||
// container is included
|
||||
assert.Contains(t, osbuildManifest.String(), "resolved-cnt-"+fakeContainerSource)
|
||||
}
|
||||
|
||||
func TestManifestGeneratorDepsolveWithSbomWriter(t *testing.T) {
|
||||
repos, err := testrepos.New()
|
||||
assert.NoError(t, err)
|
||||
fac := distrofactory.NewDefault()
|
||||
|
||||
filter, err := imagefilter.New(fac, repos)
|
||||
assert.NoError(t, err)
|
||||
res, err := filter.Filter("distro:centos-9", "type:qcow2", "arch:x86_64")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(res))
|
||||
|
||||
var osbuildManifest bytes.Buffer
|
||||
generatedSboms := map[string]string{}
|
||||
opts := &manifestgen.Options{
|
||||
Output: &osbuildManifest,
|
||||
Depsolver: fakeDepsolve,
|
||||
CommitResolver: panicCommitResolver,
|
||||
ContainerResolver: panicContainerResolver,
|
||||
|
||||
SBOMWriter: func(filename string, content io.Reader) error {
|
||||
b, err := ioutil.ReadAll(content)
|
||||
assert.NoError(t, err)
|
||||
generatedSboms[filename] = strings.TrimSpace(string(b))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
mg, err := manifestgen.New(repos, opts)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, mg)
|
||||
var bp blueprint.Blueprint
|
||||
err = mg.Generate(&bp, res[0].Distro, res[0].ImgType, res[0].Arch, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, generatedSboms, "centos-9-qcow2-x86_64.buildroot-build.spdx.json")
|
||||
assert.Contains(t, generatedSboms, "centos-9-qcow2-x86_64.image-os.spdx.json")
|
||||
expected := map[string]string{
|
||||
"centos-9-qcow2-x86_64.buildroot-build.spdx.json": `{"DocType":"spdx","Document":{"sbom-for":"build"}}`,
|
||||
"centos-9-qcow2-x86_64.image-os.spdx.json": `{"DocType":"spdx","Document":{"sbom-for":"os"}}`,
|
||||
}
|
||||
assert.Equal(t, expected, generatedSboms)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue