From 7c2ca9e905fefad3ed4a9f0ef145afde7c151002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hozza?= Date: Fri, 30 Aug 2024 12:54:38 +0200 Subject: [PATCH 1/5] Schutzfile: update osbuild ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the ref to a version, which supports SBOM documents. Signed-off-by: Tomáš Hozza --- Schutzfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Schutzfile b/Schutzfile index d240b159c..c50ccd878 100644 --- a/Schutzfile +++ b/Schutzfile @@ -8,21 +8,21 @@ "centos-8": { "dependencies": { "osbuild": { - "commit": "d67fa48c170a0093807b12d674f13b175ba9d59b" + "commit": "3df75de65a5414866fa43133309c1fc67490a373" } } }, "centos-9": { "dependencies": { "osbuild": { - "commit": "d67fa48c170a0093807b12d674f13b175ba9d59b" + "commit": "3df75de65a5414866fa43133309c1fc67490a373" } } }, "fedora-39": { "dependencies": { "osbuild": { - "commit": "d67fa48c170a0093807b12d674f13b175ba9d59b" + "commit": "3df75de65a5414866fa43133309c1fc67490a373" } }, "repos": [ @@ -65,7 +65,7 @@ "fedora-40": { "dependencies": { "osbuild": { - "commit": "d67fa48c170a0093807b12d674f13b175ba9d59b" + "commit": "3df75de65a5414866fa43133309c1fc67490a373" } }, "repos": [ From c51053ffec7052fe0c30e8012e0ff626884b7248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hozza?= Date: Fri, 30 Aug 2024 16:33:47 +0200 Subject: [PATCH 2/5] pkg: add 'sbom' package for working with SBOM documents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new `sbom` package for working with SBOM documents. It provides a very simple wrapper struct, which currently supports only SPDX standard. The SBOM document is for now stored in a raw JSON form, to not have to convert the raw data from and to the specific in-memory representation on the worker. The idea is to provide a bit of an abstraction from the specific SBOM implementation, so that in the future, it would be possible to create `sbom.Document` from SBOM documents of various standards and also serialize it back to various SBOM standards. Signed-off-by: Tomáš Hozza --- pkg/sbom/document.go | 70 +++++++++++++++++++++++++++++++ pkg/sbom/document_test.go | 86 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 pkg/sbom/document.go create mode 100644 pkg/sbom/document_test.go diff --git a/pkg/sbom/document.go b/pkg/sbom/document.go new file mode 100644 index 000000000..8d4dd2c96 --- /dev/null +++ b/pkg/sbom/document.go @@ -0,0 +1,70 @@ +package sbom + +import ( + "encoding/json" + "fmt" +) + +type StandardType uint64 + +const ( + StandardTypeNone StandardType = iota + StandardTypeSpdx +) + +func (t StandardType) String() string { + switch t { + case StandardTypeNone: + return "none" + case StandardTypeSpdx: + return "spdx" + default: + panic("invalid standard type") + } +} + +func (t StandardType) MarshalJSON() ([]byte, error) { + var s string + + if t == StandardTypeNone { + s = "" + } else { + s = t.String() + } + + return json.Marshal(s) +} + +func (t *StandardType) UnmarshalJSON(data []byte) error { + switch string(data) { + case `""`: + *t = StandardTypeNone + case `"spdx"`: + *t = StandardTypeSpdx + default: + return fmt.Errorf("invalid SBOM standard type: %s", data) + } + return nil +} + +type Document struct { + // type of the document standard + DocType StandardType + + // document in a specific standard JSON raw format + Document json.RawMessage +} + +func NewDocument(docType StandardType, doc json.RawMessage) (*Document, error) { + switch docType { + case StandardTypeSpdx: + docType = StandardTypeSpdx + default: + return nil, fmt.Errorf("unsupported SBOM document type: %s", docType) + } + + return &Document{ + DocType: docType, + Document: doc, + }, nil +} diff --git a/pkg/sbom/document_test.go b/pkg/sbom/document_test.go new file mode 100644 index 000000000..b60fe4384 --- /dev/null +++ b/pkg/sbom/document_test.go @@ -0,0 +1,86 @@ +package sbom + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStandardTypeJSONUnmarhsall(t *testing.T) { + type testStruct struct { + Type StandardType `json:"type"` + TypeOmit StandardType `json:"type_omit,omitempty"` + } + + tests := []struct { + name string + data []byte + want testStruct + }{ + { + name: "StandardTypeNone", + data: []byte(`{"type":""}`), + want: testStruct{ + Type: StandardTypeNone, + TypeOmit: StandardTypeNone, + }, + }, + { + name: "StandardTypeSpdx", + data: []byte(`{"type":"spdx","type_omit":"spdx"}`), + want: testStruct{ + Type: StandardTypeSpdx, + TypeOmit: StandardTypeSpdx, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ts testStruct + err := json.Unmarshal(tt.data, &ts) + assert.NoError(t, err) + assert.Equal(t, tt.want, ts) + }) + } +} + +func TestStandardTypeJSONMarhsall(t *testing.T) { + type TestStruct struct { + Type StandardType `json:"type"` + TypeOmit StandardType `json:"type_omit,omitempty"` + } + + tests := []struct { + name string + want []byte + data TestStruct + }{ + { + name: "StandardTypeNone", + want: []byte(`{"type":""}`), + data: TestStruct{ + Type: StandardTypeNone, + TypeOmit: StandardTypeNone, + }, + }, + { + name: "StandardTypeSpdx", + want: []byte(`{"type":"spdx","type_omit":"spdx"}`), + data: TestStruct{ + Type: StandardTypeSpdx, + TypeOmit: StandardTypeSpdx, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got []byte + got, err := json.Marshal(tt.data) + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} From 2491078418523b57055ccc83807261121de36050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hozza?= Date: Fri, 30 Aug 2024 13:53:03 +0200 Subject: [PATCH 3/5] dnfjson/Depsolve: support requesting SBOM document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cmd/build/main.go | 3 +- cmd/gen-manifests/main.go | 3 +- cmd/osbuild-playground/playground.go | 3 +- pkg/dnfjson/dnfjson.go | 38 ++++++++++--- pkg/dnfjson/dnfjson_test.go | 83 +++++++++++++++++++++++++--- 5 files changed, 112 insertions(+), 18 deletions(-) diff --git a/cmd/build/main.go b/cmd/build/main.go index 0401bf14e..9051bb20c 100644 --- a/cmd/build/main.go +++ b/cmd/build/main.go @@ -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( @@ -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 } diff --git a/cmd/gen-manifests/main.go b/cmd/gen-manifests/main.go index 4899546b2..50ece9210 100644 --- a/cmd/gen-manifests/main.go +++ b/cmd/gen-manifests/main.go @@ -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 { @@ -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 } diff --git a/cmd/osbuild-playground/playground.go b/cmd/osbuild-playground/playground.go index ba6dab04e..701575d7e 100644 --- a/cmd/osbuild-playground/playground.go +++ b/cmd/osbuild-playground/playground.go @@ -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) { @@ -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())) } diff --git a/pkg/dnfjson/dnfjson.go b/pkg/dnfjson/dnfjson.go index f94f2ec22..aa3e29b91 100644 --- a/pkg/dnfjson/dnfjson.go +++ b/pkg/dnfjson/dnfjson.go @@ -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 @@ -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 @@ -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() @@ -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 @@ -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 @@ -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 } @@ -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 @@ -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 { @@ -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 diff --git a/pkg/dnfjson/dnfjson_test.go b/pkg/dnfjson/dnfjson_test.go index 615584432..ade994ab3 100644 --- a/pkg/dnfjson/dnfjson_test.go +++ b/pkg/dnfjson/dnfjson_test.go @@ -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" ) @@ -33,6 +34,7 @@ func TestDepsolver(t *testing.T) { packages [][]string repos []rpmmd.RepoConfig rootDir string + sbomType sbom.StandardType err bool expMsg string } @@ -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 { @@ -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) } }) } @@ -165,6 +179,7 @@ func TestMakeDepsolveRequest(t *testing.T) { packageSets []rpmmd.PackageSet args []transactionArgs wantRepos []repoConfig + withSbom bool err bool }{ // single transaction @@ -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") @@ -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) + } } }) } @@ -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) }) @@ -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 From b57eb9866c8d7ae16d8f774d60c6f71531ba4cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hozza?= Date: Wed, 18 Sep 2024 16:27:24 +0200 Subject: [PATCH 4/5] dnfjson: wrap Depsolve() return values by DepsolveResult struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the number of the `Solver.Depsolve()` return values is slowly getting out of hands, introduce a `DepsolveResult` struct containing all of the return values. `Depsolve()` now returns a pointer to `DepsolveResult`. Signed-off-by: Tomáš Hozza --- cmd/build/main.go | 6 +++--- cmd/gen-manifests/main.go | 6 +++--- cmd/osbuild-playground/playground.go | 4 ++-- pkg/dnfjson/dnfjson.go | 23 +++++++++++++++++------ pkg/dnfjson/dnfjson_test.go | 20 +++++++++++--------- 5 files changed, 36 insertions(+), 23 deletions(-) diff --git a/cmd/build/main.go b/cmd/build/main.go index 9051bb20c..4725736d4 100644 --- a/cmd/build/main.go +++ b/cmd/build/main.go @@ -134,12 +134,12 @@ 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, sbom.StandardTypeNone) + res, err := solver.Depsolve(pkgSet, sbom.StandardTypeNone) if err != nil { return nil, nil, err } - depsolvedSets[name] = pkgs - repoSets[name] = repos + depsolvedSets[name] = res.Packages + repoSets[name] = res.Repos } return depsolvedSets, repoSets, nil } diff --git a/cmd/gen-manifests/main.go b/cmd/gen-manifests/main.go index 50ece9210..6f6eaf552 100644 --- a/cmd/gen-manifests/main.go +++ b/cmd/gen-manifests/main.go @@ -358,12 +358,12 @@ 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, sbom.StandardTypeNone) + res, err := solver.Depsolve(pkgSet, sbom.StandardTypeNone) if err != nil { return nil, nil, err } - depsolvedSets[name] = packages - repoSets[name] = repos + depsolvedSets[name] = res.Packages + repoSets[name] = res.Repos } return depsolvedSets, repoSets, nil } diff --git a/cmd/osbuild-playground/playground.go b/cmd/osbuild-playground/playground.go index 701575d7e..b953872a4 100644 --- a/cmd/osbuild-playground/playground.go +++ b/cmd/osbuild-playground/playground.go @@ -37,11 +37,11 @@ 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, sbom.StandardTypeNone) + res, err := solver.Depsolve(chain, sbom.StandardTypeNone) if err != nil { panic(fmt.Sprintf("failed to depsolve for pipeline %s: %s\n", name, err.Error())) } - packageSpecs[name] = packages + packageSpecs[name] = res.Packages } if err := solver.CleanCache(); err != nil { diff --git a/pkg/dnfjson/dnfjson.go b/pkg/dnfjson/dnfjson.go index aa3e29b91..d5f17fce7 100644 --- a/pkg/dnfjson/dnfjson.go +++ b/pkg/dnfjson/dnfjson.go @@ -156,6 +156,13 @@ type Solver struct { subscriptions *rhsm.Subscriptions } +// DepsolveResult contains the results of a depsolve operation. +type DepsolveResult struct { + Packages []rpmmd.PackageSpec + Repos []rpmmd.RepoConfig + SBOM *sbom.Document +} + // Create a new Solver with the given configuration. Initialising a Solver also loads system subscription information. func NewSolver(modulePlatformID, releaseVer, arch, distro, cacheDir string) *Solver { s := NewBaseSolver(cacheDir) @@ -194,10 +201,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, sbomType sbom.StandardType) ([]rpmmd.PackageSpec, []rpmmd.RepoConfig, *sbom.Document, error) { +func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet, sbomType sbom.StandardType) (*DepsolveResult, error) { req, rhsmMap, err := s.makeDepsolveRequest(pkgSets, sbomType) if err != nil { - return nil, nil, nil, fmt.Errorf("makeDepsolveRequest failed: %w", err) + return nil, fmt.Errorf("makeDepsolveRequest failed: %w", err) } // get non-exclusive read lock @@ -206,7 +213,7 @@ func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet, sbomType sbom.StandardType output, err := run(s.dnfJsonCmd, req) if err != nil { - return nil, nil, nil, fmt.Errorf("running osbuild-depsolve-dnf failed:\n%w", err) + return nil, fmt.Errorf("running osbuild-depsolve-dnf failed:\n%w", err) } // touch repos to now now := time.Now().Local() @@ -220,7 +227,7 @@ func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet, sbomType sbom.StandardType dec := json.NewDecoder(bytes.NewReader(output)) dec.DisallowUnknownFields() if err := dec.Decode(&result); err != nil { - return nil, nil, nil, fmt.Errorf("decoding depsolve result failed: %w", err) + return nil, fmt.Errorf("decoding depsolve result failed: %w", err) } packages, repos := result.toRPMMD(rhsmMap) @@ -229,11 +236,15 @@ func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet, sbomType sbom.StandardType 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 nil, fmt.Errorf("creating SBOM document failed: %w", err) } } - return packages, repos, sbomDoc, nil + return &DepsolveResult{ + Packages: packages, + Repos: repos, + SBOM: sbomDoc, + }, nil } // FetchMetadata returns the list of all the available packages in repos and diff --git a/pkg/dnfjson/dnfjson_test.go b/pkg/dnfjson/dnfjson_test.go index ade994ab3..f4e997b32 100644 --- a/pkg/dnfjson/dnfjson_test.go +++ b/pkg/dnfjson/dnfjson_test.go @@ -124,22 +124,23 @@ func TestDepsolver(t *testing.T) { } solver.SetRootDir(tc.rootDir) - deps, _, sbomDoc, err := solver.Depsolve(pkgsets, tc.sbomType) + res, err := solver.Depsolve(pkgsets, tc.sbomType) if tc.err { assert.Error(err) assert.Contains(err.Error(), tc.expMsg) return } else { assert.Nil(err) + assert.NotNil(res) } - assert.Equal(expectedResult(s.RepoConfig), deps) + assert.Equal(expectedResult(s.RepoConfig), res.Packages) if tc.sbomType != sbom.StandardTypeNone { - assert.NotNil(sbomDoc) - assert.Equal(sbom.StandardTypeSpdx, sbomDoc.DocType) + assert.NotNil(res.SBOM) + assert.Equal(sbom.StandardTypeSpdx, res.SBOM.DocType) } else { - assert.Nil(sbomDoc) + assert.Nil(res.SBOM) } }) } @@ -779,7 +780,7 @@ 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, @@ -868,8 +869,9 @@ echo '{"solver": "zypper"}' solver := NewSolver("platform:f38", "38", "x86_64", "fedora-38", "/tmp/cache") solver.dnfJsonCmd = []string{fakeSolverPath} - pkgSpec, repoCfg, _, err := solver.Depsolve(nil, sbom.StandardTypeNone) + res, err := solver.Depsolve(nil, sbom.StandardTypeNone) assert.NoError(t, err) + assert.NotNil(t, res) // prerequisite check, i.e. ensure our fake was called in the right way stdin, err := os.ReadFile(fakeSolverPath + ".stdin") @@ -878,6 +880,6 @@ echo '{"solver": "zypper"}' // adding the "solver" did not cause any issues assert.NoError(t, err) - assert.Equal(t, 0, len(pkgSpec)) - assert.Equal(t, 0, len(repoCfg)) + assert.Equal(t, 0, len(res.Packages)) + assert.Equal(t, 0, len(res.Repos)) } From 746950b156cb52acfd6f3db4063aab41e5723cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hozza?= Date: Thu, 19 Sep 2024 11:29:27 +0200 Subject: [PATCH 5/5] dnfjson/depsolveResult: mark optional struct members as omitempty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Hozza --- pkg/dnfjson/dnfjson.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/dnfjson/dnfjson.go b/pkg/dnfjson/dnfjson.go index d5f17fce7..784e9983a 100644 --- a/pkg/dnfjson/dnfjson.go +++ b/pkg/dnfjson/dnfjson.go @@ -724,10 +724,10 @@ type depsolveResult struct { Repos map[string]repoConfig `json:"repos"` // (optional) contains the solver used, e.g. "dnf5" - Solver string `json:"solver"` + Solver string `json:"solver,omitempty"` // (optional) contains the SBOM for the depsolved transaction - SBOM json.RawMessage `json:"sbom"` + SBOM json.RawMessage `json:"sbom,omitempty"` } // Package specification