Skip to content

Commit

Permalink
dnfjson/Depsolve: support requesting SBOM document
Browse files Browse the repository at this point in the history
Extend the `Solver.Depsolve()` method to allow requesting SBOM document
for the depsolved transaction. In case an SBOM document is requested, a
pointer to `sbom.Document` instance is returned with the depsolve
result.

Signed-off-by: Tomáš Hozza <[email protected]>
  • Loading branch information
thozza committed Sep 13, 2024
1 parent 32fc171 commit c014c10
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 18 deletions.
3 changes: 2 additions & 1 deletion cmd/build/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/osbuild/images/pkg/reporegistry"
"github.com/osbuild/images/pkg/rhsm/facts"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/sbom"
)

func makeManifest(
Expand Down Expand Up @@ -133,7 +134,7 @@ func depsolve(cacheDir string, packageSets map[string][]rpmmd.PackageSet, d dist
depsolvedSets := make(map[string][]rpmmd.PackageSpec)
repoSets := make(map[string][]rpmmd.RepoConfig)
for name, pkgSet := range packageSets {
pkgs, repos, err := solver.Depsolve(pkgSet)
pkgs, repos, _, err := solver.Depsolve(pkgSet, sbom.StandardTypeNone)
if err != nil {
return nil, nil, err
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/gen-manifests/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/osbuild/images/pkg/reporegistry"
"github.com/osbuild/images/pkg/rhsm/facts"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/sbom"
)

type buildRequest struct {
Expand Down Expand Up @@ -357,7 +358,7 @@ func depsolve(cacheDir string, packageSets map[string][]rpmmd.PackageSet, d dist
depsolvedSets := make(map[string][]rpmmd.PackageSpec)
repoSets := make(map[string][]rpmmd.RepoConfig)
for name, pkgSet := range packageSets {
packages, repos, err := solver.Depsolve(pkgSet)
packages, repos, _, err := solver.Depsolve(pkgSet, sbom.StandardTypeNone)
if err != nil {
return nil, nil, err
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/osbuild-playground/playground.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/osbuild/images/pkg/osbuild"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/runner"
"github.com/osbuild/images/pkg/sbom"
)

func RunPlayground(img image.ImageKind, d distro.Distro, arch distro.Arch, repos map[string][]rpmmd.RepoConfig, state_dir string) {
Expand All @@ -36,7 +37,7 @@ func RunPlayground(img image.ImageKind, d distro.Distro, arch distro.Arch, repos

packageSpecs := make(map[string][]rpmmd.PackageSpec)
for name, chain := range manifest.GetPackageSetChains() {
packages, _, err := solver.Depsolve(chain)
packages, _, _, err := solver.Depsolve(chain, sbom.StandardTypeNone)
if err != nil {
panic(fmt.Sprintf("failed to depsolve for pipeline %s: %s\n", name, err.Error()))
}
Expand Down
38 changes: 31 additions & 7 deletions pkg/dnfjson/dnfjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/osbuild/images/internal/common"
"github.com/osbuild/images/pkg/rhsm"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/sbom"
)

// BaseSolver defines the basic solver configuration without platform
Expand Down Expand Up @@ -193,10 +194,10 @@ func (s *Solver) SetProxy(proxy string) error {
// their associated repositories. Each package set is depsolved as a separate
// transactions in a chain. It returns a list of all packages (with solved
// dependencies) that will be installed into the system.
func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet) ([]rpmmd.PackageSpec, []rpmmd.RepoConfig, error) {
req, rhsmMap, err := s.makeDepsolveRequest(pkgSets)
func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet, sbomType sbom.StandardType) ([]rpmmd.PackageSpec, []rpmmd.RepoConfig, *sbom.Document, error) {
req, rhsmMap, err := s.makeDepsolveRequest(pkgSets, sbomType)
if err != nil {
return nil, nil, fmt.Errorf("makeDepsolveRequest failed: %w", err)
return nil, nil, nil, fmt.Errorf("makeDepsolveRequest failed: %w", err)
}

// get non-exclusive read lock
Expand All @@ -205,7 +206,7 @@ func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet) ([]rpmmd.PackageSpec, []rp

output, err := run(s.dnfJsonCmd, req)
if err != nil {
return nil, nil, fmt.Errorf("running osbuild-depsolve-dnf failed:\n%w", err)
return nil, nil, nil, fmt.Errorf("running osbuild-depsolve-dnf failed:\n%w", err)
}
// touch repos to now
now := time.Now().Local()
Expand All @@ -219,11 +220,20 @@ func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet) ([]rpmmd.PackageSpec, []rp
dec := json.NewDecoder(bytes.NewReader(output))
dec.DisallowUnknownFields()
if err := dec.Decode(&result); err != nil {
return nil, nil, fmt.Errorf("decoding depsolve result failed: %w", err)
return nil, nil, nil, fmt.Errorf("decoding depsolve result failed: %w", err)
}

packages, repos := result.toRPMMD(rhsmMap)
return packages, repos, nil

var sbomDoc *sbom.Document
if sbomType != sbom.StandardTypeNone {
sbomDoc, err = sbom.NewDocument(sbomType, result.Sbom)
if err != nil {
return nil, nil, nil, fmt.Errorf("creating SBOM document failed: %w", err)
}
}

return packages, repos, sbomDoc, nil
}

// FetchMetadata returns the list of all the available packages in repos and
Expand Down Expand Up @@ -411,7 +421,7 @@ func (r *repoConfig) Hash() string {
// NOTE: Due to implementation limitations of DNF and dnf-json, each package set
// in the chain must use all of the repositories used by its predecessor.
// An error is returned if this requirement is not met.
func (s *Solver) makeDepsolveRequest(pkgSets []rpmmd.PackageSet) (*Request, map[string]bool, error) {
func (s *Solver) makeDepsolveRequest(pkgSets []rpmmd.PackageSet, sbomType sbom.StandardType) (*Request, map[string]bool, error) {
// dedupe repository configurations but maintain order
// the order in which repositories are added to the request affects the
// order of the dependencies in the result
Expand Down Expand Up @@ -478,6 +488,10 @@ func (s *Solver) makeDepsolveRequest(pkgSets []rpmmd.PackageSet) (*Request, map[
Arguments: args,
}

if sbomType != sbom.StandardTypeNone {
req.Arguments.Sbom = &sbomRequest{Type: sbomType.String()}
}

return &req, rhsmMap, nil
}

Expand Down Expand Up @@ -642,6 +656,10 @@ func (r *Request) Hash() string {
return fmt.Sprintf("%x", h.Sum(nil))
}

type sbomRequest struct {
Type string `json:"type"`
}

// arguments for a dnf-json request
type arguments struct {
// Repositories to use for depsolving
Expand All @@ -659,6 +677,9 @@ type arguments struct {

// Optional metadata to download for the repositories
OptionalMetadata []string `json:"optional-metadata,omitempty"`

// Optionally request an SBOM from depsolving
Sbom *sbomRequest `json:"sbom,omitempty"`
}

type searchArgs struct {
Expand Down Expand Up @@ -693,6 +714,9 @@ type depsolveResult struct {

// (optional) contains the solver used, e.g. "dnf5"
Solver string `json:"solver"`

// (optional) contains the SBOM for the depsolved transaction
Sbom json.RawMessage `json:"sbom"`
}

// Package specification
Expand Down
83 changes: 75 additions & 8 deletions pkg/dnfjson/dnfjson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/osbuild/images/internal/common"
"github.com/osbuild/images/internal/mocks/rpmrepo"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/sbom"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -33,6 +34,7 @@ func TestDepsolver(t *testing.T) {
packages [][]string
repos []rpmmd.RepoConfig
rootDir string
sbomType sbom.StandardType
err bool
expMsg string
}
Expand Down Expand Up @@ -103,6 +105,13 @@ func TestDepsolver(t *testing.T) {
err: true,
expMsg: "this-package-does-not-exist",
},
"chain-with-sbom": {
// chain depsolve of the same packages in order should produce the same result (at least in this case)
packages: [][]string{{"kernel"}, {"vim-minimal", "tmux", "zsh"}},
repos: []rpmmd.RepoConfig{s.RepoConfig},
sbomType: sbom.StandardTypeSpdx,
err: false,
},
}

for tcName := range testCases {
Expand All @@ -115,17 +124,22 @@ func TestDepsolver(t *testing.T) {
}

solver.SetRootDir(tc.rootDir)
deps, _, err := solver.Depsolve(pkgsets)
deps, _, sbomDoc, err := solver.Depsolve(pkgsets, tc.sbomType)
if tc.err {
assert.Error(err)
assert.Contains(err.Error(), tc.expMsg)
return
} else {
assert.Nil(err)
}

if err == nil {
exp := expectedResult(s.RepoConfig)
assert.Equal(exp, deps)
assert.Equal(expectedResult(s.RepoConfig), deps)

if tc.sbomType != sbom.StandardTypeNone {
assert.NotNil(sbomDoc)
assert.Equal(sbom.StandardTypeSpdx, sbomDoc.DocType)
} else {
assert.Nil(sbomDoc)
}
})
}
Expand Down Expand Up @@ -165,6 +179,7 @@ func TestMakeDepsolveRequest(t *testing.T) {
packageSets []rpmmd.PackageSet
args []transactionArgs
wantRepos []repoConfig
withSbom bool
err bool
}{
// single transaction
Expand Down Expand Up @@ -522,11 +537,57 @@ func TestMakeDepsolveRequest(t *testing.T) {
},
},
},
// 2 transactions + wantSbom flag
{
packageSets: []rpmmd.PackageSet{
{
Include: []string{"pkg1"},
Exclude: []string{"pkg2"},
Repositories: []rpmmd.RepoConfig{baseOS, appstream},
InstallWeakDeps: true,
},
{
Include: []string{"pkg3"},
Repositories: []rpmmd.RepoConfig{baseOS, appstream},
},
},
args: []transactionArgs{
{
PackageSpecs: []string{"pkg1"},
ExcludeSpecs: []string{"pkg2"},
RepoIDs: []string{baseOS.Hash(), appstream.Hash()},
InstallWeakDeps: true,
},
{
PackageSpecs: []string{"pkg3"},
RepoIDs: []string{baseOS.Hash(), appstream.Hash()},
},
},
wantRepos: []repoConfig{
{
ID: baseOS.Hash(),
Name: "baseos",
BaseURLs: []string{"https://example.org/baseos"},
repoHash: "f177f580cf201f52d1c62968d5b85cddae3e06cb9d5058987c07de1dbd769d4b",
},
{
ID: appstream.Hash(),
Name: "appstream",
BaseURLs: []string{"https://example.org/appstream"},
repoHash: "5c4a57bbb1b6a1886291819f2ceb25eb7c92e80065bc986a75c5837cf3d55a1f",
},
},
withSbom: true,
},
}
solver := NewSolver("", "", "", "", "")
for idx, tt := range tests {
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
req, _, err := solver.makeDepsolveRequest(tt.packageSets)
var sbomType sbom.StandardType
if tt.withSbom {
sbomType = sbom.StandardTypeSpdx
}
req, _, err := solver.makeDepsolveRequest(tt.packageSets, sbomType)
if tt.err {
assert.NotNilf(t, err, "expected an error, but got 'nil' instead")
assert.Nilf(t, req, "got non-nill request, but expected an error")
Expand All @@ -536,6 +597,12 @@ func TestMakeDepsolveRequest(t *testing.T) {

assert.Equal(t, tt.args, req.Arguments.Transactions)
assert.Equal(t, tt.wantRepos, req.Arguments.Repos)
if tt.withSbom {
assert.NotNil(t, req.Arguments.Sbom)
assert.Equal(t, req.Arguments.Sbom.Type, sbom.StandardTypeSpdx.String())
} else {
assert.Nil(t, req.Arguments.Sbom)
}
}
})
}
Expand Down Expand Up @@ -712,13 +779,13 @@ func TestErrorRepoInfo(t *testing.T) {
solver := NewSolver("platform:f38", "38", "x86_64", "fedora-38", "/tmp/cache")
for idx, tc := range testCases {
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
_, _, err := solver.Depsolve([]rpmmd.PackageSet{
_, _, _, err := solver.Depsolve([]rpmmd.PackageSet{
{
Include: []string{"osbuild"},
Exclude: nil,
Repositories: []rpmmd.RepoConfig{tc.repo},
},
})
}, sbom.StandardTypeNone)
assert.Error(err)
assert.Contains(err.Error(), tc.expMsg)
})
Expand Down Expand Up @@ -801,7 +868,7 @@ echo '{"solver": "zypper"}'

solver := NewSolver("platform:f38", "38", "x86_64", "fedora-38", "/tmp/cache")
solver.dnfJsonCmd = []string{fakeSolverPath}
pkgSpec, repoCfg, err := solver.Depsolve(nil)
pkgSpec, repoCfg, _, err := solver.Depsolve(nil, sbom.StandardTypeNone)
assert.NoError(t, err)

// prerequisite check, i.e. ensure our fake was called in the right way
Expand Down

0 comments on commit c014c10

Please sign in to comment.