diff --git a/Schutzfile b/Schutzfile index 070a2c2911..3053150f99 100644 --- a/Schutzfile +++ b/Schutzfile @@ -23,21 +23,21 @@ "centos-9": { "dependencies": { "osbuild": { - "commit": "4173a05047b1e3a0f5a2859e50fe55ed2642edad" + "commit": "6c34e00bedd8101293382f2d563d111825b46349" } } }, "centos-10": { "dependencies": { "osbuild": { - "commit": "4173a05047b1e3a0f5a2859e50fe55ed2642edad" + "commit": "6c34e00bedd8101293382f2d563d111825b46349" } } }, "fedora-42": { "dependencies": { "osbuild": { - "commit": "4173a05047b1e3a0f5a2859e50fe55ed2642edad" + "commit": "6c34e00bedd8101293382f2d563d111825b46349" } }, "repos": [ @@ -73,14 +73,14 @@ "fedora-43": { "dependencies": { "osbuild": { - "commit": "4173a05047b1e3a0f5a2859e50fe55ed2642edad" + "commit": "6c34e00bedd8101293382f2d563d111825b46349" } } }, "fedora-44": { "dependencies": { "osbuild": { - "commit": "4173a05047b1e3a0f5a2859e50fe55ed2642edad" + "commit": "6c34e00bedd8101293382f2d563d111825b46349" } } } diff --git a/data/dependencies/deps_test.go b/data/dependencies/deps_test.go index 4753490ea2..a85f039492 100644 --- a/data/dependencies/deps_test.go +++ b/data/dependencies/deps_test.go @@ -8,5 +8,5 @@ import ( ) func TestMinimumOSBuildVersion(t *testing.T) { - assert.Equal(t, "169", dependencies.MinimumOSBuildVersion()) + assert.Equal(t, "170", dependencies.MinimumOSBuildVersion()) } diff --git a/data/dependencies/osbuild b/data/dependencies/osbuild index 9a1371776c..3968aef87f 100644 --- a/data/dependencies/osbuild +++ b/data/dependencies/osbuild @@ -1 +1 @@ -169 \ No newline at end of file +170 \ No newline at end of file diff --git a/internal/common/pointers.go b/internal/common/pointers.go index aa8efb8947..60010f9c3e 100644 --- a/internal/common/pointers.go +++ b/internal/common/pointers.go @@ -4,6 +4,16 @@ func ToPtr[T any](x T) *T { return &x } +// ClonePtr returns a new pointer to the same value as the input pointer. +// It returns nil if the input pointer is nil. If the value is a complex type, +// the returned pointer will be a shallow copy of the input pointer. +func ClonePtr[T any](x *T) *T { + if x == nil { + return nil + } + return ToPtr(*x) +} + // ValueOrEmpty returns the value from a given pointer. If ref is nil, // a zero value of type T will be returned. func ValueOrEmpty[T any](ref *T) (value T) { diff --git a/internal/common/pointers_test.go b/internal/common/pointers_test.go index 920f251fd5..704e1c86fa 100644 --- a/internal/common/pointers_test.go +++ b/internal/common/pointers_test.go @@ -25,6 +25,30 @@ func TestToPtr(t *testing.T) { } +func TestClonePtr(t *testing.T) { + t.Run("nil pointer returns nil", func(t *testing.T) { + var p *int + result := ClonePtr(p) + assert.Nil(t, result) + }) + + t.Run("non-nil pointer returns independent copy", func(t *testing.T) { + original := 42 + p := &original + result := ClonePtr(p) + + // Should have same value + assert.Equal(t, 42, *result) + + // Should be different pointer + assert.NotSame(t, p, result) + + // Modifying original should not affect clone + original = 100 + assert.Equal(t, 42, *result) + }) +} + func TestValueOrEmpty(t *testing.T) { var ptrInt *int valueInt := ValueOrEmpty(ptrInt) diff --git a/pkg/customizations/subscription/subscription.go b/pkg/customizations/subscription/subscription.go index 0968c7331c..ed33be4af4 100644 --- a/pkg/customizations/subscription/subscription.go +++ b/pkg/customizations/subscription/subscription.go @@ -70,29 +70,15 @@ func (c *RHSMConfig) Clone() *RHSMConfig { clone := &RHSMConfig{} - if c.DnfPlugins.ProductID.Enabled != nil { - clone.DnfPlugins.ProductID.Enabled = common.ToPtr(*c.DnfPlugins.ProductID.Enabled) - } - if c.DnfPlugins.SubscriptionManager.Enabled != nil { - clone.DnfPlugins.SubscriptionManager.Enabled = common.ToPtr(*c.DnfPlugins.SubscriptionManager.Enabled) - } + clone.DnfPlugins.ProductID.Enabled = common.ClonePtr(c.DnfPlugins.ProductID.Enabled) + clone.DnfPlugins.SubscriptionManager.Enabled = common.ClonePtr(c.DnfPlugins.SubscriptionManager.Enabled) - if c.YumPlugins.ProductID.Enabled != nil { - clone.YumPlugins.ProductID.Enabled = common.ToPtr(*c.YumPlugins.ProductID.Enabled) - } - if c.YumPlugins.SubscriptionManager.Enabled != nil { - clone.YumPlugins.SubscriptionManager.Enabled = common.ToPtr(*c.YumPlugins.SubscriptionManager.Enabled) - } + clone.YumPlugins.ProductID.Enabled = common.ClonePtr(c.YumPlugins.ProductID.Enabled) + clone.YumPlugins.SubscriptionManager.Enabled = common.ClonePtr(c.YumPlugins.SubscriptionManager.Enabled) - if c.SubMan.Rhsm.ManageRepos != nil { - clone.SubMan.Rhsm.ManageRepos = common.ToPtr(*c.SubMan.Rhsm.ManageRepos) - } - if c.SubMan.Rhsm.AutoEnableYumPlugins != nil { - clone.SubMan.Rhsm.AutoEnableYumPlugins = common.ToPtr(*c.SubMan.Rhsm.AutoEnableYumPlugins) - } - if c.SubMan.Rhsmcertd.AutoRegistration != nil { - clone.SubMan.Rhsmcertd.AutoRegistration = common.ToPtr(*c.SubMan.Rhsmcertd.AutoRegistration) - } + clone.SubMan.Rhsm.ManageRepos = common.ClonePtr(c.SubMan.Rhsm.ManageRepos) + clone.SubMan.Rhsm.AutoEnableYumPlugins = common.ClonePtr(c.SubMan.Rhsm.AutoEnableYumPlugins) + clone.SubMan.Rhsmcertd.AutoRegistration = common.ClonePtr(c.SubMan.Rhsmcertd.AutoRegistration) return clone } @@ -112,27 +98,27 @@ func (c *RHSMConfig) Update(new *RHSMConfig) *RHSMConfig { } if new.DnfPlugins.ProductID.Enabled != nil { - c.DnfPlugins.ProductID.Enabled = common.ToPtr(*new.DnfPlugins.ProductID.Enabled) + c.DnfPlugins.ProductID.Enabled = common.ClonePtr(new.DnfPlugins.ProductID.Enabled) } if new.DnfPlugins.SubscriptionManager.Enabled != nil { - c.DnfPlugins.SubscriptionManager.Enabled = common.ToPtr(*new.DnfPlugins.SubscriptionManager.Enabled) + c.DnfPlugins.SubscriptionManager.Enabled = common.ClonePtr(new.DnfPlugins.SubscriptionManager.Enabled) } if new.YumPlugins.ProductID.Enabled != nil { - c.YumPlugins.ProductID.Enabled = common.ToPtr(*new.YumPlugins.ProductID.Enabled) + c.YumPlugins.ProductID.Enabled = common.ClonePtr(new.YumPlugins.ProductID.Enabled) } if new.YumPlugins.SubscriptionManager.Enabled != nil { - c.YumPlugins.SubscriptionManager.Enabled = common.ToPtr(*new.YumPlugins.SubscriptionManager.Enabled) + c.YumPlugins.SubscriptionManager.Enabled = common.ClonePtr(new.YumPlugins.SubscriptionManager.Enabled) } if new.SubMan.Rhsm.ManageRepos != nil { - c.SubMan.Rhsm.ManageRepos = common.ToPtr(*new.SubMan.Rhsm.ManageRepos) + c.SubMan.Rhsm.ManageRepos = common.ClonePtr(new.SubMan.Rhsm.ManageRepos) } if new.SubMan.Rhsm.AutoEnableYumPlugins != nil { - c.SubMan.Rhsm.AutoEnableYumPlugins = common.ToPtr(*new.SubMan.Rhsm.AutoEnableYumPlugins) + c.SubMan.Rhsm.AutoEnableYumPlugins = common.ClonePtr(new.SubMan.Rhsm.AutoEnableYumPlugins) } if new.SubMan.Rhsmcertd.AutoRegistration != nil { - c.SubMan.Rhsmcertd.AutoRegistration = common.ToPtr(*new.SubMan.Rhsmcertd.AutoRegistration) + c.SubMan.Rhsmcertd.AutoRegistration = common.ClonePtr(new.SubMan.Rhsmcertd.AutoRegistration) } return c @@ -147,11 +133,11 @@ func RHSMConfigFromBP(bpRHSM *blueprint.RHSMCustomization) *RHSMConfig { c := &RHSMConfig{} if plugins := bpRHSM.Config.DNFPlugins; plugins != nil { - if plugins.ProductID != nil && plugins.ProductID.Enabled != nil { - c.DnfPlugins.ProductID.Enabled = common.ToPtr(*plugins.ProductID.Enabled) + if plugins.ProductID != nil { + c.DnfPlugins.ProductID.Enabled = common.ClonePtr(plugins.ProductID.Enabled) } - if plugins.SubscriptionManager != nil && plugins.SubscriptionManager.Enabled != nil { - c.DnfPlugins.SubscriptionManager.Enabled = common.ToPtr(*plugins.SubscriptionManager.Enabled) + if plugins.SubscriptionManager != nil { + c.DnfPlugins.SubscriptionManager.Enabled = common.ClonePtr(plugins.SubscriptionManager.Enabled) } } @@ -159,15 +145,11 @@ func RHSMConfigFromBP(bpRHSM *blueprint.RHSMCustomization) *RHSMConfig { if subMan := bpRHSM.Config.SubscriptionManager; subMan != nil { if subMan.RHSMConfig != nil { - if subMan.RHSMConfig.ManageRepos != nil { - c.SubMan.Rhsm.ManageRepos = common.ToPtr(*subMan.RHSMConfig.ManageRepos) - } - if subMan.RHSMConfig.AutoEnableYumPlugins != nil { - c.SubMan.Rhsm.AutoEnableYumPlugins = common.ToPtr(*subMan.RHSMConfig.AutoEnableYumPlugins) - } + c.SubMan.Rhsm.ManageRepos = common.ClonePtr(subMan.RHSMConfig.ManageRepos) + c.SubMan.Rhsm.AutoEnableYumPlugins = common.ClonePtr(subMan.RHSMConfig.AutoEnableYumPlugins) } - if subMan.RHSMCertdConfig != nil && subMan.RHSMCertdConfig.AutoRegistration != nil { - c.SubMan.Rhsmcertd.AutoRegistration = common.ToPtr(*subMan.RHSMCertdConfig.AutoRegistration) + if subMan.RHSMCertdConfig != nil { + c.SubMan.Rhsmcertd.AutoRegistration = common.ClonePtr(subMan.RHSMCertdConfig.AutoRegistration) } } diff --git a/pkg/depsolvednf/api.go b/pkg/depsolvednf/api.go index 40e9f726de..93344b7cf9 100644 --- a/pkg/depsolvednf/api.go +++ b/pkg/depsolvednf/api.go @@ -14,11 +14,12 @@ import ( // It also contains raw SBOM data (Solver is responsible for creating // sbom.Document). type depsolveResultRaw struct { - Packages rpmmd.PackageList - Modules []rpmmd.ModuleSpec - Repos []rpmmd.RepoConfig - Solver string - SBOMRaw json.RawMessage + Packages rpmmd.PackageList + Transactions []rpmmd.PackageList + Modules []rpmmd.ModuleSpec + Repos []rpmmd.RepoConfig + Solver string + SBOMRaw json.RawMessage } // apiHandler defines the interface for API version implementations. @@ -61,5 +62,5 @@ type solverConfig struct { var activeHandler apiHandler func init() { - activeHandler = newV1Handler() + activeHandler = newV2Handler() } diff --git a/pkg/depsolvednf/depsolvednf.go b/pkg/depsolvednf/depsolvednf.go index 3f69fe6bc4..9c0ed1b5bc 100644 --- a/pkg/depsolvednf/depsolvednf.go +++ b/pkg/depsolvednf/depsolvednf.go @@ -168,21 +168,33 @@ type Solver struct { // DepsolveResult contains the results of a depsolve operation. type DepsolveResult struct { + // XXX: Packages is kept for backwards compatibility and should + // be removed once all clients have been updated to use Transactions. Packages rpmmd.PackageList - Modules []rpmmd.ModuleSpec - Repos []rpmmd.RepoConfig - SBOM *sbom.Document - Solver string + // Transactions is a list of package lists, one for each depsolve + // transaction. Each transaction contains only the packages to be + // installed that are unique to that transaction. The transaction results + // are disjoint sets that should be installed in the order they appear in + // the list. + Transactions []rpmmd.PackageList + Modules []rpmmd.ModuleSpec + Repos []rpmmd.RepoConfig + SBOM *sbom.Document + Solver string } // DumpResult contains the results of a dump operation. type DumpResult struct { Packages rpmmd.PackageList + Repos []rpmmd.RepoConfig + Solver string } // SearchResult contains the results of a search operation. type SearchResult struct { Packages rpmmd.PackageList + Repos []rpmmd.RepoConfig + Solver string } // Create a new Solver with the given configuration. Initialising a Solver also loads system subscription information. @@ -307,6 +319,11 @@ func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet, sbomType sbom.StandardType // Apply RHSM secrets to packages from RHSM repos. applyRHSMSecrets(resultRaw.Packages, allRepos) + // Apply RHSM secrets to packages in each transaction as well. + for _, transaction := range resultRaw.Transactions { + applyRHSMSecrets(transaction, allRepos) + } + var sbomDoc *sbom.Document if sbomType != sbom.StandardTypeNone { sbomDoc, err = sbom.NewDocument(sbomType, resultRaw.SBOMRaw) @@ -316,11 +333,12 @@ func (s *Solver) Depsolve(pkgSets []rpmmd.PackageSet, sbomType sbom.StandardType } return &DepsolveResult{ - Packages: resultRaw.Packages, - Modules: resultRaw.Modules, - Repos: resultRaw.Repos, - SBOM: sbomDoc, - Solver: resultRaw.Solver, + Packages: resultRaw.Packages, + Transactions: resultRaw.Transactions, + Modules: resultRaw.Modules, + Repos: resultRaw.Repos, + SBOM: sbomDoc, + Solver: resultRaw.Solver, }, nil } @@ -375,6 +393,8 @@ func (s *Solver) FetchMetadata(repos []rpmmd.RepoConfig) (rpmmd.PackageList, err return nil, err } + // XXX: Cache and expose the whole operation result instead of just the packages in the future. + pkgs := res.Packages sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].NVR() < pkgs[j].NVR() @@ -421,6 +441,8 @@ func (s *Solver) SearchMetadata(repos []rpmmd.RepoConfig, packages []string) (rp return nil, err } + // XXX: Cache and expose the whole operation result instead of just the packages in the future. + pkgs := res.Packages sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].NVR() < pkgs[j].NVR() diff --git a/pkg/depsolvednf/depsolvednf_test.go b/pkg/depsolvednf/depsolvednf_test.go index 0163217f74..46fde51405 100644 --- a/pkg/depsolvednf/depsolvednf_test.go +++ b/pkg/depsolvednf/depsolvednf_test.go @@ -7,9 +7,10 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" - "strings" "testing" + "time" "github.com/osbuild/images/internal/common" "github.com/osbuild/images/internal/mocks/rpmrepo" @@ -21,6 +22,148 @@ import ( var forceDNF = flag.Bool("force-dnf", false, "force dnf testing, making them fail instead of skip if dnf isn't installed") +// testHandler holds an API handler with its name for test iteration. +type testHandler struct { + name string + handler apiHandler + assertDepsolveResult func(t *testing.T, pkgSets []rpmmd.PackageSet, actual DepsolveResult) +} + +// getTestHandlers returns the list of handlers to test against. +func getTestHandlers() []testHandler { + return []testHandler{ + {name: "V1", handler: newV1Handler(), assertDepsolveResult: assertDepsolveResultV1}, + {name: "V2", handler: newV2Handler(), assertDepsolveResult: assertDepsolveResultV2}, + } +} + +// mockActiveHandler replaces the activeHandler with the given handler +// and returns a function to restore the original handler. +func mockActiveHandler(h apiHandler) func() { + original := activeHandler + activeHandler = h + return func() { + activeHandler = original + } +} + +// assertPackagesMatchCore compares two packages by the original core fields only. +// This is needed because newer versions of the depsolver API return much more +// metadata than the original core fields, but the newer API is a superset of +// the original core fields. +func assertPackagesMatchCore(t *testing.T, expected, actual rpmmd.Package) { + t.Helper() + // Values set by the original core fields + assert.Equal(t, expected.Name, actual.Name, "Name mismatch") + assert.Equal(t, expected.Epoch, actual.Epoch, "Epoch mismatch") + assert.Equal(t, expected.Version, actual.Version, "Version mismatch") + assert.Equal(t, expected.Release, actual.Release, "Release mismatch") + assert.Equal(t, expected.Arch, actual.Arch, "Arch mismatch") + assert.Equal(t, expected.RepoID, actual.RepoID, "RepoID mismatch") + assert.Equal(t, expected.Location, actual.Location, "Location mismatch") + assert.Equal(t, expected.RemoteLocations, actual.RemoteLocations, "RemoteLocations mismatch") + assert.Equal(t, expected.Checksum, actual.Checksum, "Checksum mismatch") + // Values set by the Solver from the Repository metadata + assert.Equal(t, expected.CheckGPG, actual.CheckGPG, "CheckGPG mismatch") + assert.Equal(t, expected.IgnoreSSL, actual.IgnoreSSL, "IgnoreSSL mismatch") +} + +// assertExpectedPackages checks actual packages against expected using original core fields, +// and verifies all requested packages are present. This is shared between V1 and V2 assertions. +func assertExpectedPackages(t *testing.T, pkgSets []rpmmd.PackageSet, expected, actual rpmmd.PackageList) { + t.Helper() + + // Check that the list of packages matches expected + require.Equal(t, len(expected), len(actual), "package count mismatch") + for i := range expected { + assertPackagesMatchCore(t, expected[i], actual[i]) + } + + // Check that all requested packages are present + for _, pkgSet := range pkgSets { + for _, reqPkg := range pkgSet.Include { + assert.True(t, slices.ContainsFunc(actual, func(p rpmmd.Package) bool { + return p.Name == reqPkg + }), "requested package %q not found in the depsolve result", reqPkg) + } + } +} + +// assertDepsolveResultV1 checks the expected packages against the actual packages for the V1 API result. +func assertDepsolveResultV1(t *testing.T, pkgSets []rpmmd.PackageSet, actual DepsolveResult) { + t.Helper() + + require.Equal(t, 1, len(actual.Repos), "expected exactly 1 repo") + expectedPackages := expectedDepsolvedPackages(actual.Repos[0]) + + // Check Packages field + assertExpectedPackages(t, pkgSets, expectedPackages, actual.Packages) + + // The Transactions field is not set in the V1 API + assert.Empty(t, actual.Transactions) +} + +// assertDepsolveResultV2 checks the expected packages against the actual packages for the V2 API result. +func assertDepsolveResultV2(t *testing.T, pkgSets []rpmmd.PackageSet, actual DepsolveResult) { + t.Helper() + + require.Equal(t, 1, len(actual.Repos), "expected exactly 1 repo") + expectedPackages := expectedDepsolvedPackages(actual.Repos[0]) + + // Check Packages field (for backwards compat while Packages field exists) + assertExpectedPackages(t, pkgSets, expectedPackages, actual.Packages) + + // V2 specific tests below + + // Transaction count must match number of package sets + require.Equal(t, len(pkgSets), len(actual.Transactions), "transaction count mismatch") + + // Each requested package must be in the correct transaction + for i, pkgSet := range pkgSets { + for _, reqPkg := range pkgSet.Include { + assert.True(t, slices.ContainsFunc(actual.Transactions[i], func(p rpmmd.Package) bool { + return p.Name == reqPkg + }), "requested package %q not found in transaction %d", reqPkg, i) + } + } + + // Union of all Transactions must equal expected packages + allTransactionPkgs := make(rpmmd.PackageList, 0) + for _, transaction := range actual.Transactions { + allTransactionPkgs = append(allTransactionPkgs, transaction...) + } + sort.Slice(allTransactionPkgs, func(i, j int) bool { + return allTransactionPkgs[i].FullNEVRA() < allTransactionPkgs[j].FullNEVRA() + }) + require.Equal(t, len(expectedPackages), len(allTransactionPkgs), "transaction packages count mismatch") + + // Check full metadata for all packages + for i := range expectedPackages { + // Check full metadata for bash package as a smoke test + if expectedPackages[i].Name == "bash" { + assert.Equal(t, expectedPackages[i], actual.Packages[i], "full bash metadata mismatch") + continue + } + + // NOTE: We don't compare the full metadata here, because V2 returns more metadata than what we define in our + // test packages metadata. We don't include the full metadata for all packages to keep this source file + // reasonably small. + assertPackagesMatchCore(t, expectedPackages[i], allTransactionPkgs[i]) + + // Sanity test that additional fields that should never be empty for any package are not empty + assert.NotEmpty(t, allTransactionPkgs[i].Group) + assert.Greater(t, allTransactionPkgs[i].DownloadSize, uint64(0)) + assert.NotEmpty(t, allTransactionPkgs[i].License) + assert.NotEmpty(t, allTransactionPkgs[i].SourceRpm) + assert.NotEmpty(t, allTransactionPkgs[i].BuildTime) + assert.NotEmpty(t, allTransactionPkgs[i].Packager) + assert.NotEmpty(t, allTransactionPkgs[i].Vendor) + assert.NotEmpty(t, allTransactionPkgs[i].Summary) + assert.NotEmpty(t, allTransactionPkgs[i].Description) + assert.NotEmpty(t, allTransactionPkgs[i].Provides) + } +} + func requireDNF(t *testing.T) { t.Helper() if !*forceDNF { @@ -125,47 +268,49 @@ func TestSolverDepsolve(t *testing.T) { }, } - for tcName := range testCases { - t.Run(tcName, func(t *testing.T) { - assert := assert.New(t) - tc := testCases[tcName] - pkgsets := make([]rpmmd.PackageSet, len(tc.packages)) - for idx := range tc.packages { - pkgsets[idx] = rpmmd.PackageSet{Include: tc.packages[idx], Repositories: tc.repos, InstallWeakDeps: true} - } + for _, h := range getTestHandlers() { + t.Run(h.name, func(t *testing.T) { + restore := mockActiveHandler(h.handler) + defer restore() - solver := newTestSolver(t) - solver.SetRootDir(tc.rootDir) - res, err := solver.Depsolve(pkgsets, tc.sbomType) - if tc.err { - assert.Error(err) - assert.Contains(err.Error(), tc.expMsg) - return - } else { - assert.Nil(err) - require.NotNil(t, res) - } + for tcName, tc := range testCases { + t.Run(tcName, func(t *testing.T) { + assert := assert.New(t) + pkgsets := make([]rpmmd.PackageSet, len(tc.packages)) + for idx := range tc.packages { + pkgsets[idx] = rpmmd.PackageSet{Include: tc.packages[idx], Repositories: tc.repos, InstallWeakDeps: true} + } - assert.Equal(len(res.Repos), 1) - assert.Equal(expectedDepsolveResult(res.Repos[0]), res.Packages) + solver := newTestSolver(t) + solver.SetRootDir(tc.rootDir) + actualResult, err := solver.Depsolve(pkgsets, tc.sbomType) + if tc.err { + assert.Error(err) + assert.Contains(err.Error(), tc.expMsg) + return + } else { + assert.Nil(err) + require.NotNil(t, actualResult) + } - if tc.sbomType != sbom.StandardTypeNone { - require.NotNil(t, res.SBOM) - assert.Equal(sbom.StandardTypeSpdx, res.SBOM.DocType) - } else { - assert.Nil(res.SBOM) + h.assertDepsolveResult(t, pkgsets, *actualResult) + + // NOTE: The SBOM document is not stable due to UUIDs, so we need to take it from the result + if tc.sbomType != sbom.StandardTypeNone { + require.NotNil(t, actualResult.SBOM) + assert.Equal(sbom.StandardTypeSpdx, actualResult.SBOM.DocType) + assert.NotEmpty(actualResult.SBOM.Document) + } else { + assert.Nil(actualResult.SBOM) + } + }) } }) } } func TestSolverDepsolveAll(t *testing.T) { - if !*forceDNF { - // dnf tests aren't forced: skip them if the dnf sniff check fails - if findDepsolveDnf() == "" { - t.Skip("Test needs an installed osbuild-depsolve-dnf") - } - } + requireDNF(t) s := rpmrepo.NewTestServer() defer s.Close() @@ -329,39 +474,47 @@ func TestSolverDepsolveAll(t *testing.T) { }, } - for tcName := range testCases { - t.Run(tcName, func(t *testing.T) { - assert := assert.New(t) - tc := testCases[tcName] + for _, h := range getTestHandlers() { + t.Run(h.name, func(t *testing.T) { + restore := mockActiveHandler(h.handler) + defer restore() - solver := NewSolver("platform:el9", "9", "x86_64", "rhel9.0", tmpdir) - solver.SetSBOMType(tc.sbomType) - res, err := solver.DepsolveAll(tc.packageSets) - if len(tc.expErrs) != 0 { - assert.Error(err) - for _, expErr := range tc.expErrs { - assert.Contains(err.Error(), expErr) - } - return - } else { - assert.Nil(err) - require.NotNil(t, res) - } + for tcName := range testCases { + t.Run(tcName, func(t *testing.T) { + assert := assert.New(t) + tc := testCases[tcName] + + solver := NewSolver("platform:el9", "9", "x86_64", "rhel9.0", tmpdir) + solver.SetSBOMType(tc.sbomType) + res, err := solver.DepsolveAll(tc.packageSets) + if len(tc.expErrs) != 0 { + assert.Error(err) + for _, expErr := range tc.expErrs { + assert.Contains(err.Error(), expErr) + } + return + } else { + assert.Nil(err) + require.NotNil(t, res) + } + + assert.Equal(len(res), len(tc.packageSets)) - assert.Equal(len(res), len(tc.packageSets)) + for pipelineName := range tc.packageSets { + pipelineResult := res[pipelineName] + assert.NotNil(pipelineResult) - for pipelineName := range tc.packageSets { - pipelineResult := res[pipelineName] - assert.NotNil(pipelineResult) - assert.Equal(len(pipelineResult.Repos), 1) - assert.Equal(expectedDepsolveResult(pipelineResult.Repos[0]), pipelineResult.Packages) + h.assertDepsolveResult(t, tc.packageSets[pipelineName], pipelineResult) - if tc.sbomType != sbom.StandardTypeNone { - require.NotNil(t, pipelineResult.SBOM) - assert.Equal(sbom.StandardTypeSpdx, pipelineResult.SBOM.DocType) - } else { - assert.Nil(pipelineResult.SBOM) - } + if tc.sbomType != sbom.StandardTypeNone { + require.NotNil(t, pipelineResult.SBOM) + assert.Equal(sbom.StandardTypeSpdx, pipelineResult.SBOM.DocType) + assert.NotEmpty(pipelineResult.SBOM.Document) + } else { + assert.Nil(pipelineResult.SBOM) + } + } + }) } }) } @@ -371,17 +524,25 @@ func TestSolverFetchMetadata(t *testing.T) { requireDNF(t) repoServer := rpmrepo.NewTestServer() defer repoServer.Close() - solver := newTestSolver(t) - - res, err := solver.FetchMetadata([]rpmmd.RepoConfig{repoServer.RepoConfig}) - require.NoError(t, err) - require.NotNil(t, res) - // 1125 is the number of packages in the test repository (internal/mocks/rpmrepo) - require.Equal(t, 1125, len(res)) - // ensure that the packages are sorted by full NEVRA - require.Truef(t, sort.SliceIsSorted(res, func(i, j int) bool { - return res[i].NVR() < res[j].NVR() - }), "packages are not sorted by NVR") + + for _, h := range getTestHandlers() { + t.Run(h.name, func(t *testing.T) { + restore := mockActiveHandler(h.handler) + defer restore() + + solver := newTestSolver(t) + + res, err := solver.FetchMetadata([]rpmmd.RepoConfig{repoServer.RepoConfig}) + require.NoError(t, err) + require.NotNil(t, res) + // 1125 is the number of packages in the test repository (internal/mocks/rpmrepo) + require.Equal(t, 1125, len(res)) + // ensure that the packages are sorted by full NEVRA + require.Truef(t, sort.SliceIsSorted(res, func(i, j int) bool { + return res[i].NVR() < res[j].NVR() + }), "packages are not sorted by NVR") + }) + } } func TestSolverSearchMetadata(t *testing.T) { @@ -416,19 +577,27 @@ func TestSolverSearchMetadata(t *testing.T) { s := rpmrepo.NewTestServer() defer s.Close() - solver := newTestSolver(t) - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - res, err := solver.SearchMetadata([]rpmmd.RepoConfig{s.RepoConfig}, tc.packages) - require.NoError(t, err) - require.NotNil(t, res) - require.Equal(t, len(tc.expNVRs), len(res)) - require.Truef(t, sort.SliceIsSorted(res, func(i, j int) bool { - return res[i].NVR() < res[j].NVR() - }), "packages are not sorted by NVR") - for i, pkg := range res { - require.Equal(t, tc.expNVRs[i], pkg.NVR()) + for _, h := range getTestHandlers() { + t.Run(h.name, func(t *testing.T) { + restore := mockActiveHandler(h.handler) + defer restore() + + solver := newTestSolver(t) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := solver.SearchMetadata([]rpmmd.RepoConfig{s.RepoConfig}, tc.packages) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, len(tc.expNVRs), len(res)) + require.Truef(t, sort.SliceIsSorted(res, func(i, j int) bool { + return res[i].NVR() < res[j].NVR() + }), "packages are not sorted by NVR") + for i, pkg := range res { + require.Equal(t, tc.expNVRs[i], pkg.NVR()) + } + }) } }) } @@ -633,137 +802,175 @@ func TestApplyRHSMSecrets(t *testing.T) { } } -func expectedDepsolveResult(repo rpmmd.RepoConfig) rpmmd.PackageList { - // need to change the url for the RemoteLocation and the repo ID since the port is different each time and we don't want to have a fixed one +// expectedDepsolvedPackages returns expected expected depsolved packages metadata. +// Repo-specific values (RemoteLocations, RepoID, etc.) are applied at assertion time. +func expectedDepsolvedPackages(repo rpmmd.RepoConfig) rpmmd.PackageList { expectedTemplate := rpmmd.PackageList{ - {Name: "acl", Epoch: 0, Version: "2.3.1", Release: "3.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/acl-2.3.1-3.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "986044c3837eddbc9231d7be5e5fc517e245296978b988a803bc9f9172fe84ea"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "alternatives", Epoch: 0, Version: "1.20", Release: "2.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/alternatives-1.20-2.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "1851d5f64ebaeac67c5c2d9e4adc1e73aa6433b44a167268a3510c3d056062db"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "audit-libs", Epoch: 0, Version: "3.0.7", Release: "100.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/audit-libs-3.0.7-100.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "a4bdda48abaedffeb74398cd55afbd00cb4153ae24bd2a3e6de9d87462df5ffa"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "basesystem", Epoch: 0, Version: "11", Release: "13.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/basesystem-11-13.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "a7a687ef39dd28d01d34fab18ea7e3e87f649f6c202dded82260b7ea625b9973"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "bash", Epoch: 0, Version: "5.1.8", Release: "2.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/bash-5.1.8-2.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "3d45552ea940db0556dd2dc73e92c20c0d7cbc9e617f251904f20475d4ecc6b6"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "bzip2-libs", Epoch: 0, Version: "1.0.8", Release: "8.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/bzip2-libs-1.0.8-8.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "fabd6b5c065c2b9d4a8d39a938ae577d801de2ddc73c8cdf6f7803db29c28d0a"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "ca-certificates", Epoch: 0, Version: "2020.2.50", Release: "94.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/ca-certificates-2020.2.50-94.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "3099471d984fb7d9e1cf42406eb08c154b34b8560742ed1f5eb9139f059c2d09"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "centos-gpg-keys", Epoch: 0, Version: "9.0", Release: "9.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/centos-gpg-keys-9.0-9.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "2785ab660c124c9bda4ef4057e72d7fc73e8ac254ddd09a5541a6d323740dad7"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "centos-stream-release", Epoch: 0, Version: "9.0", Release: "9.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/centos-stream-release-9.0-9.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "44246cc9b62ac0fb833ece49cff6ac0a932234fcba26b8c895f42baebf0a19c2"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "centos-stream-repos", Epoch: 0, Version: "9.0", Release: "9.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/centos-stream-repos-9.0-9.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "90208bb7dd1558a3311a28ea06d75ad7e83be3f223c5fb2eff1b9ac47bb98ebe"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "coreutils", Epoch: 0, Version: "8.32", Release: "31.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/coreutils-8.32-31.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "647a3b9a52df25cb2aaf7f3715b219839b4cf71913638c88172d925173280812"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "coreutils-common", Epoch: 0, Version: "8.32", Release: "31.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/coreutils-common-8.32-31.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "864b166ac6d55cad5010da369fd7ad4872f81c2c111867dfbf96ccf4c8273c7e"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "cpio", Epoch: 0, Version: "2.13", Release: "16.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/cpio-2.13-16.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "216b76d33443b732be42fe1d443e106a17e632ac9ca465928c37a8c0ede596a4"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "cracklib", Epoch: 0, Version: "2.9.6", Release: "27.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/cracklib-2.9.6-27.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "be9deb2efd06b4b2c1c130acae94c687161d04830119e65a989d904ba9fd1864"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "cracklib-dicts", Epoch: 0, Version: "2.9.6", Release: "27.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/cracklib-dicts-2.9.6-27.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "01df2a72fcdf988132e82764ce1a22a5a9513fa253b54e17d23058bdb53c2d85"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "crypto-policies", Epoch: 0, Version: "20220203", Release: "1.gitf03e75e.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/crypto-policies-20220203-1.gitf03e75e.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "28d73d3800cb895b265bc0755c41241c593ebd7551a7da7f001f1e254b85c662"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "cryptsetup-libs", Epoch: 0, Version: "2.4.3", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/cryptsetup-libs-2.4.3-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "6919d88afdd2cf89982ec8edd4401ff93394a81873f81cf89bb273384f39104f"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "dbus", Epoch: 1, Version: "1.12.20", Release: "5.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/dbus-1.12.20-5.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "bb85bd28cc162e98da53b756b988ffd9350f4dbcc186f4c6962ae047e27f83d3"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "dbus-broker", Epoch: 0, Version: "28", Release: "5.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/dbus-broker-28-5.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "e9efdcdcfe430e474e3a7f09596a0a5a4314692d9ae846bb1ca86ff88ef81038"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "dbus-common", Epoch: 1, Version: "1.12.20", Release: "5.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/dbus-common-1.12.20-5.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "150048b6fdafd4271bd6badab3f8a2e56b86967266f890770eab7578289cc773"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "device-mapper", Epoch: 9, Version: "1.02.181", Release: "3.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/device-mapper-1.02.181-3.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "5d1cd7733f147020ef3a9e08fa2e9d74a25e0ac89dfbadc69912541286146feb"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "device-mapper-libs", Epoch: 9, Version: "1.02.181", Release: "3.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/device-mapper-libs-1.02.181-3.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "a716ccca85fad2885af4d099f8c213eb4617d637d8ca6cf7d2b483b9de88a5d3"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "dracut", Epoch: 0, Version: "055", Release: "10.git20210824.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/dracut-055-10.git20210824.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "54015283e7f85fbee9d8a814c3bd60c7f81a6eb2ff480ca689e4526637a81c83"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "elfutils-default-yama-scope", Epoch: 0, Version: "0.186", Release: "1.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/elfutils-default-yama-scope-0.186-1.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "0d2dcfaa16f83de78a251cf0b9a4c5e4ec7d4deb2e8d1cae7209be7745fabeb5"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "elfutils-libelf", Epoch: 0, Version: "0.186", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/elfutils-libelf-0.186-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "0e295e6150b6929408ac29792ec5f3ebeb4a20607eb553177f0e4899b3008d63"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "elfutils-libs", Epoch: 0, Version: "0.186", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/elfutils-libs-0.186-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "bcc47b8ab496d3d11d772b037e022bc3a4ce3b080b7d1c24fa7f999426a6b8f3"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "expat", Epoch: 0, Version: "2.2.10", Release: "5.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/expat-2.2.10-5.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "f97cd3c1e79b4dfff232ba0208c2e1a7d557608c1c37e8303de4f75387be9bb7"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "filesystem", Epoch: 0, Version: "3.16", Release: "2.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/filesystem-3.16-2.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "b69a472751268a1b9acd566dc7aa486fc1d6c8cb6d23f36d6a6dfead62e71475"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "findutils", Epoch: 1, Version: "4.8.0", Release: "5.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/findutils-4.8.0-5.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "552548e6d6f9623ccd9d31bb185bba3a66730da6e9d02296b417d501356c3848"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "gdbm-libs", Epoch: 1, Version: "1.19", Release: "4.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/gdbm-libs-1.19-4.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "8cd5a78cab8783dd241c52c4fcda28fb111c443887dd6d0fe38385e8383c98b3"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "glibc", Epoch: 0, Version: "2.34", Release: "21.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/glibc-2.34-21.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "6e40002c40b2e142dac88fba59d9893054b364585b2bc4b63ebf4cb3066616e2"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "glibc-common", Epoch: 0, Version: "2.34", Release: "21.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/glibc-common-2.34-21.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "606fda6e7bbe188920afcae1529967fc13c10763ed727d8ac6ce1037a8549228"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "glibc-gconv-extra", Epoch: 0, Version: "2.34", Release: "21.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/glibc-gconv-extra-2.34-21.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "cc159162a083a3adf927bcf36fe4c053f3dd3640ff2f7c544018d354e046eccb"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "glibc-minimal-langpack", Epoch: 0, Version: "2.34", Release: "21.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/glibc-minimal-langpack-2.34-21.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "bfec403288415b69acb3fd4bd014561d639673c7002c6968e3722e88cb104bdc"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "gmp", Epoch: 1, Version: "6.2.0", Release: "10.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/gmp-6.2.0-10.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "1a6ededc80029ef258288ddbf24bcce7c6228647841416950c88e3f14b7258a2"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "grep", Epoch: 0, Version: "3.6", Release: "5.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/grep-3.6-5.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "10a41b66b1fbd6eb055178e22c37199e5b49b4852e77c806f7af7211044a4a55"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "gzip", Epoch: 0, Version: "1.10", Release: "8.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/gzip-1.10-8.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "3b5ce98a03a3336a3f32ac7a0867fbc23da702e8618bfd20d49d882d42a460f4"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "json-c", Epoch: 0, Version: "0.14", Release: "11.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/json-c-0.14-11.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "1a75404c6bc8c1369914077dc99480e73bf13a40f15fd1cd8afc792b8600adf8"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "kbd", Epoch: 0, Version: "2.4.0", Release: "8.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/kbd-2.4.0-8.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "9c7395caebf76e15f496d9dc7690d772cb34f29d3f6626086b578565e412df51"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "kbd-misc", Epoch: 0, Version: "2.4.0", Release: "8.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/kbd-misc-2.4.0-8.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "2dda3fe56c9a5bce5880dca58d905682c5e9f94ee023e43a3e311d2d411e1849"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "kernel", Epoch: 0, Version: "5.14.0", Release: "55.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/kernel-5.14.0-55.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "be5dba9121cda9eac9cc8f20b24f7e0d198364b370546c3665e4e6ce70a335b4"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "kernel-core", Epoch: 0, Version: "5.14.0", Release: "55.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/kernel-core-5.14.0-55.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "0afe6e35348485ae2696e6170dcf34370f33fcf42a357fc815e332d939dd1025"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "kernel-modules", Epoch: 0, Version: "5.14.0", Release: "55.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/kernel-modules-5.14.0-55.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "0914a0cbe0304289e224789f8e50e3e48a2525eba742ad764a1901e8c1351fb5"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "kmod", Epoch: 0, Version: "28", Release: "7.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/kmod-28-7.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "3d4bc7935959a109a10020d0d19a5e059719ae4c99c5f32d3020ff6da47d53ea"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "kmod-libs", Epoch: 0, Version: "28", Release: "7.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/kmod-libs-28-7.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "0727ff3131223446158aaec88cbf8f894a9e3592e73f231a1802629518eeb64b"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "kpartx", Epoch: 0, Version: "0.8.7", Release: "4.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/kpartx-0.8.7-4.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "8f05761c418a55f811404dc1515b131bafe9b1e3fe56274be6d880c8822984b5"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libacl", Epoch: 0, Version: "2.3.1", Release: "3.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libacl-2.3.1-3.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "fd829e9a03f6d321313002d6fcb37ee0434f548aa75fcd3ecdbdd891115de6a7"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libattr", Epoch: 0, Version: "2.5.1", Release: "3.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libattr-2.5.1-3.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "d4db095a015e84065f27a642ee7829cd1690041ba8c51501f908cc34760c9409"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libblkid", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libblkid-2.37.2-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "f5cf36e8081c2d72e9dd64dd1614155857dd6e71ebb2237e5b0e11ace5481bac"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libcap", Epoch: 0, Version: "2.48", Release: "8.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libcap-2.48-8.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "c41f91075ee8ca480c2631a485bcc74876b9317b4dc9bd66566da32313621bd7"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libcap-ng", Epoch: 0, Version: "0.8.2", Release: "6.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libcap-ng-0.8.2-6.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "0ee8b2d02fd362223fcf36c11297e1f9ae939f76cef09c0bce9cad5f53287122"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libdb", Epoch: 0, Version: "5.3.28", Release: "53.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libdb-5.3.28-53.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "3a44d15d695944bde4e7290800b815f98bfd9cd6f6f868cec3e8991606f556d5"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libeconf", Epoch: 0, Version: "0.4.1", Release: "2.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libeconf-0.4.1-2.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "1d6fe169e74daff38ad5b0d6424c4d1b14545d5974c39e4421d20838a68f5892"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libevent", Epoch: 0, Version: "2.1.12", Release: "6.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libevent-2.1.12-6.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "82179f6f214ddf523e143c16c3474ccf8832551c6305faf89edfbd83b3424d48"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libfdisk", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libfdisk-2.37.2-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "a41bad6e261c719224abfd6745ccb1d2a0cac9d024ca9656904001a38d7cd8c7"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libffi", Epoch: 0, Version: "3.4.2", Release: "7.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libffi-3.4.2-7.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "f0ac4b6454d4018833dd10e3f437d8271c7c6a628d99b37e75b83af890b86bc4"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libgcc", Epoch: 0, Version: "11.2.1", Release: "9.1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libgcc-11.2.1-9.1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "6fc0ea086ecf7ae65bdfc2e9ba6503ee9d9bf717f3c0a55c4bc9c99e12608edf"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libgcrypt", Epoch: 0, Version: "1.10.0", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libgcrypt-1.10.0-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "059533802d440244c1fb6f777e20ed445220cdc85300e164d8ffb0ecfdfb42f4"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libgpg-error", Epoch: 0, Version: "1.42", Release: "5.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libgpg-error-1.42-5.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "a1883804c376f737109f4dff06077d1912b90150a732d11be7bc5b3b67e512fe"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libkcapi", Epoch: 0, Version: "1.3.1", Release: "3.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libkcapi-1.3.1-3.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "9b4733e8a790b51d845cedfa67e6321fd5a2923dd0fb7ce1f5e630aa382ba3c1"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libkcapi-hmaccalc", Epoch: 0, Version: "1.3.1", Release: "3.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libkcapi-hmaccalc-1.3.1-3.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "1b39f1faa4a8813cbfa2650e82f6ea06a4248e0c493ce7e4829c7d892e7757ed"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libmount", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libmount-2.37.2-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "26191af0cc7acf9bb335ebd8b4ed357582165ee3be78fce9f4395f84ad2805ce"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libpwquality", Epoch: 0, Version: "1.4.4", Release: "8.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libpwquality-1.4.4-8.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "93f00e5efac1e3f1ecbc0d6a4c068772cb12912cd20c9ea58716d6c0cd004886"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libseccomp", Epoch: 0, Version: "2.5.2", Release: "2.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libseccomp-2.5.2-2.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "d5c1c4473ebf5fd9c605eb866118d7428cdec9b188db18e45545801cc2a689c3"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libselinux", Epoch: 0, Version: "3.3", Release: "2.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libselinux-3.3-2.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "8e589b8408b04cbc19564620b229b6768edbaeb9090885d2273d84b8fc2f172b"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libsemanage", Epoch: 0, Version: "3.3", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libsemanage-3.3-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "7e62a0ed0a508486b565e48794a146022f344aeb6801834ad7c5afe6a97ef065"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libsepol", Epoch: 0, Version: "3.3", Release: "2.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libsepol-3.3-2.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "fc508147fe876706b61941a6ce554d7f7786f1ec3d097c4411fd6c7511acd289"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libsigsegv", Epoch: 0, Version: "2.13", Release: "4.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libsigsegv-2.13-4.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "931bd0ec7050e8c3b37a9bfb489e30af32486a3c77203f1e9113eeceaa3b0a3a"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libsmartcols", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libsmartcols-2.37.2-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "c62433784604a2e6571e0fcbdd4a2d60f059c5c15624207998c5f03b18d9d382"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libtasn1", Epoch: 0, Version: "4.16.0", Release: "7.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libtasn1-4.16.0-7.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "656031558c53da4a5b3ccfd883bd6d55996037891323152b1f07e8d1d5377406"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libutempter", Epoch: 0, Version: "1.2.1", Release: "6.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libutempter-1.2.1-6.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "fab361a9cba04490fd8b5664049983d1e57ebf7c1080804726ba600708524125"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libuuid", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libuuid-2.37.2-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "ffd8317ccc6f80524b7bf15a8157d82f36a2b9c7478bb04eb4a34c18d019e6fa"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libxcrypt", Epoch: 0, Version: "4.4.18", Release: "3.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libxcrypt-4.4.18-3.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "97e88678b420f619a44608fff30062086aa1dd6931ecbd54f21bba005ff1de1a"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "libzstd", Epoch: 0, Version: "1.5.0", Release: "2.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/libzstd-1.5.0-2.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "8282f33f06743ab88e36fea978559ac617c44cda14eb65495cad37505fdace41"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "linux-firmware", Epoch: 0, Version: "20211216", Release: "124.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/linux-firmware-20211216-124.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "0524c9cd96db4d57a5c190165ee2f8ade91854e21c19c61c6bd3504030ca24fa"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "linux-firmware-whence", Epoch: 0, Version: "20211216", Release: "124.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/linux-firmware-whence-20211216-124.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "7c58504c14979118ea36352982aaa5814ba0f448e17c1baddb811b9511315a58"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "lz4-libs", Epoch: 0, Version: "1.9.3", Release: "5.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/lz4-libs-1.9.3-5.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "cba6a63054d070956a182e33269ee245bcfbe87e3e605c27816519db762a66ad"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "ncurses-base", Epoch: 0, Version: "6.2", Release: "8.20210508.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/ncurses-base-6.2-8.20210508.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "e4cc4a4a479b8c27776debba5c20e8ef21dc4b513da62a25ed09f88386ac08a8"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "ncurses-libs", Epoch: 0, Version: "6.2", Release: "8.20210508.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/ncurses-libs-6.2-8.20210508.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "328f4d50e66b00f24344ebe239817204fda8e68b1d988c6943abb3c36231beaa"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "openssl", Epoch: 1, Version: "3.0.1", Release: "5.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/openssl-3.0.1-5.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "aa9ee73fe806ddeafab4a5b0e370256e6c61f67f67114101d0735aed3c9a5eda"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "openssl-libs", Epoch: 1, Version: "3.0.1", Release: "5.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/openssl-libs-3.0.1-5.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "fcf2515ec9115551c99d552da721803ecbca23b7ae5a974309975000e8bef666"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "openssl-pkcs11", Epoch: 0, Version: "0.4.11", Release: "7.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/openssl-pkcs11-0.4.11-7.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "4be41142a5fb2b4cd6d812e126838cffa57b7c84e5a79d65f66bb9cf1d2830a3"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "p11-kit", Epoch: 0, Version: "0.24.1", Release: "2.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/p11-kit-0.24.1-2.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "da167e41efd19cf25fd1c708b6f123d0203824324b14dd32401d49f2aa0ef0a6"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "p11-kit-trust", Epoch: 0, Version: "0.24.1", Release: "2.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/p11-kit-trust-0.24.1-2.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "ae9a633c58980328bef6358c6aa3c9ce0a65130c66fbfa4249922ddf5a3e2bb1"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "pam", Epoch: 0, Version: "1.5.1", Release: "9.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/pam-1.5.1-9.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "e64caedce811645ecdd78e7b4ae83c189aa884ff1ba6445374f39186c588c52c"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "pcre", Epoch: 0, Version: "8.44", Release: "3.el9.3", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/pcre-8.44-3.el9.3.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "4a3cb61eb08c4f24e44756b6cb329812fe48d5c65c1fba546fadfa975045a8c5"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "pcre2", Epoch: 0, Version: "10.37", Release: "3.el9.1", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/pcre2-10.37-3.el9.1.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "441e71f24e95b7c319f02264db53f88aa49778b2214f7dd5c75f1a3838e72dea"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "pcre2-syntax", Epoch: 0, Version: "10.37", Release: "3.el9.1", Arch: "noarch", RemoteLocations: []string{"%s/Packages/pcre2-syntax-10.37-3.el9.1.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "55d7d2bc962334c236418b78199a496b05dea4efdc89e52453154bd1a5ad0e2e"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "procps-ng", Epoch: 0, Version: "3.3.17", Release: "4.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/procps-ng-3.3.17-4.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "3a7cc3f6d6dfdaeb9e7bfdb06d968c3ae78246ff4f793c2d2e2bd71156961d69"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "readline", Epoch: 0, Version: "8.1", Release: "4.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/readline-8.1-4.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "49945472925286ad89b0575657b43f9224777e36b442f0c88df67f0b61e26aee"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "sed", Epoch: 0, Version: "4.8", Release: "9.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/sed-4.8-9.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "a2c5d9a7f569abb5a592df1c3aaff0441bf827c9d0e2df0ab42b6c443dbc475f"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "setup", Epoch: 0, Version: "2.13.7", Release: "6.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/setup-2.13.7-6.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "c0202712e8ec928cf61f3d777f23859ba6de2e85786e928ee5472fdde570aeee"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "shadow-utils", Epoch: 2, Version: "4.9", Release: "3.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/shadow-utils-4.9-3.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "46fca2ed21478e5143434da4fbd47ca4599a885fab9f8636f9c7ba54942dd27e"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "systemd", Epoch: 0, Version: "249", Release: "9.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/systemd-249-9.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "eb57c1242f8a7d68e6c258f40b048d8b7bd0749254ac97b7f399b3bc8011a81b"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "systemd-libs", Epoch: 0, Version: "249", Release: "9.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/systemd-libs-249-9.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "708fbc3c7fd77a21e0b391e2a80d5c344962de9865e79514b2c89210ef06ba39"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "systemd-pam", Epoch: 0, Version: "249", Release: "9.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/systemd-pam-249-9.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "eb7af981fb95425c68ccb0e4b95688672afd3032b57002e65fda8f734a089556"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "systemd-rpm-macros", Epoch: 0, Version: "249", Release: "9.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/systemd-rpm-macros-249-9.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "3552f7cc9077d5831f859f6cf721d419eccc83cb381d14a7a1483512272bd586"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "systemd-udev", Epoch: 0, Version: "249", Release: "9.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/systemd-udev-249-9.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "d9c47e7088b8d279b8fd51e2274df957c3b68a265b42123ef9bbeb339d5ce3ba"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "tmux", Epoch: 0, Version: "3.2a", Release: "4.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/tmux-3.2a-4.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "68074b673bfac39af1fbfc85d43bc1c111456db01a563bda6400ad485de5eb70"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "tzdata", Epoch: 0, Version: "2021e", Release: "1.el9", Arch: "noarch", RemoteLocations: []string{"%s/Packages/tzdata-2021e-1.el9.noarch.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "42d89577a0f887c4baa162250862dea2c1830b1ced56c45ced9645ad8e2a3671"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "util-linux", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/util-linux-2.37.2-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "4ca41a925461daa936db284a59bf325ea061cdb39d8738e288cc19afe30a8ae8"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "util-linux-core", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/util-linux-core-2.37.2-1.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "0313682867c1d07785a6d02ff87e1899f484bd1ce6348fa5c673eca78c0da2bd"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "vim-minimal", Epoch: 2, Version: "8.2.2637", Release: "11.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/vim-minimal-8.2.2637-11.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "ab6e48c8a118bed88dc734aaf21e743b57e94d448f9e38745c3b777af96809c7"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "xz", Epoch: 0, Version: "5.2.5", Release: "7.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/xz-5.2.5-7.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "b1c2d99961e50bb46400caa528aab9c7b361f5754427fd05ae22a7b551bf2ce5"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "xz-libs", Epoch: 0, Version: "5.2.5", Release: "7.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/xz-libs-5.2.5-7.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "770819da28cce56e2e2b141b0eee1694d7f3dcf78a5700e1469436461399f001"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "zlib", Epoch: 0, Version: "1.2.11", Release: "31.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/zlib-1.2.11-31.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "1c59b113fda8863e9066cc5d01c6d00bd9c50c4650e1c5b932082c8886e185d1"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - {Name: "zsh", Epoch: 0, Version: "5.8", Release: "7.el9", Arch: "x86_64", RemoteLocations: []string{"%s/Packages/zsh-5.8-7.el9.x86_64.rpm"}, Checksum: rpmmd.Checksum{Type: "sha256", Value: "133da157fbd2b43e4a41af3ba7bb5267cf9ebed0aaf8124a76e5eca948c37572"}, Secrets: "", CheckGPG: false, IgnoreSSL: true}, - } - - exp := rpmmd.PackageList(expectedTemplate) - for idx := range exp { - urlTemplate := exp[idx].RemoteLocations[0] - exp[idx].RemoteLocations[0] = fmt.Sprintf(urlTemplate, strings.Join(repo.BaseURLs, ",")) - exp[idx].Location = strings.TrimPrefix(urlTemplate, "%s/") - exp[idx].RepoID = repo.Id - } - return exp -} + {Name: "acl", Epoch: 0, Version: "2.3.1", Release: "3.el9", Arch: "x86_64", Location: "Packages/acl-2.3.1-3.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "986044c3837eddbc9231d7be5e5fc517e245296978b988a803bc9f9172fe84ea"}}, + {Name: "alternatives", Epoch: 0, Version: "1.20", Release: "2.el9", Arch: "x86_64", Location: "Packages/alternatives-1.20-2.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "1851d5f64ebaeac67c5c2d9e4adc1e73aa6433b44a167268a3510c3d056062db"}}, + {Name: "audit-libs", Epoch: 0, Version: "3.0.7", Release: "100.el9", Arch: "x86_64", Location: "Packages/audit-libs-3.0.7-100.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "a4bdda48abaedffeb74398cd55afbd00cb4153ae24bd2a3e6de9d87462df5ffa"}}, + {Name: "basesystem", Epoch: 0, Version: "11", Release: "13.el9", Arch: "noarch", Location: "Packages/basesystem-11-13.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "a7a687ef39dd28d01d34fab18ea7e3e87f649f6c202dded82260b7ea625b9973"}}, + { + Name: "bash", + Epoch: 0, + Version: "5.1.8", + Release: "2.el9", + Arch: "x86_64", + Group: "Unspecified", + DownloadSize: 1772315, + InstallSize: 7739748, + License: "GPLv3+", + SourceRpm: "bash-5.1.8-2.el9.src.rpm", + BuildTime: time.Date(2021, time.August, 9, 19, 33, 6, 0, time.UTC), + Packager: "builder@centos.org", + Vendor: "CentOS", + URL: "https://www.gnu.org/software/bash", + Summary: "The GNU Bourne Again shell", + Description: "The GNU Bourne Again shell (Bash) is a shell or command language\ninterpreter that is compatible with the Bourne shell (sh). Bash\nincorporates useful features from the Korn shell (ksh) and the C shell\n(csh). Most sh scripts can be run by bash without modification.", + Provides: rpmmd.RelDepList{ + {Name: "/bin/bash"}, + {Name: "/bin/sh"}, + {Name: "bash", Relationship: "=", Version: "5.1.8-2.el9"}, + {Name: "bash(x86-64)", Relationship: "=", Version: "5.1.8-2.el9"}, + {Name: "config(bash)", Relationship: "=", Version: "5.1.8-2.el9"}, + }, + Requires: rpmmd.RelDepList{ + {Name: "filesystem", Relationship: ">=", Version: "3"}, + {Name: "libc.so.6(GLIBC_2.34)(64bit)"}, + {Name: "libtinfo.so.6()(64bit)"}, + {Name: "rtld(GNU_HASH)"}, + }, + RegularRequires: rpmmd.RelDepList{ + {Name: "filesystem", Relationship: ">=", Version: "3"}, + {Name: "libc.so.6(GLIBC_2.34)(64bit)"}, + {Name: "libtinfo.so.6()(64bit)"}, + {Name: "rtld(GNU_HASH)"}, + }, + Files: []string{"/etc/skel/.bash_logout", "/etc/skel/.bash_profile", "/etc/skel/.bashrc", "/usr/bin/alias", "/usr/bin/bash", "/usr/bin/bashbug", "/usr/bin/bashbug-64", "/usr/bin/bg", "/usr/bin/cd", "/usr/bin/command", "/usr/bin/fc", "/usr/bin/fg", "/usr/bin/getopts", "/usr/bin/hash", "/usr/bin/jobs", "/usr/bin/read", "/usr/bin/sh", "/usr/bin/type", "/usr/bin/ulimit", "/usr/bin/umask", "/usr/bin/unalias", "/usr/bin/wait", "/usr/lib/.build-id", "/usr/lib/.build-id/41", "/usr/lib/.build-id/41/61a572a65bba4bbed7ba5a976bedceeb471435", "/usr/share/doc/bash", "/usr/share/doc/bash/FAQ", "/usr/share/doc/bash/INTRO", "/usr/share/doc/bash/RBASH", "/usr/share/doc/bash/README", "/usr/share/doc/bash/bash.html", "/usr/share/doc/bash/bashref.html", "/usr/share/info/bash.info.gz", "/usr/share/licenses/bash", "/usr/share/licenses/bash/COPYING", "/usr/share/locale/af/LC_MESSAGES/bash.mo", "/usr/share/locale/bg/LC_MESSAGES/bash.mo", "/usr/share/locale/ca/LC_MESSAGES/bash.mo", "/usr/share/locale/cs/LC_MESSAGES/bash.mo", "/usr/share/locale/da/LC_MESSAGES/bash.mo", "/usr/share/locale/de/LC_MESSAGES/bash.mo", "/usr/share/locale/el/LC_MESSAGES/bash.mo", "/usr/share/locale/en@boldquot/LC_MESSAGES/bash.mo", "/usr/share/locale/en@quot/LC_MESSAGES/bash.mo", "/usr/share/locale/eo/LC_MESSAGES/bash.mo", "/usr/share/locale/es/LC_MESSAGES/bash.mo", "/usr/share/locale/et/LC_MESSAGES/bash.mo", "/usr/share/locale/fi/LC_MESSAGES/bash.mo", "/usr/share/locale/fr/LC_MESSAGES/bash.mo", "/usr/share/locale/ga/LC_MESSAGES/bash.mo", "/usr/share/locale/gl/LC_MESSAGES/bash.mo", "/usr/share/locale/hr/LC_MESSAGES/bash.mo", "/usr/share/locale/hu/LC_MESSAGES/bash.mo", "/usr/share/locale/id/LC_MESSAGES/bash.mo", "/usr/share/locale/it/LC_MESSAGES/bash.mo", "/usr/share/locale/ja/LC_MESSAGES/bash.mo", "/usr/share/locale/ko/LC_MESSAGES/bash.mo", "/usr/share/locale/lt/LC_MESSAGES/bash.mo", "/usr/share/locale/nb/LC_MESSAGES/bash.mo", "/usr/share/locale/nl/LC_MESSAGES/bash.mo", "/usr/share/locale/pl/LC_MESSAGES/bash.mo", "/usr/share/locale/pt/LC_MESSAGES/bash.mo", "/usr/share/locale/pt_BR/LC_MESSAGES/bash.mo", "/usr/share/locale/ro/LC_MESSAGES/bash.mo", "/usr/share/locale/ru/LC_MESSAGES/bash.mo", "/usr/share/locale/sk/LC_MESSAGES/bash.mo", "/usr/share/locale/sl/LC_MESSAGES/bash.mo", "/usr/share/locale/sr/LC_MESSAGES/bash.mo", "/usr/share/locale/sv/LC_MESSAGES/bash.mo", "/usr/share/locale/tr/LC_MESSAGES/bash.mo", "/usr/share/locale/uk/LC_MESSAGES/bash.mo", "/usr/share/locale/vi/LC_MESSAGES/bash.mo", "/usr/share/locale/zh_CN/LC_MESSAGES/bash.mo", "/usr/share/locale/zh_TW/LC_MESSAGES/bash.mo", "/usr/share/man/man1/..1.gz", "/usr/share/man/man1/:.1.gz", "/usr/share/man/man1/[.1.gz", "/usr/share/man/man1/alias.1.gz", "/usr/share/man/man1/bash.1.gz", "/usr/share/man/man1/bashbug-64.1.gz", "/usr/share/man/man1/bashbug.1.gz", "/usr/share/man/man1/bg.1.gz", "/usr/share/man/man1/bind.1.gz", "/usr/share/man/man1/break.1.gz", "/usr/share/man/man1/builtin.1.gz", "/usr/share/man/man1/builtins.1.gz", "/usr/share/man/man1/caller.1.gz", "/usr/share/man/man1/cd.1.gz", "/usr/share/man/man1/command.1.gz", "/usr/share/man/man1/compgen.1.gz", "/usr/share/man/man1/complete.1.gz", "/usr/share/man/man1/compopt.1.gz", "/usr/share/man/man1/continue.1.gz", "/usr/share/man/man1/declare.1.gz", "/usr/share/man/man1/dirs.1.gz", "/usr/share/man/man1/disown.1.gz", "/usr/share/man/man1/enable.1.gz", "/usr/share/man/man1/eval.1.gz", "/usr/share/man/man1/exec.1.gz", "/usr/share/man/man1/exit.1.gz", "/usr/share/man/man1/export.1.gz", "/usr/share/man/man1/fc.1.gz", "/usr/share/man/man1/fg.1.gz", "/usr/share/man/man1/getopts.1.gz", "/usr/share/man/man1/hash.1.gz", "/usr/share/man/man1/help.1.gz", "/usr/share/man/man1/history.1.gz", "/usr/share/man/man1/jobs.1.gz", "/usr/share/man/man1/let.1.gz", "/usr/share/man/man1/local.1.gz", "/usr/share/man/man1/logout.1.gz", "/usr/share/man/man1/mapfile.1.gz", "/usr/share/man/man1/popd.1.gz", "/usr/share/man/man1/pushd.1.gz", "/usr/share/man/man1/read.1.gz", "/usr/share/man/man1/readonly.1.gz", "/usr/share/man/man1/return.1.gz", "/usr/share/man/man1/set.1.gz", "/usr/share/man/man1/sh.1.gz", "/usr/share/man/man1/shift.1.gz", "/usr/share/man/man1/shopt.1.gz", "/usr/share/man/man1/source.1.gz", "/usr/share/man/man1/suspend.1.gz", "/usr/share/man/man1/times.1.gz", "/usr/share/man/man1/trap.1.gz", "/usr/share/man/man1/type.1.gz", "/usr/share/man/man1/typeset.1.gz", "/usr/share/man/man1/ulimit.1.gz", "/usr/share/man/man1/umask.1.gz", "/usr/share/man/man1/unalias.1.gz", "/usr/share/man/man1/unset.1.gz", "/usr/share/man/man1/wait.1.gz"}, + Location: "Packages/bash-5.1.8-2.el9.x86_64.rpm", + Checksum: rpmmd.Checksum{Type: "sha256", Value: "3d45552ea940db0556dd2dc73e92c20c0d7cbc9e617f251904f20475d4ecc6b6"}, + }, + {Name: "bzip2-libs", Epoch: 0, Version: "1.0.8", Release: "8.el9", Arch: "x86_64", Location: "Packages/bzip2-libs-1.0.8-8.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "fabd6b5c065c2b9d4a8d39a938ae577d801de2ddc73c8cdf6f7803db29c28d0a"}}, + {Name: "ca-certificates", Epoch: 0, Version: "2020.2.50", Release: "94.el9", Arch: "noarch", Location: "Packages/ca-certificates-2020.2.50-94.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "3099471d984fb7d9e1cf42406eb08c154b34b8560742ed1f5eb9139f059c2d09"}}, + {Name: "centos-gpg-keys", Epoch: 0, Version: "9.0", Release: "9.el9", Arch: "noarch", Location: "Packages/centos-gpg-keys-9.0-9.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "2785ab660c124c9bda4ef4057e72d7fc73e8ac254ddd09a5541a6d323740dad7"}}, + {Name: "centos-stream-release", Epoch: 0, Version: "9.0", Release: "9.el9", Arch: "noarch", Location: "Packages/centos-stream-release-9.0-9.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "44246cc9b62ac0fb833ece49cff6ac0a932234fcba26b8c895f42baebf0a19c2"}}, + {Name: "centos-stream-repos", Epoch: 0, Version: "9.0", Release: "9.el9", Arch: "noarch", Location: "Packages/centos-stream-repos-9.0-9.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "90208bb7dd1558a3311a28ea06d75ad7e83be3f223c5fb2eff1b9ac47bb98ebe"}}, + {Name: "coreutils", Epoch: 0, Version: "8.32", Release: "31.el9", Arch: "x86_64", Location: "Packages/coreutils-8.32-31.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "647a3b9a52df25cb2aaf7f3715b219839b4cf71913638c88172d925173280812"}}, + {Name: "coreutils-common", Epoch: 0, Version: "8.32", Release: "31.el9", Arch: "x86_64", Location: "Packages/coreutils-common-8.32-31.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "864b166ac6d55cad5010da369fd7ad4872f81c2c111867dfbf96ccf4c8273c7e"}}, + {Name: "cpio", Epoch: 0, Version: "2.13", Release: "16.el9", Arch: "x86_64", Location: "Packages/cpio-2.13-16.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "216b76d33443b732be42fe1d443e106a17e632ac9ca465928c37a8c0ede596a4"}}, + {Name: "cracklib", Epoch: 0, Version: "2.9.6", Release: "27.el9", Arch: "x86_64", Location: "Packages/cracklib-2.9.6-27.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "be9deb2efd06b4b2c1c130acae94c687161d04830119e65a989d904ba9fd1864"}}, + {Name: "cracklib-dicts", Epoch: 0, Version: "2.9.6", Release: "27.el9", Arch: "x86_64", Location: "Packages/cracklib-dicts-2.9.6-27.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "01df2a72fcdf988132e82764ce1a22a5a9513fa253b54e17d23058bdb53c2d85"}}, + {Name: "crypto-policies", Epoch: 0, Version: "20220203", Release: "1.gitf03e75e.el9", Arch: "noarch", Location: "Packages/crypto-policies-20220203-1.gitf03e75e.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "28d73d3800cb895b265bc0755c41241c593ebd7551a7da7f001f1e254b85c662"}}, + {Name: "cryptsetup-libs", Epoch: 0, Version: "2.4.3", Release: "1.el9", Arch: "x86_64", Location: "Packages/cryptsetup-libs-2.4.3-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "6919d88afdd2cf89982ec8edd4401ff93394a81873f81cf89bb273384f39104f"}}, + {Name: "dbus", Epoch: 1, Version: "1.12.20", Release: "5.el9", Arch: "x86_64", Location: "Packages/dbus-1.12.20-5.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "bb85bd28cc162e98da53b756b988ffd9350f4dbcc186f4c6962ae047e27f83d3"}}, + {Name: "dbus-broker", Epoch: 0, Version: "28", Release: "5.el9", Arch: "x86_64", Location: "Packages/dbus-broker-28-5.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "e9efdcdcfe430e474e3a7f09596a0a5a4314692d9ae846bb1ca86ff88ef81038"}}, + {Name: "dbus-common", Epoch: 1, Version: "1.12.20", Release: "5.el9", Arch: "noarch", Location: "Packages/dbus-common-1.12.20-5.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "150048b6fdafd4271bd6badab3f8a2e56b86967266f890770eab7578289cc773"}}, + {Name: "device-mapper", Epoch: 9, Version: "1.02.181", Release: "3.el9", Arch: "x86_64", Location: "Packages/device-mapper-1.02.181-3.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "5d1cd7733f147020ef3a9e08fa2e9d74a25e0ac89dfbadc69912541286146feb"}}, + {Name: "device-mapper-libs", Epoch: 9, Version: "1.02.181", Release: "3.el9", Arch: "x86_64", Location: "Packages/device-mapper-libs-1.02.181-3.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "a716ccca85fad2885af4d099f8c213eb4617d637d8ca6cf7d2b483b9de88a5d3"}}, + {Name: "dracut", Epoch: 0, Version: "055", Release: "10.git20210824.el9", Arch: "x86_64", Location: "Packages/dracut-055-10.git20210824.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "54015283e7f85fbee9d8a814c3bd60c7f81a6eb2ff480ca689e4526637a81c83"}}, + {Name: "elfutils-default-yama-scope", Epoch: 0, Version: "0.186", Release: "1.el9", Arch: "noarch", Location: "Packages/elfutils-default-yama-scope-0.186-1.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "0d2dcfaa16f83de78a251cf0b9a4c5e4ec7d4deb2e8d1cae7209be7745fabeb5"}}, + {Name: "elfutils-libelf", Epoch: 0, Version: "0.186", Release: "1.el9", Arch: "x86_64", Location: "Packages/elfutils-libelf-0.186-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "0e295e6150b6929408ac29792ec5f3ebeb4a20607eb553177f0e4899b3008d63"}}, + {Name: "elfutils-libs", Epoch: 0, Version: "0.186", Release: "1.el9", Arch: "x86_64", Location: "Packages/elfutils-libs-0.186-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "bcc47b8ab496d3d11d772b037e022bc3a4ce3b080b7d1c24fa7f999426a6b8f3"}}, + {Name: "expat", Epoch: 0, Version: "2.2.10", Release: "5.el9", Arch: "x86_64", Location: "Packages/expat-2.2.10-5.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "f97cd3c1e79b4dfff232ba0208c2e1a7d557608c1c37e8303de4f75387be9bb7"}}, + {Name: "filesystem", Epoch: 0, Version: "3.16", Release: "2.el9", Arch: "x86_64", Location: "Packages/filesystem-3.16-2.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "b69a472751268a1b9acd566dc7aa486fc1d6c8cb6d23f36d6a6dfead62e71475"}}, + {Name: "findutils", Epoch: 1, Version: "4.8.0", Release: "5.el9", Arch: "x86_64", Location: "Packages/findutils-4.8.0-5.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "552548e6d6f9623ccd9d31bb185bba3a66730da6e9d02296b417d501356c3848"}}, + {Name: "gdbm-libs", Epoch: 1, Version: "1.19", Release: "4.el9", Arch: "x86_64", Location: "Packages/gdbm-libs-1.19-4.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "8cd5a78cab8783dd241c52c4fcda28fb111c443887dd6d0fe38385e8383c98b3"}}, + {Name: "glibc", Epoch: 0, Version: "2.34", Release: "21.el9", Arch: "x86_64", Location: "Packages/glibc-2.34-21.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "6e40002c40b2e142dac88fba59d9893054b364585b2bc4b63ebf4cb3066616e2"}}, + {Name: "glibc-common", Epoch: 0, Version: "2.34", Release: "21.el9", Arch: "x86_64", Location: "Packages/glibc-common-2.34-21.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "606fda6e7bbe188920afcae1529967fc13c10763ed727d8ac6ce1037a8549228"}}, + {Name: "glibc-gconv-extra", Epoch: 0, Version: "2.34", Release: "21.el9", Arch: "x86_64", Location: "Packages/glibc-gconv-extra-2.34-21.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "cc159162a083a3adf927bcf36fe4c053f3dd3640ff2f7c544018d354e046eccb"}}, + {Name: "glibc-minimal-langpack", Epoch: 0, Version: "2.34", Release: "21.el9", Arch: "x86_64", Location: "Packages/glibc-minimal-langpack-2.34-21.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "bfec403288415b69acb3fd4bd014561d639673c7002c6968e3722e88cb104bdc"}}, + {Name: "gmp", Epoch: 1, Version: "6.2.0", Release: "10.el9", Arch: "x86_64", Location: "Packages/gmp-6.2.0-10.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "1a6ededc80029ef258288ddbf24bcce7c6228647841416950c88e3f14b7258a2"}}, + {Name: "grep", Epoch: 0, Version: "3.6", Release: "5.el9", Arch: "x86_64", Location: "Packages/grep-3.6-5.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "10a41b66b1fbd6eb055178e22c37199e5b49b4852e77c806f7af7211044a4a55"}}, + {Name: "gzip", Epoch: 0, Version: "1.10", Release: "8.el9", Arch: "x86_64", Location: "Packages/gzip-1.10-8.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "3b5ce98a03a3336a3f32ac7a0867fbc23da702e8618bfd20d49d882d42a460f4"}}, + {Name: "json-c", Epoch: 0, Version: "0.14", Release: "11.el9", Arch: "x86_64", Location: "Packages/json-c-0.14-11.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "1a75404c6bc8c1369914077dc99480e73bf13a40f15fd1cd8afc792b8600adf8"}}, + {Name: "kbd", Epoch: 0, Version: "2.4.0", Release: "8.el9", Arch: "x86_64", Location: "Packages/kbd-2.4.0-8.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "9c7395caebf76e15f496d9dc7690d772cb34f29d3f6626086b578565e412df51"}}, + {Name: "kbd-misc", Epoch: 0, Version: "2.4.0", Release: "8.el9", Arch: "noarch", Location: "Packages/kbd-misc-2.4.0-8.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "2dda3fe56c9a5bce5880dca58d905682c5e9f94ee023e43a3e311d2d411e1849"}}, + {Name: "kernel", Epoch: 0, Version: "5.14.0", Release: "55.el9", Arch: "x86_64", Location: "Packages/kernel-5.14.0-55.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "be5dba9121cda9eac9cc8f20b24f7e0d198364b370546c3665e4e6ce70a335b4"}}, + {Name: "kernel-core", Epoch: 0, Version: "5.14.0", Release: "55.el9", Arch: "x86_64", Location: "Packages/kernel-core-5.14.0-55.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "0afe6e35348485ae2696e6170dcf34370f33fcf42a357fc815e332d939dd1025"}}, + {Name: "kernel-modules", Epoch: 0, Version: "5.14.0", Release: "55.el9", Arch: "x86_64", Location: "Packages/kernel-modules-5.14.0-55.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "0914a0cbe0304289e224789f8e50e3e48a2525eba742ad764a1901e8c1351fb5"}}, + {Name: "kmod", Epoch: 0, Version: "28", Release: "7.el9", Arch: "x86_64", Location: "Packages/kmod-28-7.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "3d4bc7935959a109a10020d0d19a5e059719ae4c99c5f32d3020ff6da47d53ea"}}, + {Name: "kmod-libs", Epoch: 0, Version: "28", Release: "7.el9", Arch: "x86_64", Location: "Packages/kmod-libs-28-7.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "0727ff3131223446158aaec88cbf8f894a9e3592e73f231a1802629518eeb64b"}}, + {Name: "kpartx", Epoch: 0, Version: "0.8.7", Release: "4.el9", Arch: "x86_64", Location: "Packages/kpartx-0.8.7-4.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "8f05761c418a55f811404dc1515b131bafe9b1e3fe56274be6d880c8822984b5"}}, + {Name: "libacl", Epoch: 0, Version: "2.3.1", Release: "3.el9", Arch: "x86_64", Location: "Packages/libacl-2.3.1-3.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "fd829e9a03f6d321313002d6fcb37ee0434f548aa75fcd3ecdbdd891115de6a7"}}, + {Name: "libattr", Epoch: 0, Version: "2.5.1", Release: "3.el9", Arch: "x86_64", Location: "Packages/libattr-2.5.1-3.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "d4db095a015e84065f27a642ee7829cd1690041ba8c51501f908cc34760c9409"}}, + {Name: "libblkid", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", Location: "Packages/libblkid-2.37.2-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "f5cf36e8081c2d72e9dd64dd1614155857dd6e71ebb2237e5b0e11ace5481bac"}}, + {Name: "libcap", Epoch: 0, Version: "2.48", Release: "8.el9", Arch: "x86_64", Location: "Packages/libcap-2.48-8.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "c41f91075ee8ca480c2631a485bcc74876b9317b4dc9bd66566da32313621bd7"}}, + {Name: "libcap-ng", Epoch: 0, Version: "0.8.2", Release: "6.el9", Arch: "x86_64", Location: "Packages/libcap-ng-0.8.2-6.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "0ee8b2d02fd362223fcf36c11297e1f9ae939f76cef09c0bce9cad5f53287122"}}, + {Name: "libdb", Epoch: 0, Version: "5.3.28", Release: "53.el9", Arch: "x86_64", Location: "Packages/libdb-5.3.28-53.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "3a44d15d695944bde4e7290800b815f98bfd9cd6f6f868cec3e8991606f556d5"}}, + {Name: "libeconf", Epoch: 0, Version: "0.4.1", Release: "2.el9", Arch: "x86_64", Location: "Packages/libeconf-0.4.1-2.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "1d6fe169e74daff38ad5b0d6424c4d1b14545d5974c39e4421d20838a68f5892"}}, + {Name: "libevent", Epoch: 0, Version: "2.1.12", Release: "6.el9", Arch: "x86_64", Location: "Packages/libevent-2.1.12-6.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "82179f6f214ddf523e143c16c3474ccf8832551c6305faf89edfbd83b3424d48"}}, + {Name: "libfdisk", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", Location: "Packages/libfdisk-2.37.2-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "a41bad6e261c719224abfd6745ccb1d2a0cac9d024ca9656904001a38d7cd8c7"}}, + {Name: "libffi", Epoch: 0, Version: "3.4.2", Release: "7.el9", Arch: "x86_64", Location: "Packages/libffi-3.4.2-7.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "f0ac4b6454d4018833dd10e3f437d8271c7c6a628d99b37e75b83af890b86bc4"}}, + {Name: "libgcc", Epoch: 0, Version: "11.2.1", Release: "9.1.el9", Arch: "x86_64", Location: "Packages/libgcc-11.2.1-9.1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "6fc0ea086ecf7ae65bdfc2e9ba6503ee9d9bf717f3c0a55c4bc9c99e12608edf"}}, + {Name: "libgcrypt", Epoch: 0, Version: "1.10.0", Release: "1.el9", Arch: "x86_64", Location: "Packages/libgcrypt-1.10.0-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "059533802d440244c1fb6f777e20ed445220cdc85300e164d8ffb0ecfdfb42f4"}}, + {Name: "libgpg-error", Epoch: 0, Version: "1.42", Release: "5.el9", Arch: "x86_64", Location: "Packages/libgpg-error-1.42-5.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "a1883804c376f737109f4dff06077d1912b90150a732d11be7bc5b3b67e512fe"}}, + {Name: "libkcapi", Epoch: 0, Version: "1.3.1", Release: "3.el9", Arch: "x86_64", Location: "Packages/libkcapi-1.3.1-3.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "9b4733e8a790b51d845cedfa67e6321fd5a2923dd0fb7ce1f5e630aa382ba3c1"}}, + {Name: "libkcapi-hmaccalc", Epoch: 0, Version: "1.3.1", Release: "3.el9", Arch: "x86_64", Location: "Packages/libkcapi-hmaccalc-1.3.1-3.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "1b39f1faa4a8813cbfa2650e82f6ea06a4248e0c493ce7e4829c7d892e7757ed"}}, + {Name: "libmount", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", Location: "Packages/libmount-2.37.2-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "26191af0cc7acf9bb335ebd8b4ed357582165ee3be78fce9f4395f84ad2805ce"}}, + {Name: "libpwquality", Epoch: 0, Version: "1.4.4", Release: "8.el9", Arch: "x86_64", Location: "Packages/libpwquality-1.4.4-8.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "93f00e5efac1e3f1ecbc0d6a4c068772cb12912cd20c9ea58716d6c0cd004886"}}, + {Name: "libseccomp", Epoch: 0, Version: "2.5.2", Release: "2.el9", Arch: "x86_64", Location: "Packages/libseccomp-2.5.2-2.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "d5c1c4473ebf5fd9c605eb866118d7428cdec9b188db18e45545801cc2a689c3"}}, + {Name: "libselinux", Epoch: 0, Version: "3.3", Release: "2.el9", Arch: "x86_64", Location: "Packages/libselinux-3.3-2.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "8e589b8408b04cbc19564620b229b6768edbaeb9090885d2273d84b8fc2f172b"}}, + {Name: "libsemanage", Epoch: 0, Version: "3.3", Release: "1.el9", Arch: "x86_64", Location: "Packages/libsemanage-3.3-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "7e62a0ed0a508486b565e48794a146022f344aeb6801834ad7c5afe6a97ef065"}}, + {Name: "libsepol", Epoch: 0, Version: "3.3", Release: "2.el9", Arch: "x86_64", Location: "Packages/libsepol-3.3-2.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "fc508147fe876706b61941a6ce554d7f7786f1ec3d097c4411fd6c7511acd289"}}, + {Name: "libsigsegv", Epoch: 0, Version: "2.13", Release: "4.el9", Arch: "x86_64", Location: "Packages/libsigsegv-2.13-4.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "931bd0ec7050e8c3b37a9bfb489e30af32486a3c77203f1e9113eeceaa3b0a3a"}}, + {Name: "libsmartcols", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", Location: "Packages/libsmartcols-2.37.2-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "c62433784604a2e6571e0fcbdd4a2d60f059c5c15624207998c5f03b18d9d382"}}, + {Name: "libtasn1", Epoch: 0, Version: "4.16.0", Release: "7.el9", Arch: "x86_64", Location: "Packages/libtasn1-4.16.0-7.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "656031558c53da4a5b3ccfd883bd6d55996037891323152b1f07e8d1d5377406"}}, + {Name: "libutempter", Epoch: 0, Version: "1.2.1", Release: "6.el9", Arch: "x86_64", Location: "Packages/libutempter-1.2.1-6.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "fab361a9cba04490fd8b5664049983d1e57ebf7c1080804726ba600708524125"}}, + {Name: "libuuid", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", Location: "Packages/libuuid-2.37.2-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "ffd8317ccc6f80524b7bf15a8157d82f36a2b9c7478bb04eb4a34c18d019e6fa"}}, + {Name: "libxcrypt", Epoch: 0, Version: "4.4.18", Release: "3.el9", Arch: "x86_64", Location: "Packages/libxcrypt-4.4.18-3.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "97e88678b420f619a44608fff30062086aa1dd6931ecbd54f21bba005ff1de1a"}}, + {Name: "libzstd", Epoch: 0, Version: "1.5.0", Release: "2.el9", Arch: "x86_64", Location: "Packages/libzstd-1.5.0-2.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "8282f33f06743ab88e36fea978559ac617c44cda14eb65495cad37505fdace41"}}, + {Name: "linux-firmware", Epoch: 0, Version: "20211216", Release: "124.el9", Arch: "noarch", Location: "Packages/linux-firmware-20211216-124.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "0524c9cd96db4d57a5c190165ee2f8ade91854e21c19c61c6bd3504030ca24fa"}}, + {Name: "linux-firmware-whence", Epoch: 0, Version: "20211216", Release: "124.el9", Arch: "noarch", Location: "Packages/linux-firmware-whence-20211216-124.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "7c58504c14979118ea36352982aaa5814ba0f448e17c1baddb811b9511315a58"}}, + {Name: "lz4-libs", Epoch: 0, Version: "1.9.3", Release: "5.el9", Arch: "x86_64", Location: "Packages/lz4-libs-1.9.3-5.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "cba6a63054d070956a182e33269ee245bcfbe87e3e605c27816519db762a66ad"}}, + {Name: "ncurses-base", Epoch: 0, Version: "6.2", Release: "8.20210508.el9", Arch: "noarch", Location: "Packages/ncurses-base-6.2-8.20210508.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "e4cc4a4a479b8c27776debba5c20e8ef21dc4b513da62a25ed09f88386ac08a8"}}, + {Name: "ncurses-libs", Epoch: 0, Version: "6.2", Release: "8.20210508.el9", Arch: "x86_64", Location: "Packages/ncurses-libs-6.2-8.20210508.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "328f4d50e66b00f24344ebe239817204fda8e68b1d988c6943abb3c36231beaa"}}, + {Name: "openssl", Epoch: 1, Version: "3.0.1", Release: "5.el9", Arch: "x86_64", Location: "Packages/openssl-3.0.1-5.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "aa9ee73fe806ddeafab4a5b0e370256e6c61f67f67114101d0735aed3c9a5eda"}}, + {Name: "openssl-libs", Epoch: 1, Version: "3.0.1", Release: "5.el9", Arch: "x86_64", Location: "Packages/openssl-libs-3.0.1-5.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "fcf2515ec9115551c99d552da721803ecbca23b7ae5a974309975000e8bef666"}}, + {Name: "openssl-pkcs11", Epoch: 0, Version: "0.4.11", Release: "7.el9", Arch: "x86_64", Location: "Packages/openssl-pkcs11-0.4.11-7.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "4be41142a5fb2b4cd6d812e126838cffa57b7c84e5a79d65f66bb9cf1d2830a3"}}, + {Name: "p11-kit", Epoch: 0, Version: "0.24.1", Release: "2.el9", Arch: "x86_64", Location: "Packages/p11-kit-0.24.1-2.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "da167e41efd19cf25fd1c708b6f123d0203824324b14dd32401d49f2aa0ef0a6"}}, + {Name: "p11-kit-trust", Epoch: 0, Version: "0.24.1", Release: "2.el9", Arch: "x86_64", Location: "Packages/p11-kit-trust-0.24.1-2.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "ae9a633c58980328bef6358c6aa3c9ce0a65130c66fbfa4249922ddf5a3e2bb1"}}, + {Name: "pam", Epoch: 0, Version: "1.5.1", Release: "9.el9", Arch: "x86_64", Location: "Packages/pam-1.5.1-9.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "e64caedce811645ecdd78e7b4ae83c189aa884ff1ba6445374f39186c588c52c"}}, + {Name: "pcre", Epoch: 0, Version: "8.44", Release: "3.el9.3", Arch: "x86_64", Location: "Packages/pcre-8.44-3.el9.3.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "4a3cb61eb08c4f24e44756b6cb329812fe48d5c65c1fba546fadfa975045a8c5"}}, + {Name: "pcre2", Epoch: 0, Version: "10.37", Release: "3.el9.1", Arch: "x86_64", Location: "Packages/pcre2-10.37-3.el9.1.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "441e71f24e95b7c319f02264db53f88aa49778b2214f7dd5c75f1a3838e72dea"}}, + {Name: "pcre2-syntax", Epoch: 0, Version: "10.37", Release: "3.el9.1", Arch: "noarch", Location: "Packages/pcre2-syntax-10.37-3.el9.1.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "55d7d2bc962334c236418b78199a496b05dea4efdc89e52453154bd1a5ad0e2e"}}, + {Name: "procps-ng", Epoch: 0, Version: "3.3.17", Release: "4.el9", Arch: "x86_64", Location: "Packages/procps-ng-3.3.17-4.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "3a7cc3f6d6dfdaeb9e7bfdb06d968c3ae78246ff4f793c2d2e2bd71156961d69"}}, + {Name: "readline", Epoch: 0, Version: "8.1", Release: "4.el9", Arch: "x86_64", Location: "Packages/readline-8.1-4.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "49945472925286ad89b0575657b43f9224777e36b442f0c88df67f0b61e26aee"}}, + {Name: "sed", Epoch: 0, Version: "4.8", Release: "9.el9", Arch: "x86_64", Location: "Packages/sed-4.8-9.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "a2c5d9a7f569abb5a592df1c3aaff0441bf827c9d0e2df0ab42b6c443dbc475f"}}, + {Name: "setup", Epoch: 0, Version: "2.13.7", Release: "6.el9", Arch: "noarch", Location: "Packages/setup-2.13.7-6.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "c0202712e8ec928cf61f3d777f23859ba6de2e85786e928ee5472fdde570aeee"}}, + {Name: "shadow-utils", Epoch: 2, Version: "4.9", Release: "3.el9", Arch: "x86_64", Location: "Packages/shadow-utils-4.9-3.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "46fca2ed21478e5143434da4fbd47ca4599a885fab9f8636f9c7ba54942dd27e"}}, + {Name: "systemd", Epoch: 0, Version: "249", Release: "9.el9", Arch: "x86_64", Location: "Packages/systemd-249-9.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "eb57c1242f8a7d68e6c258f40b048d8b7bd0749254ac97b7f399b3bc8011a81b"}}, + {Name: "systemd-libs", Epoch: 0, Version: "249", Release: "9.el9", Arch: "x86_64", Location: "Packages/systemd-libs-249-9.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "708fbc3c7fd77a21e0b391e2a80d5c344962de9865e79514b2c89210ef06ba39"}}, + {Name: "systemd-pam", Epoch: 0, Version: "249", Release: "9.el9", Arch: "x86_64", Location: "Packages/systemd-pam-249-9.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "eb7af981fb95425c68ccb0e4b95688672afd3032b57002e65fda8f734a089556"}}, + {Name: "systemd-rpm-macros", Epoch: 0, Version: "249", Release: "9.el9", Arch: "noarch", Location: "Packages/systemd-rpm-macros-249-9.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "3552f7cc9077d5831f859f6cf721d419eccc83cb381d14a7a1483512272bd586"}}, + {Name: "systemd-udev", Epoch: 0, Version: "249", Release: "9.el9", Arch: "x86_64", Location: "Packages/systemd-udev-249-9.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "d9c47e7088b8d279b8fd51e2274df957c3b68a265b42123ef9bbeb339d5ce3ba"}}, + {Name: "tmux", Epoch: 0, Version: "3.2a", Release: "4.el9", Arch: "x86_64", Location: "Packages/tmux-3.2a-4.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "68074b673bfac39af1fbfc85d43bc1c111456db01a563bda6400ad485de5eb70"}}, + {Name: "tzdata", Epoch: 0, Version: "2021e", Release: "1.el9", Arch: "noarch", Location: "Packages/tzdata-2021e-1.el9.noarch.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "42d89577a0f887c4baa162250862dea2c1830b1ced56c45ced9645ad8e2a3671"}}, + {Name: "util-linux", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", Location: "Packages/util-linux-2.37.2-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "4ca41a925461daa936db284a59bf325ea061cdb39d8738e288cc19afe30a8ae8"}}, + {Name: "util-linux-core", Epoch: 0, Version: "2.37.2", Release: "1.el9", Arch: "x86_64", Location: "Packages/util-linux-core-2.37.2-1.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "0313682867c1d07785a6d02ff87e1899f484bd1ce6348fa5c673eca78c0da2bd"}}, + {Name: "vim-minimal", Epoch: 2, Version: "8.2.2637", Release: "11.el9", Arch: "x86_64", Location: "Packages/vim-minimal-8.2.2637-11.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "ab6e48c8a118bed88dc734aaf21e743b57e94d448f9e38745c3b777af96809c7"}}, + {Name: "xz", Epoch: 0, Version: "5.2.5", Release: "7.el9", Arch: "x86_64", Location: "Packages/xz-5.2.5-7.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "b1c2d99961e50bb46400caa528aab9c7b361f5754427fd05ae22a7b551bf2ce5"}}, + {Name: "xz-libs", Epoch: 0, Version: "5.2.5", Release: "7.el9", Arch: "x86_64", Location: "Packages/xz-libs-5.2.5-7.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "770819da28cce56e2e2b141b0eee1694d7f3dcf78a5700e1469436461399f001"}}, + {Name: "zlib", Epoch: 0, Version: "1.2.11", Release: "31.el9", Arch: "x86_64", Location: "Packages/zlib-1.2.11-31.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "1c59b113fda8863e9066cc5d01c6d00bd9c50c4650e1c5b932082c8886e185d1"}}, + {Name: "zsh", Epoch: 0, Version: "5.8", Release: "7.el9", Arch: "x86_64", Location: "Packages/zsh-5.8-7.el9.x86_64.rpm", Checksum: rpmmd.Checksum{Type: "sha256", Value: "133da157fbd2b43e4a41af3ba7bb5267cf9ebed0aaf8124a76e5eca948c37572"}}, + } -func TestErrorRepoInfo(t *testing.T) { - if !*forceDNF { - // dnf tests aren't forced: skip them if the dnf sniff check fails - if findDepsolveDnf() == "" { - t.Skip("Test needs an installed osbuild-depsolve-dnf") + for idx := range expectedTemplate { + for _, baseurl := range repo.BaseURLs { + expectedTemplate[idx].RemoteLocations = append(expectedTemplate[idx].RemoteLocations, fmt.Sprintf("%s/%s", baseurl, expectedTemplate[idx].Location)) + } + if repo.CheckGPG != nil { + expectedTemplate[idx].CheckGPG = *repo.CheckGPG + } + if repo.IgnoreSSL != nil { + expectedTemplate[idx].IgnoreSSL = *repo.IgnoreSSL } + expectedTemplate[idx].RepoID = repo.Id } + return expectedTemplate +} - assert := assert.New(t) +func TestErrorRepoInfo(t *testing.T) { + requireDNF(t) type testCase struct { repo rpmmd.RepoConfig @@ -803,98 +1010,127 @@ 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{ - { - Include: []string{"osbuild"}, - Exclude: nil, - Repositories: []rpmmd.RepoConfig{tc.repo}, - }, - }, sbom.StandardTypeNone) - assert.Error(err) - assert.Contains(err.Error(), tc.expMsg) + for _, h := range getTestHandlers() { + t.Run(h.name, func(t *testing.T) { + restore := mockActiveHandler(h.handler) + defer restore() + + assert := assert.New(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{ + { + Include: []string{"osbuild"}, + Exclude: nil, + Repositories: []rpmmd.RepoConfig{tc.repo}, + }, + }, sbom.StandardTypeNone) + assert.Error(err) + assert.Contains(err.Error(), tc.expMsg) + }) + } }) } } func TestHashRequest(t *testing.T) { - solver := NewSolver("platform:f38", "38", "x86_64", "fedora-38", "/tmp/cache") - repos := []rpmmd.RepoConfig{ - rpmmd.RepoConfig{ - Name: "A test repository", - BaseURLs: []string{"https://arepourl/"}, - IgnoreSSL: common.ToPtr(false), - }, - } + for _, h := range getTestHandlers() { + t.Run(h.name, func(t *testing.T) { + restore := mockActiveHandler(h.handler) + defer restore() - req, err := activeHandler.makeDumpRequest(solver.solverCfg(), repos) - assert.Nil(t, err) - reqData, err := json.Marshal(req) - if err != nil { - t.Fatalf("marshalling dump request failed: %v", err) - } - reqHash := hashRequest(reqData) - assert.Equal(t, 64, len(reqHash)) + solver := NewSolver("platform:f38", "38", "x86_64", "fedora-38", "/tmp/cache") + repos := []rpmmd.RepoConfig{ + { + Name: "A test repository", + BaseURLs: []string{"https://arepourl/"}, + IgnoreSSL: common.ToPtr(false), + }, + } - req2, err := activeHandler.makeSearchRequest(solver.solverCfg(), repos, []string{"package0*"}) - assert.Nil(t, err) - reqData2, err := json.Marshal(req2) - if err != nil { - t.Fatalf("marshalling search request failed: %v", err) + req, err := activeHandler.makeDumpRequest(solver.solverCfg(), repos) + assert.Nil(t, err) + reqData, err := json.Marshal(req) + if err != nil { + t.Fatalf("marshalling dump request failed: %v", err) + } + reqHash := hashRequest(reqData) + assert.Equal(t, 64, len(reqHash)) + + req2, err := activeHandler.makeSearchRequest(solver.solverCfg(), repos, []string{"package0*"}) + assert.Nil(t, err) + reqData2, err := json.Marshal(req2) + if err != nil { + t.Fatalf("marshalling search request failed: %v", err) + } + reqHash2 := hashRequest(reqData2) + assert.Equal(t, 64, len(reqHash2)) + assert.NotEqual(t, reqHash, reqHash2) + }) } - reqHash2 := hashRequest(reqData2) - assert.Equal(t, 64, len(reqHash2)) - assert.NotEqual(t, reqHash, reqHash2) } func TestSolverRunErrorEmptyOutput(t *testing.T) { - fakeSolverPath := filepath.Join(t.TempDir(), "osbuild-depsolve-dnf") - fakeSolver := `#!/bin/sh -e + for _, h := range getTestHandlers() { + t.Run(h.name, func(t *testing.T) { + restore := mockActiveHandler(h.handler) + defer restore() + + fakeSolverPath := filepath.Join(t.TempDir(), "osbuild-depsolve-dnf") + fakeSolver := `#!/bin/sh -e cat - > "$0".stdin exit 1 ` - err := os.WriteFile(fakeSolverPath, []byte(fakeSolver), 0o755) - assert.NoError(t, err) + err := os.WriteFile(fakeSolverPath, []byte(fakeSolver), 0o755) + assert.NoError(t, err) - solver := NewSolver("platform:f38", "38", "x86_64", "fedora-38", t.TempDir()) - solver.depsolveDNFCmd = []string{fakeSolverPath} - res, err := solver.Depsolve(nil, sbom.StandardTypeNone) + solver := NewSolver("platform:f38", "38", "x86_64", "fedora-38", t.TempDir()) + solver.depsolveDNFCmd = []string{fakeSolverPath} + res, err := solver.Depsolve(nil, sbom.StandardTypeNone) - assert.EqualError(t, err, `DNF error occurred: InternalError: osbuild-depsolve-dnf output was empty`) - assert.Nil(t, res) + assert.EqualError(t, err, `DNF error occurred: InternalError: osbuild-depsolve-dnf output was empty`) + assert.Nil(t, res) + }) + } } func TestSolverRunWithSolverNoError(t *testing.T) { - tmpdir := t.TempDir() - fakeSolver := `#!/bin/sh -e + for _, h := range getTestHandlers() { + t.Run(h.name, func(t *testing.T) { + restore := mockActiveHandler(h.handler) + defer restore() + + tmpdir := t.TempDir() + fakeSolver := `#!/bin/sh -e cat - > "$0".stdin echo '{"solver": "zypper"}' >&2 echo "output-on-stderr" ` - fakeSolverPath := filepath.Join(tmpdir, "fake-solver") - err := os.WriteFile(fakeSolverPath, []byte(fakeSolver), 0755) //nolint:gosec - assert.NoError(t, err) - - var capturedStderr bytes.Buffer - solver := NewSolver("platform:f38", "38", "x86_64", "fedora-38", "/tmp/cache") - solver.Stderr = &capturedStderr - solver.depsolveDNFCmd = []string{fakeSolverPath} - res, err := solver.Depsolve(nil, sbom.StandardTypeNone) - assert.NoError(t, err) - assert.NotNil(t, res) - assert.Equal(t, "output-on-stderr\n", capturedStderr.String()) - - // prerequisite check, i.e. ensure our fake was called in the right way - stdin, err := os.ReadFile(fakeSolverPath + ".stdin") - assert.NoError(t, err) - assert.Contains(t, string(stdin), `"command":"depsolve"`) - - // adding the "solver" did not cause any issues - assert.NoError(t, err) - assert.Equal(t, 0, len(res.Packages)) - assert.Equal(t, 0, len(res.Repos)) + fakeSolverPath := filepath.Join(tmpdir, "fake-solver") + err := os.WriteFile(fakeSolverPath, []byte(fakeSolver), 0755) //nolint:gosec + assert.NoError(t, err) + + var capturedStderr bytes.Buffer + solver := NewSolver("platform:f38", "38", "x86_64", "fedora-38", "/tmp/cache") + solver.Stderr = &capturedStderr + solver.depsolveDNFCmd = []string{fakeSolverPath} + res, err := solver.Depsolve(nil, sbom.StandardTypeNone) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, "output-on-stderr\n", capturedStderr.String()) + + // prerequisite check, i.e. ensure our fake was called in the right way + stdin, err := os.ReadFile(fakeSolverPath + ".stdin") + assert.NoError(t, err) + assert.Contains(t, string(stdin), `"command":"depsolve"`) + + // adding the "solver" did not cause any issues + assert.NoError(t, err) + assert.Equal(t, 0, len(res.Packages)) + assert.Equal(t, 0, len(res.Repos)) + }) + } } func TestDepsolverSubscriptionsError(t *testing.T) { @@ -902,25 +1138,33 @@ func TestDepsolverSubscriptionsError(t *testing.T) { t.Skip("Test must run on unsubscribed system") } - tmpdir := t.TempDir() - solver := NewSolver("platform:el9", "9", "x86_64", "rhel9.0", tmpdir) - - rootDir := t.TempDir() - reposDir := filepath.Join(rootDir, "etc", "yum.repos.d") - require.NoError(t, os.MkdirAll(reposDir, 0777)) - s := rpmrepo.NewTestServer() defer s.Close() - s.WriteConfig(filepath.Join(reposDir, "test.repo")) - s.RepoConfig.RHSM = true - pkgsets := []rpmmd.PackageSet{ - { - Include: []string{"kernel"}, - Repositories: []rpmmd.RepoConfig{s.RepoConfig}, - }, + for _, h := range getTestHandlers() { + t.Run(h.name, func(t *testing.T) { + restore := mockActiveHandler(h.handler) + defer restore() + + tmpdir := t.TempDir() + solver := NewSolver("platform:el9", "9", "x86_64", "rhel9.0", tmpdir) + + rootDir := t.TempDir() + reposDir := filepath.Join(rootDir, "etc", "yum.repos.d") + require.NoError(t, os.MkdirAll(reposDir, 0777)) + + s.WriteConfig(filepath.Join(reposDir, "test.repo")) + s.RepoConfig.RHSM = true + + pkgsets := []rpmmd.PackageSet{ + { + Include: []string{"kernel"}, + Repositories: []rpmmd.RepoConfig{s.RepoConfig}, + }, + } + solver.SetRootDir(rootDir) + _, err := solver.Depsolve(pkgsets, 0) + assert.EqualError(t, err, "This system does not have any valid subscriptions. Subscribe it before specifying rhsm: true in sources (error details: no matching key and certificate pair)") + }) } - solver.SetRootDir(rootDir) - _, err := solver.Depsolve(pkgsets, 0) - assert.EqualError(t, err, "This system does not have any valid subscriptions. Subscribe it before specifying rhsm: true in sources (error details: no matching key and certificate pair)") } diff --git a/pkg/depsolvednf/v2.go b/pkg/depsolvednf/v2.go new file mode 100644 index 0000000000..b336c4cfcd --- /dev/null +++ b/pkg/depsolvednf/v2.go @@ -0,0 +1,627 @@ +package depsolvednf + +import ( + "encoding/json" + "fmt" + "slices" + "sort" + "time" + + "github.com/osbuild/images/internal/common" + "github.com/osbuild/images/pkg/rpmmd" + "github.com/osbuild/images/pkg/sbom" +) + +// v2 API structs + +// v2Checksum represents a checksum with algorithm and value. +type v2Checksum struct { + Algorithm string `json:"algorithm"` + Value string `json:"value"` +} + +// v2Dependency represents an RPM dependency or provided capability. +type v2Dependency struct { + Name string `json:"name"` + Relation string `json:"relation,omitempty"` + Version string `json:"version,omitempty"` +} + +// v2Repository represents a DNF/YUM repository configuration. +// Used for both request and response (response is a superset of request fields). +// In request, at least one of 'baseurl' or 'metalink' or 'mirrorlist' must be +// present. +type v2Repository struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + BaseURLs []string `json:"baseurl,omitempty"` + Metalink string `json:"metalink,omitempty"` + MirrorList string `json:"mirrorlist,omitempty"` + GPGKeys []string `json:"gpgkey,omitempty"` + GPGCheck *bool `json:"gpgcheck,omitempty"` + RepoGPGCheck *bool `json:"repo_gpgcheck,omitempty"` + SSLVerify *bool `json:"sslverify,omitempty"` + SSLCACert string `json:"sslcacert,omitempty"` + SSLClientKey string `json:"sslclientkey,omitempty"` + SSLClientCert string `json:"sslclientcert,omitempty"` + MetadataExpire string `json:"metadata_expire,omitempty"` + ModuleHotfixes *bool `json:"module_hotfixes,omitempty"` + RHSM bool `json:"rhsm,omitempty"` +} + +// v2Package represents an RPM package with full metadata. +// This is a unified representation used for depsolve, dump, and search responses. +type v2Package struct { + // Core fields (always expected to have values) + Name string `json:"name"` + Epoch int `json:"epoch"` + Version string `json:"version"` + Release string `json:"release"` + Arch string `json:"arch"` + RepoID string `json:"repo_id"` + Location string `json:"location"` + RemoteLocations []string `json:"remote_locations"` + Checksum *v2Checksum `json:"checksum"` + + // Metadata fields (may be nil/empty) + HeaderChecksum *v2Checksum `json:"header_checksum"` + License string `json:"license"` + Summary string `json:"summary"` + Description string `json:"description"` + URL string `json:"url"` + Vendor string `json:"vendor"` + Packager string `json:"packager"` + BuildTime string `json:"build_time"` // RFC3339 format or empty + DownloadSize int64 `json:"download_size"` + InstallSize int64 `json:"install_size"` + Group string `json:"group"` + SourceRPM string `json:"source_rpm"` + Reason string `json:"reason"` + + // Dependencies (always arrays, may be empty) + Provides []v2Dependency `json:"provides"` + Requires []v2Dependency `json:"requires"` + RequiresPre []v2Dependency `json:"requires_pre"` + Conflicts []v2Dependency `json:"conflicts"` + Obsoletes []v2Dependency `json:"obsoletes"` + RegularRequires []v2Dependency `json:"regular_requires"` + Recommends []v2Dependency `json:"recommends"` + Suggests []v2Dependency `json:"suggests"` + Enhances []v2Dependency `json:"enhances"` + Supplements []v2Dependency `json:"supplements"` + Files []string `json:"files"` +} + +// v2ModuleConfigData contains module configuration data. +type v2ModuleConfigData struct { + Name string `json:"name"` + Stream string `json:"stream"` + Profiles []string `json:"profiles"` + State string `json:"state"` +} + +// v2ModuleConfigFile represents a module configuration file. +type v2ModuleConfigFile struct { + Path string `json:"path"` + Data v2ModuleConfigData `json:"data"` +} + +// v2ModuleFailsafeFile represents a module failsafe file. +type v2ModuleFailsafeFile struct { + Path string `json:"path"` + Data string `json:"data"` +} + +// v2ModuleSpec represents a module specification. +type v2ModuleSpec struct { + ModuleConfigFile v2ModuleConfigFile `json:"module-file"` + FailsafeFile v2ModuleFailsafeFile `json:"failsafe-file"` +} + +// v2Request is the top-level request structure for the V2 API. +type v2Request struct { + // API version, must be 2 + APIVersion int `json:"api_version"` + + // Command should be "depsolve", "dump", or "search" + Command string `json:"command"` + + // Platform ID, e.g., "platform:el9" + ModulePlatformID string `json:"module_platform_id,omitempty"` + + // Distro Releasever, e.g., "9" + Releasever string `json:"releasever"` + + // System architecture + Arch string `json:"arch"` + + // Cache directory for the DNF metadata + CacheDir string `json:"cachedir"` + + // Proxy to use + Proxy string `json:"proxy,omitempty"` + + // Arguments for the action defined by Command + Arguments v2Arguments `json:"arguments"` +} + +// v2Arguments contains command arguments for V2 API requests. +type v2Arguments struct { + // Repositories to use for depsolving + Repos []v2Repository `json:"repos"` + + // Search terms to use with search command + Search *v2SearchArgs `json:"search,omitempty"` + + // Depsolve package sets and repository mappings for this request + Transactions []v2TransactionArgs `json:"transactions,omitempty"` + + // Load repository configurations, gpg keys, and vars from an os-root-like tree. + RootDir string `json:"root_dir,omitempty"` + + // Optional metadata to download for the repositories + OptionalMetadata []string `json:"optional-metadata,omitempty"` + + // Optionally request an SBOM from depsolving + Sbom *v2SbomRequest `json:"sbom,omitempty"` +} + +// v2TransactionArgs contains arguments for a single depsolve transaction. +type v2TransactionArgs struct { + // Packages to depsolve + PackageSpecs []string `json:"package-specs"` + + // Packages to exclude from results + ExcludeSpecs []string `json:"exclude-specs,omitempty"` + + // Modules to enable during depsolve + ModuleEnableSpecs []string `json:"module-enable-specs,omitempty"` + + // IDs of repositories to use for this depsolve. + // If empty, all repositories will be used. + RepoIDs []string `json:"repo-ids,omitempty"` + + // If we want weak deps for this depsolve + InstallWeakDeps bool `json:"install_weak_deps"` +} + +// v2SearchArgs contains arguments for search command. +type v2SearchArgs struct { + // Only include latest NEVRA when true + Latest bool `json:"latest"` + + // List of package name globs to search for + Packages []string `json:"packages"` +} + +// v2SbomRequest contains SBOM generation request. +type v2SbomRequest struct { + Type string `json:"type"` +} + +// v2DepsolveResult represents the V2 depsolve command response. +type v2DepsolveResult struct { + Solver string `json:"solver"` + Transactions [][]v2Package `json:"transactions"` + Repos map[string]v2Repository `json:"repos"` + Modules map[string]v2ModuleSpec `json:"modules"` + SBOM json.RawMessage `json:"sbom,omitempty"` +} + +// v2PackageListResult is the common response structure for dump and search. +type v2PackageListResult struct { + Solver string `json:"solver"` + Packages []v2Package `json:"packages"` + Repos map[string]v2Repository `json:"repos"` +} + +// V2 API Handler Implementation + +// v2Handler implements the apiHandler interface for API version 2. +type v2Handler struct{} + +func newV2Handler() *v2Handler { + return &v2Handler{} +} + +var _ apiHandler = newV2Handler() + +func (h *v2Handler) makeDepsolveRequest(cfg *solverConfig, pkgSets []rpmmd.PackageSet, sbomType sbom.StandardType) ([]byte, error) { + allRepos := collectRepos(pkgSets) + + transactions := make([]v2TransactionArgs, len(pkgSets)) + for dsIdx, pkgSet := range pkgSets { + transactions[dsIdx] = v2TransactionArgs{ + PackageSpecs: pkgSet.Include, + ExcludeSpecs: pkgSet.Exclude, + ModuleEnableSpecs: pkgSet.EnabledModules, + InstallWeakDeps: pkgSet.InstallWeakDeps, + } + + for _, jobRepo := range pkgSet.Repositories { + transactions[dsIdx].RepoIDs = append(transactions[dsIdx].RepoIDs, jobRepo.Hash()) + } + } + + dnfRepos, err := h.reposFromRPMMD(cfg, allRepos) + if err != nil { + return nil, err + } + + args := v2Arguments{ + Repos: dnfRepos, + RootDir: cfg.rootDir, + Transactions: transactions, + OptionalMetadata: optionalMetadataForDistro(cfg.modulePlatformID), + } + + req := v2Request{ + APIVersion: 2, + Command: "depsolve", + ModulePlatformID: cfg.modulePlatformID, + Arch: cfg.arch, + Releasever: cfg.releaseVer, + CacheDir: cfg.cacheDir, + Proxy: cfg.proxy, + Arguments: args, + } + + if sbomType != sbom.StandardTypeNone { + req.Arguments.Sbom = &v2SbomRequest{Type: sbomType.String()} + } + + return json.Marshal(req) +} + +func (h *v2Handler) makeDumpRequest(cfg *solverConfig, repos []rpmmd.RepoConfig) ([]byte, error) { + dnfRepos, err := h.reposFromRPMMD(cfg, repos) + if err != nil { + return nil, err + } + req := v2Request{ + APIVersion: 2, + Command: "dump", + ModulePlatformID: cfg.modulePlatformID, + Arch: cfg.arch, + Releasever: cfg.releaseVer, + CacheDir: cfg.cacheDir, + Proxy: cfg.proxy, + Arguments: v2Arguments{ + Repos: dnfRepos, + }, + } + return json.Marshal(req) +} + +func (h *v2Handler) makeSearchRequest(cfg *solverConfig, repos []rpmmd.RepoConfig, packages []string) ([]byte, error) { + dnfRepos, err := h.reposFromRPMMD(cfg, repos) + if err != nil { + return nil, err + } + req := v2Request{ + APIVersion: 2, + Command: "search", + ModulePlatformID: cfg.modulePlatformID, + Arch: cfg.arch, + CacheDir: cfg.cacheDir, + Releasever: cfg.releaseVer, + Proxy: cfg.proxy, + Arguments: v2Arguments{ + Repos: dnfRepos, + Search: &v2SearchArgs{ + Packages: packages, + }, + }, + } + return json.Marshal(req) +} + +func (h *v2Handler) parseDepsolveResult(output []byte) (*depsolveResultRaw, error) { + var result v2DepsolveResult + if err := json.Unmarshal(output, &result); err != nil { + return nil, fmt.Errorf("decoding depsolve result failed: %w", err) + } + + // Build both per-transaction lists and a flattened list of all packages. + // V2 returns disjoint sets per transaction + transactions := make([]rpmmd.PackageList, len(result.Transactions)) + var allPkgs rpmmd.PackageList + + // Convert depsolved packages + for transIdx, transaction := range result.Transactions { + transPkgs := make(rpmmd.PackageList, 0, len(transaction)) + for _, pkg := range transaction { + repo, ok := result.Repos[pkg.RepoID] + if !ok { + return nil, fmt.Errorf("repo ID not found in repositories: %s", pkg.RepoID) + } + + rpmPkg, err := h.toRPMMDPackage(pkg, repo) + if err != nil { + return nil, err + } + transPkgs = append(transPkgs, rpmPkg) + allPkgs = append(allPkgs, rpmPkg) + } + transactions[transIdx] = transPkgs + } + + // Sort flattened packages by full NEVRA to ensure consistent ordering + sort.Slice(allPkgs, func(i, j int) bool { + return allPkgs[i].FullNEVRA() < allPkgs[j].FullNEVRA() + }) + + // Convert modules + modules := make([]rpmmd.ModuleSpec, 0, len(result.Modules)) + for _, mod := range result.Modules { + modules = append(modules, h.toRPMMDModuleSpec(mod)) + } + + // Convert repos + repos := make([]rpmmd.RepoConfig, 0, len(result.Repos)) + for repoID := range result.Repos { + repo := result.Repos[repoID] + repos = append(repos, h.toRPMMDRepoConfig(repo)) + } + // Sort repos by ID for deterministic ordering + sort.Slice(repos, func(i, j int) bool { + return repos[i].Id < repos[j].Id + }) + + return &depsolveResultRaw{ + Packages: allPkgs, + Transactions: transactions, + Modules: modules, + Repos: repos, + Solver: result.Solver, + SBOMRaw: result.SBOM, + }, nil +} + +func (h *v2Handler) parseDumpResult(output []byte) (*DumpResult, error) { + pkgs, repos, solver, err := h.parsePackageListResult(output, "dump") + if err != nil { + return nil, err + } + return &DumpResult{Packages: pkgs, Repos: repos, Solver: solver}, nil +} + +func (h *v2Handler) parseSearchResult(output []byte) (*SearchResult, error) { + pkgs, repos, solver, err := h.parsePackageListResult(output, "search") + if err != nil { + return nil, err + } + return &SearchResult{Packages: pkgs, Repos: repos, Solver: solver}, nil +} + +// V2 API Helper Functions + +// parsePackageListResult parses the common dump/search response structure. +func (h *v2Handler) parsePackageListResult(output []byte, operation string) (rpmmd.PackageList, []rpmmd.RepoConfig, string, error) { + var result v2PackageListResult + if err := json.Unmarshal(output, &result); err != nil { + return nil, nil, "", fmt.Errorf("decoding %s result failed: %w", operation, err) + } + + // Convert packages + pkgs := make(rpmmd.PackageList, 0, len(result.Packages)) + for _, pkg := range result.Packages { + repo, ok := result.Repos[pkg.RepoID] + if !ok { + return nil, nil, "", fmt.Errorf("repo ID not found in repositories: %s", pkg.RepoID) + } + rpmPkg, err := h.toRPMMDPackage(pkg, repo) + if err != nil { + return nil, nil, "", err + } + pkgs = append(pkgs, rpmPkg) + } + + // Convert repos + repos := make([]rpmmd.RepoConfig, 0, len(result.Repos)) + for repoID := range result.Repos { + repos = append(repos, h.toRPMMDRepoConfig(result.Repos[repoID])) + } + // Sort repos by ID for deterministic ordering + sort.Slice(repos, func(i, j int) bool { + return repos[i].Id < repos[j].Id + }) + + return pkgs, repos, result.Solver, nil +} + +func (h *v2Handler) reposFromRPMMD(cfg *solverConfig, rpmRepos []rpmmd.RepoConfig) ([]v2Repository, error) { + dnfRepos := make([]v2Repository, len(rpmRepos)) + for idx, rr := range rpmRepos { + dr := v2Repository{ + ID: rr.Hash(), + Name: rr.Name, + BaseURLs: slices.Clone(rr.BaseURLs), + Metalink: rr.Metalink, + MirrorList: rr.MirrorList, + GPGKeys: slices.Clone(rr.GPGKeys), + GPGCheck: common.ClonePtr(rr.CheckGPG), + RepoGPGCheck: common.ClonePtr(rr.CheckRepoGPG), + MetadataExpire: rr.MetadataExpire, + SSLCACert: rr.SSLCACert, + SSLClientKey: rr.SSLClientKey, + SSLClientCert: rr.SSLClientCert, + ModuleHotfixes: common.ClonePtr(rr.ModuleHotfixes), + } + + if rr.IgnoreSSL != nil { + dr.SSLVerify = common.ToPtr(!*rr.IgnoreSSL) + } + + if rr.RHSM { + // TODO: Enable V2 RHSM secrets discovery by setting dr.RHSM = true + // and removing the client-side secrets resolution below. + // This requires functional testing to ensure RHSM secrets discovery + // works correctly in the solver. + // See: https://github.com/osbuild/images/issues/2055 + + // NOTE: It is assumed that the s.subscriptions are not nil if the repo needs RHSM secrets + // because validateSubscriptionsForRepos() is called before makeDepsolveRequest(). + secrets, err := cfg.subscriptions.GetSecretsForBaseurl(rr.BaseURLs, cfg.arch, cfg.releaseVer) + if err != nil { + return nil, fmt.Errorf("getting RHSM secrets for baseurl %s failed: %w", rr.BaseURLs, err) + } + dr.SSLCACert = secrets.SSLCACert + dr.SSLClientKey = secrets.SSLClientKey + dr.SSLClientCert = secrets.SSLClientCert + } + + dnfRepos[idx] = dr + } + return dnfRepos, nil +} + +func (h *v2Handler) toRPMMDPackage(pkg v2Package, repo v2Repository) (rpmmd.Package, error) { + rpmPkg := rpmmd.Package{ + Name: pkg.Name, + Version: pkg.Version, + Release: pkg.Release, + Arch: pkg.Arch, + RepoID: pkg.RepoID, + Location: pkg.Location, + RemoteLocations: pkg.RemoteLocations, + License: pkg.License, + Summary: pkg.Summary, + Description: pkg.Description, + URL: pkg.URL, + Vendor: pkg.Vendor, + Packager: pkg.Packager, + Group: pkg.Group, + SourceRpm: pkg.SourceRPM, + Reason: pkg.Reason, + Files: pkg.Files, + } + + if pkg.Epoch < 0 { + return rpmmd.Package{}, fmt.Errorf("invalid negative epoch for package %s", pkg.Name) + } + rpmPkg.Epoch = uint(pkg.Epoch) + + if repo.GPGCheck != nil { + rpmPkg.CheckGPG = *repo.GPGCheck + } + + // Parse checksum + if pkg.Checksum != nil { + rpmPkg.Checksum = rpmmd.Checksum{ + Type: pkg.Checksum.Algorithm, + Value: pkg.Checksum.Value, + } + } + + // Parse header checksum + if pkg.HeaderChecksum != nil { + rpmPkg.HeaderChecksum = rpmmd.Checksum{ + Type: pkg.HeaderChecksum.Algorithm, + Value: pkg.HeaderChecksum.Value, + } + } + + // Parse build time (RFC3339 format) + if pkg.BuildTime != "" { + buildTime, err := time.Parse(time.RFC3339, pkg.BuildTime) + if err != nil { + return rpmmd.Package{}, fmt.Errorf("parsing build_time %q for package %s failed: %w", pkg.BuildTime, pkg.Name, err) + } + rpmPkg.BuildTime = buildTime + } + + // Parse sizes + if pkg.DownloadSize < 0 { + return rpmmd.Package{}, fmt.Errorf("invalid negative download size for package %s", pkg.Name) + } + rpmPkg.DownloadSize = uint64(pkg.DownloadSize) + if pkg.InstallSize < 0 { + return rpmmd.Package{}, fmt.Errorf("invalid negative install size for package %s", pkg.Name) + } + rpmPkg.InstallSize = uint64(pkg.InstallSize) + + // Convert dependencies + rpmPkg.Provides = h.toRPMMDRelDepList(pkg.Provides) + rpmPkg.Requires = h.toRPMMDRelDepList(pkg.Requires) + rpmPkg.RequiresPre = h.toRPMMDRelDepList(pkg.RequiresPre) + rpmPkg.Conflicts = h.toRPMMDRelDepList(pkg.Conflicts) + rpmPkg.Obsoletes = h.toRPMMDRelDepList(pkg.Obsoletes) + rpmPkg.RegularRequires = h.toRPMMDRelDepList(pkg.RegularRequires) + rpmPkg.Recommends = h.toRPMMDRelDepList(pkg.Recommends) + rpmPkg.Suggests = h.toRPMMDRelDepList(pkg.Suggests) + rpmPkg.Enhances = h.toRPMMDRelDepList(pkg.Enhances) + rpmPkg.Supplements = h.toRPMMDRelDepList(pkg.Supplements) + + // Set SSL verification from repo + if sslVerify := repo.SSLVerify; sslVerify != nil { + rpmPkg.IgnoreSSL = !*sslVerify + } + + // Set mTLS secrets if SSLClientKey is set. + // The Solver will override secrets to 'org.osbuild.rhsm' if the repo needs RHSM secrets. + if repo.SSLClientKey != "" { + rpmPkg.Secrets = "org.osbuild.mtls" + } + + return rpmPkg, nil +} + +func (h *v2Handler) toRPMMDRelDepList(deps []v2Dependency) rpmmd.RelDepList { + if len(deps) == 0 { + return nil + } + result := make(rpmmd.RelDepList, len(deps)) + for i, dep := range deps { + result[i] = rpmmd.RelDep{ + Name: dep.Name, + Relationship: dep.Relation, + Version: dep.Version, + } + } + return result +} + +func (h *v2Handler) toRPMMDModuleSpec(mod v2ModuleSpec) rpmmd.ModuleSpec { + return rpmmd.ModuleSpec{ + ModuleConfigFile: rpmmd.ModuleConfigFile{ + Path: mod.ModuleConfigFile.Path, + Data: rpmmd.ModuleConfigData{ + Name: mod.ModuleConfigFile.Data.Name, + Stream: mod.ModuleConfigFile.Data.Stream, + State: mod.ModuleConfigFile.Data.State, + Profiles: mod.ModuleConfigFile.Data.Profiles, + }, + }, + FailsafeFile: rpmmd.ModuleFailsafeFile{ + Path: mod.FailsafeFile.Path, + Data: mod.FailsafeFile.Data, + }, + } +} + +func (h *v2Handler) toRPMMDRepoConfig(repo v2Repository) rpmmd.RepoConfig { + var ignoreSSL bool + if sslVerify := repo.SSLVerify; sslVerify != nil { + ignoreSSL = !*sslVerify + } + + return rpmmd.RepoConfig{ + Id: repo.ID, + Name: repo.Name, + BaseURLs: slices.Clone(repo.BaseURLs), + Metalink: repo.Metalink, + MirrorList: repo.MirrorList, + GPGKeys: slices.Clone(repo.GPGKeys), + CheckGPG: common.ClonePtr(repo.GPGCheck), + CheckRepoGPG: common.ClonePtr(repo.RepoGPGCheck), + IgnoreSSL: &ignoreSSL, + MetadataExpire: repo.MetadataExpire, + ModuleHotfixes: common.ClonePtr(repo.ModuleHotfixes), + Enabled: common.ToPtr(true), + SSLCACert: repo.SSLCACert, + SSLClientKey: repo.SSLClientKey, + SSLClientCert: repo.SSLClientCert, + RHSM: repo.RHSM, + } +} diff --git a/pkg/depsolvednf/v2_test.go b/pkg/depsolvednf/v2_test.go new file mode 100644 index 0000000000..013cccbcc3 --- /dev/null +++ b/pkg/depsolvednf/v2_test.go @@ -0,0 +1,1568 @@ +package depsolvednf + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/osbuild/images/internal/common" + "github.com/osbuild/images/pkg/rpmmd" + "github.com/osbuild/images/pkg/sbom" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test fixtures for V2 API parsing tests. +// These define the expected rpmmd types after parsing the corresponding JSON. + +// testExpectedPackage is the expected rpmmd.Package after parsing testPackageJSON. +var testExpectedPackage = rpmmd.Package{ + Name: "bash", + Epoch: 1, + Version: "5.1.8", + Release: "9.el9", + Arch: "x86_64", + RepoID: "baseos", + Location: "Packages/bash-5.1.8-9.el9.x86_64.rpm", + RemoteLocations: []string{"https://example.com/baseos/Packages/bash-5.1.8-9.el9.x86_64.rpm"}, + Checksum: rpmmd.Checksum{Type: "sha256", Value: "abc123"}, + HeaderChecksum: rpmmd.Checksum{Type: "sha256", Value: "def456"}, + License: "GPLv3+", + Summary: "The GNU Bourne Again shell", + Description: "Bash is a shell", + URL: "https://www.gnu.org/software/bash", + Vendor: "Red Hat", + Packager: "Red Hat, Inc.", + BuildTime: time.Date(2023, 1, 15, 10, 30, 0, 0, time.UTC), + DownloadSize: 1234567, + InstallSize: 2345678, + Group: "System Environment/Shells", + SourceRpm: "bash-5.1.8-9.el9.src.rpm", + Reason: "user", + Provides: rpmmd.RelDepList{ + {Name: "bash", Relationship: "=", Version: "5.1.8-9.el9"}, + {Name: "/bin/bash"}, + }, + Requires: rpmmd.RelDepList{ + {Name: "libc.so.6()(64bit)"}, + }, + Files: []string{"/bin/bash", "/usr/bin/bash"}, + CheckGPG: true, // from repo.GPGCheck=true + IgnoreSSL: true, // from repo.SSLVerify=false +} + +// testExpectedRepo is the expected rpmmd.RepoConfig after parsing testRepoJSON. +var testExpectedRepo = rpmmd.RepoConfig{ + Id: "baseos", + Name: "BaseOS", + BaseURLs: []string{"https://example.com/baseos"}, + GPGKeys: []string{"file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"}, + CheckGPG: common.ToPtr(true), + CheckRepoGPG: common.ToPtr(true), + IgnoreSSL: common.ToPtr(true), // SSLVerify=false -> IgnoreSSL=true + SSLCACert: "/etc/pki/tls/certs/ca-bundle.crt", + MetadataExpire: "86400", + ModuleHotfixes: common.ToPtr(true), + Enabled: common.ToPtr(true), +} + +// testParseDetailsInput is a complete V2 API response JSON containing +// one package and one repo, matching testExpectedPackage and testExpectedRepo. +const testParseDetailsInput = `{ + "solver": "dnf5", + "transactions": [ + [ + { + "name": "bash", + "epoch": 1, + "version": "5.1.8", + "release": "9.el9", + "arch": "x86_64", + "repo_id": "baseos", + "location": "Packages/bash-5.1.8-9.el9.x86_64.rpm", + "remote_locations": ["https://example.com/baseos/Packages/bash-5.1.8-9.el9.x86_64.rpm"], + "checksum": {"algorithm": "sha256", "value": "abc123"}, + "header_checksum": {"algorithm": "sha256", "value": "def456"}, + "license": "GPLv3+", + "summary": "The GNU Bourne Again shell", + "description": "Bash is a shell", + "url": "https://www.gnu.org/software/bash", + "vendor": "Red Hat", + "packager": "Red Hat, Inc.", + "build_time": "2023-01-15T10:30:00Z", + "download_size": 1234567, + "install_size": 2345678, + "group": "System Environment/Shells", + "source_rpm": "bash-5.1.8-9.el9.src.rpm", + "reason": "user", + "provides": [ + {"name": "bash", "relation": "=", "version": "5.1.8-9.el9"}, + {"name": "/bin/bash"} + ], + "requires": [{"name": "libc.so.6()(64bit)"}], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": ["/bin/bash", "/usr/bin/bash"] + } + ] + ], + "repos": { + "baseos": { + "id": "baseos", + "name": "BaseOS", + "baseurl": ["https://example.com/baseos"], + "metalink": "", + "mirrorlist": "", + "gpgcheck": true, + "repo_gpgcheck": true, + "gpgkey": ["file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"], + "sslverify": false, + "sslcacert": "/etc/pki/tls/certs/ca-bundle.crt", + "sslclientkey": "", + "sslclientcert": "", + "metadata_expire": "86400", + "module_hotfixes": true, + "rhsm": false + } + }, + "modules": {} +}` + +// testParseDetailsDumpSearchInput is a V2 API dump/search response JSON +// containing one package and one repo, matching testExpectedPackage and testExpectedRepo. +const testParseDetailsDumpSearchInput = `{ + "solver": "dnf5", + "packages": [ + { + "name": "bash", + "epoch": 1, + "version": "5.1.8", + "release": "9.el9", + "arch": "x86_64", + "repo_id": "baseos", + "location": "Packages/bash-5.1.8-9.el9.x86_64.rpm", + "remote_locations": ["https://example.com/baseos/Packages/bash-5.1.8-9.el9.x86_64.rpm"], + "checksum": {"algorithm": "sha256", "value": "abc123"}, + "header_checksum": {"algorithm": "sha256", "value": "def456"}, + "license": "GPLv3+", + "summary": "The GNU Bourne Again shell", + "description": "Bash is a shell", + "url": "https://www.gnu.org/software/bash", + "vendor": "Red Hat", + "packager": "Red Hat, Inc.", + "build_time": "2023-01-15T10:30:00Z", + "download_size": 1234567, + "install_size": 2345678, + "group": "System Environment/Shells", + "source_rpm": "bash-5.1.8-9.el9.src.rpm", + "reason": "user", + "provides": [ + {"name": "bash", "relation": "=", "version": "5.1.8-9.el9"}, + {"name": "/bin/bash"} + ], + "requires": [{"name": "libc.so.6()(64bit)"}], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": ["/bin/bash", "/usr/bin/bash"] + } + ], + "repos": { + "baseos": { + "id": "baseos", + "name": "BaseOS", + "baseurl": ["https://example.com/baseos"], + "metalink": "", + "mirrorlist": "", + "gpgcheck": true, + "repo_gpgcheck": true, + "gpgkey": ["file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"], + "sslverify": false, + "sslcacert": "/etc/pki/tls/certs/ca-bundle.crt", + "sslclientkey": "", + "sslclientcert": "", + "metadata_expire": "86400", + "module_hotfixes": true, + "rhsm": false + } + } +}` + +func TestV2HandlerMakeDepsolveRequest(t *testing.T) { + baseOS := rpmmd.RepoConfig{ + Name: "baseos", + BaseURLs: []string{"https://example.org/baseos"}, + } + appstream := rpmmd.RepoConfig{ + Name: "appstream", + BaseURLs: []string{"https://example.org/appstream"}, + } + userRepo := rpmmd.RepoConfig{ + Name: "user-repo", + BaseURLs: []string{"https://example.org/user-repo"}, + } + userRepo2 := rpmmd.RepoConfig{ + Name: "user-repo-2", + BaseURLs: []string{"https://example.org/user-repo-2"}, + } + moduleHotfixRepo := rpmmd.RepoConfig{ + Name: "module-hotfixes", + BaseURLs: []string{"https://example.org/nginx"}, + ModuleHotfixes: common.ToPtr(true), + } + mtlsRepo := rpmmd.RepoConfig{ + Name: "mtls", + BaseURLs: []string{"https://example.org/mtls"}, + SSLCACert: "/cacert", + SSLClientCert: "/cert", + SSLClientKey: "/key", + } + + testCases := []struct { + name string + packageSets []rpmmd.PackageSet + withSbom bool + wantJSON string + }{ + { + name: "single transaction", + packageSets: []rpmmd.PackageSet{ + { + Include: []string{"pkg1"}, + Exclude: []string{"pkg2"}, + Repositories: []rpmmd.RepoConfig{ + baseOS, + appstream, + }, + InstallWeakDeps: true, + }, + }, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "depsolve", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %[1]q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %[2]q, "name": "appstream", "baseurl": ["https://example.org/appstream"]} + ], + "transactions": [ + {"package-specs": ["pkg1"], "exclude-specs": ["pkg2"], "repo-ids": [%[1]q, %[2]q], "install_weak_deps": true} + ], + "root_dir": "/root", + "optional-metadata": ["filelists"] + } + }`, baseOS.Hash(), appstream.Hash()), + }, + { + name: "2 transactions + package set specific repo", + packageSets: []rpmmd.PackageSet{ + { + Include: []string{"pkg1"}, + Exclude: []string{"pkg2"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream}, + InstallWeakDeps: true, + }, + { + Include: []string{"pkg3"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream, userRepo}, + }, + }, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "depsolve", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %[1]q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %[2]q, "name": "appstream", "baseurl": ["https://example.org/appstream"]}, + {"id": %[3]q, "name": "user-repo", "baseurl": ["https://example.org/user-repo"]} + ], + "transactions": [ + {"package-specs": ["pkg1"], "exclude-specs": ["pkg2"], "repo-ids": [%[1]q, %[2]q], "install_weak_deps": true}, + {"package-specs": ["pkg3"], "repo-ids": [%[1]q, %[2]q, %[3]q], "install_weak_deps": false} + ], + "root_dir": "/root", + "optional-metadata": ["filelists"] + } + }`, baseOS.Hash(), appstream.Hash(), userRepo.Hash()), + }, + { + name: "2 transactions + no package set specific repos", + packageSets: []rpmmd.PackageSet{ + { + Include: []string{"pkg1"}, + Exclude: []string{"pkg2"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream}, + InstallWeakDeps: true, + }, + { + Include: []string{"pkg3"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream}, + }, + }, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "depsolve", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %[1]q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %[2]q, "name": "appstream", "baseurl": ["https://example.org/appstream"]} + ], + "transactions": [ + {"package-specs": ["pkg1"], "exclude-specs": ["pkg2"], "repo-ids": [%[1]q, %[2]q], "install_weak_deps": true}, + {"package-specs": ["pkg3"], "repo-ids": [%[1]q, %[2]q], "install_weak_deps": false} + ], + "root_dir": "/root", + "optional-metadata": ["filelists"] + } + }`, baseOS.Hash(), appstream.Hash()), + }, + { + name: "3 transactions + package set specific repo used by 2nd and 3rd transaction", + packageSets: []rpmmd.PackageSet{ + { + Include: []string{"pkg1"}, + Exclude: []string{"pkg2"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream}, + InstallWeakDeps: true, + }, + { + Include: []string{"pkg3"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream, userRepo}, + }, + { + Include: []string{"pkg4"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream, userRepo}, + }, + }, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "depsolve", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %[1]q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %[2]q, "name": "appstream", "baseurl": ["https://example.org/appstream"]}, + {"id": %[3]q, "name": "user-repo", "baseurl": ["https://example.org/user-repo"]} + ], + "transactions": [ + {"package-specs": ["pkg1"], "exclude-specs": ["pkg2"], "repo-ids": [%[1]q, %[2]q], "install_weak_deps": true}, + {"package-specs": ["pkg3"], "repo-ids": [%[1]q, %[2]q, %[3]q], "install_weak_deps": false}, + {"package-specs": ["pkg4"], "repo-ids": [%[1]q, %[2]q, %[3]q], "install_weak_deps": false} + ], + "root_dir": "/root", + "optional-metadata": ["filelists"] + } + }`, baseOS.Hash(), appstream.Hash(), userRepo.Hash()), + }, + { + name: "3 transactions + 3rd transaction using another repo", + packageSets: []rpmmd.PackageSet{ + { + Include: []string{"pkg1"}, + Exclude: []string{"pkg2"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream}, + InstallWeakDeps: true, + }, + { + Include: []string{"pkg3"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream, userRepo}, + }, + { + Include: []string{"pkg4"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream, userRepo, userRepo2}, + }, + }, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "depsolve", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %[1]q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %[2]q, "name": "appstream", "baseurl": ["https://example.org/appstream"]}, + {"id": %[3]q, "name": "user-repo", "baseurl": ["https://example.org/user-repo"]}, + {"id": %[4]q, "name": "user-repo-2", "baseurl": ["https://example.org/user-repo-2"]} + ], + "transactions": [ + {"package-specs": ["pkg1"], "exclude-specs": ["pkg2"], "repo-ids": [%[1]q, %[2]q], "install_weak_deps": true}, + {"package-specs": ["pkg3"], "repo-ids": [%[1]q, %[2]q, %[3]q], "install_weak_deps": false}, + {"package-specs": ["pkg4"], "repo-ids": [%[1]q, %[2]q, %[3]q, %[4]q], "install_weak_deps": false} + ], + "root_dir": "/root", + "optional-metadata": ["filelists"] + } + }`, baseOS.Hash(), appstream.Hash(), userRepo.Hash(), userRepo2.Hash()), + }, + { + name: "module hotfixes flag passed", + packageSets: []rpmmd.PackageSet{ + { + Include: []string{"pkg1"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream, moduleHotfixRepo}, + }, + }, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "depsolve", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %[1]q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %[2]q, "name": "appstream", "baseurl": ["https://example.org/appstream"]}, + {"id": %[3]q, "name": "module-hotfixes", "baseurl": ["https://example.org/nginx"], "module_hotfixes": true} + ], + "transactions": [ + {"package-specs": ["pkg1"], "repo-ids": [%[1]q, %[2]q, %[3]q], "install_weak_deps": false} + ], + "root_dir": "/root", + "optional-metadata": ["filelists"] + } + }`, baseOS.Hash(), appstream.Hash(), moduleHotfixRepo.Hash()), + }, + { + name: "mtls certs passed", + packageSets: []rpmmd.PackageSet{ + { + Include: []string{"pkg1"}, + Repositories: []rpmmd.RepoConfig{baseOS, appstream, mtlsRepo}, + }, + }, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "depsolve", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %[1]q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %[2]q, "name": "appstream", "baseurl": ["https://example.org/appstream"]}, + {"id": %[3]q, "name": "mtls", "baseurl": ["https://example.org/mtls"], "sslcacert": "/cacert", "sslclientkey": "/key", "sslclientcert": "/cert"} + ], + "transactions": [ + {"package-specs": ["pkg1"], "repo-ids": [%[1]q, %[2]q, %[3]q], "install_weak_deps": false} + ], + "root_dir": "/root", + "optional-metadata": ["filelists"] + } + }`, baseOS.Hash(), appstream.Hash(), mtlsRepo.Hash()), + }, + { + name: "2 transactions + withSbom 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}, + }, + }, + withSbom: true, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "depsolve", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %[1]q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %[2]q, "name": "appstream", "baseurl": ["https://example.org/appstream"]} + ], + "transactions": [ + {"package-specs": ["pkg1"], "exclude-specs": ["pkg2"], "repo-ids": [%[1]q, %[2]q], "install_weak_deps": true}, + {"package-specs": ["pkg3"], "repo-ids": [%[1]q, %[2]q], "install_weak_deps": false} + ], + "root_dir": "/root", + "optional-metadata": ["filelists"], + "sbom": {"type": "spdx"} + } + }`, baseOS.Hash(), appstream.Hash()), + }, + } + + cfg := &solverConfig{ + modulePlatformID: "platform:el8", + arch: "x86_64", + releaseVer: "8", + cacheDir: "/cache", + rootDir: "/root", + } + v2Handler := newV2Handler() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + var sbomType sbom.StandardType + if tt.withSbom { + sbomType = sbom.StandardTypeSpdx + } + + rawReq, err := v2Handler.makeDepsolveRequest(cfg, tt.packageSets, sbomType) + require.NoError(t, err) + require.NotEmpty(t, rawReq) + assert.JSONEq(t, tt.wantJSON, string(rawReq)) + }) + } +} + +func TestV2HandlerMakeDumpRequest(t *testing.T) { + baseOS := rpmmd.RepoConfig{ + Name: "baseos", + BaseURLs: []string{"https://example.org/baseos"}, + } + appstream := rpmmd.RepoConfig{ + Name: "appstream", + BaseURLs: []string{"https://example.org/appstream"}, + } + mtlsRepo := rpmmd.RepoConfig{ + Name: "mtls", + BaseURLs: []string{"https://example.org/mtls"}, + SSLCACert: "/cacert", + SSLClientCert: "/cert", + SSLClientKey: "/key", + } + + testCases := []struct { + name string + repos []rpmmd.RepoConfig + wantJSON string + }{ + { + name: "single repo", + repos: []rpmmd.RepoConfig{baseOS}, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "dump", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %q, "name": "baseos", "baseurl": ["https://example.org/baseos"]} + ] + } + }`, baseOS.Hash()), + }, + { + name: "multiple repos", + repos: []rpmmd.RepoConfig{baseOS, appstream}, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "dump", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %q, "name": "appstream", "baseurl": ["https://example.org/appstream"]} + ] + } + }`, baseOS.Hash(), appstream.Hash()), + }, + { + name: "mtls certs passed", + repos: []rpmmd.RepoConfig{baseOS, mtlsRepo}, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "dump", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %q, "name": "mtls", "baseurl": ["https://example.org/mtls"], "sslcacert": "/cacert", "sslclientkey": "/key", "sslclientcert": "/cert"} + ] + } + }`, baseOS.Hash(), mtlsRepo.Hash()), + }, + } + + cfg := &solverConfig{ + modulePlatformID: "platform:el8", + arch: "x86_64", + releaseVer: "8", + cacheDir: "/cache", + } + v2Handler := newV2Handler() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + rawReq, err := v2Handler.makeDumpRequest(cfg, tt.repos) + require.NoError(t, err) + require.NotEmpty(t, rawReq) + assert.JSONEq(t, tt.wantJSON, string(rawReq)) + }) + } +} + +func TestV2HandlerMakeSearchRequest(t *testing.T) { + baseOS := rpmmd.RepoConfig{ + Name: "baseos", + BaseURLs: []string{"https://example.org/baseos"}, + } + appstream := rpmmd.RepoConfig{ + Name: "appstream", + BaseURLs: []string{"https://example.org/appstream"}, + } + + testCases := []struct { + name string + repos []rpmmd.RepoConfig + packages []string + wantJSON string + }{ + { + name: "single package search", + repos: []rpmmd.RepoConfig{baseOS, appstream}, + packages: []string{"vim"}, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "search", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %q, "name": "baseos", "baseurl": ["https://example.org/baseos"]}, + {"id": %q, "name": "appstream", "baseurl": ["https://example.org/appstream"]} + ], + "search": {"latest": false, "packages": ["vim"]} + } + }`, baseOS.Hash(), appstream.Hash()), + }, + { + name: "glob pattern search", + repos: []rpmmd.RepoConfig{baseOS}, + packages: []string{"python3*", "kernel-*"}, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "search", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %q, "name": "baseos", "baseurl": ["https://example.org/baseos"]} + ], + "search": {"latest": false, "packages": ["python3*", "kernel-*"]} + } + }`, baseOS.Hash()), + }, + { + name: "empty packages list", + repos: []rpmmd.RepoConfig{baseOS}, + packages: []string{}, + wantJSON: fmt.Sprintf(`{ + "api_version": 2, + "command": "search", + "module_platform_id": "platform:el8", + "releasever": "8", + "arch": "x86_64", + "cachedir": "/cache", + "arguments": { + "repos": [ + {"id": %q, "name": "baseos", "baseurl": ["https://example.org/baseos"]} + ], + "search": {"latest": false, "packages": []} + } + }`, baseOS.Hash()), + }, + } + + cfg := &solverConfig{ + modulePlatformID: "platform:el8", + arch: "x86_64", + releaseVer: "8", + cacheDir: "/cache", + } + v2Handler := newV2Handler() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + rawReq, err := v2Handler.makeSearchRequest(cfg, tt.repos, tt.packages) + require.NoError(t, err) + require.NotEmpty(t, rawReq) + assert.JSONEq(t, tt.wantJSON, string(rawReq)) + }) + } +} + +func TestV2HandlerParseDepsolveResult(t *testing.T) { + testCases := []struct { + name string + input string + wantTransactions []int // len = number of transactions, values = packages per transaction + wantRepos int + wantSolver string + wantModules int + wantErr bool + }{ + { + name: "single transaction with packages", + input: `{ + "solver": "dnf5", + "transactions": [ + [ + { + "name": "bash", + "epoch": 0, + "version": "5.1.8", + "release": "9.el9", + "arch": "x86_64", + "repo_id": "baseos", + "location": "Packages/bash-5.1.8-9.el9.x86_64.rpm", + "remote_locations": ["https://example.com/baseos/Packages/bash-5.1.8-9.el9.x86_64.rpm"], + "checksum": {"algorithm": "sha256", "value": "aaaa"}, + "header_checksum": null, + "license": "GPLv3+", + "summary": "The GNU Bourne Again shell", + "description": "The GNU Bourne Again shell", + "url": "https://www.gnu.org/software/bash", + "vendor": "Red Hat", + "packager": "Red Hat", + "build_time": "2023-01-15T10:30:00Z", + "download_size": 1234567, + "install_size": 2345678, + "group": "System Environment/Shells", + "source_rpm": "bash-5.1.8-9.el9.src.rpm", + "reason": "user", + "provides": [{"name": "bash", "relation": "=", "version": "5.1.8-9.el9"}], + "requires": [{"name": "libc.so.6()(64bit)"}], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": ["/bin/bash", "/usr/bin/bash"] + } + ] + ], + "repos": { + "baseos": { + "id": "baseos", + "name": "BaseOS", + "baseurl": ["https://example.com/baseos"], + "metalink": null, + "mirrorlist": null, + "gpgcheck": true, + "repo_gpgcheck": false, + "gpgkey": ["file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"], + "sslverify": true, + "sslcacert": null, + "sslclientkey": null, + "sslclientcert": null, + "metadata_expire": "", + "module_hotfixes": null, + "rhsm": false + } + }, + "modules": {} + }`, + wantTransactions: []int{1}, + wantRepos: 1, + wantSolver: "dnf5", + wantModules: 0, + }, + { + name: "multiple transactions", + input: `{ + "solver": "dnf5", + "transactions": [ + [ + { + "name": "pkg1", + "epoch": 0, + "version": "1.0", + "release": "1.el9", + "arch": "x86_64", + "repo_id": "baseos", + "location": "Packages/pkg1.rpm", + "remote_locations": ["https://example.com/pkg1.rpm"], + "checksum": {"algorithm": "sha256", "value": "aaaa"}, + "header_checksum": null, + "license": "MIT", + "summary": "Package 1", + "description": "Package 1 description", + "url": "", + "vendor": "", + "packager": "", + "build_time": null, + "download_size": null, + "install_size": null, + "group": "", + "source_rpm": "", + "reason": "", + "provides": [], + "requires": [], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": [] + } + ], + [ + { + "name": "pkg2", + "epoch": 0, + "version": "2.0", + "release": "1.el9", + "arch": "x86_64", + "repo_id": "baseos", + "location": "Packages/pkg2.rpm", + "remote_locations": ["https://example.com/pkg2.rpm"], + "checksum": {"algorithm": "sha256", "value": "bbbb"}, + "header_checksum": null, + "license": "MIT", + "summary": "Package 2", + "description": "Package 2 description", + "url": "", + "vendor": "", + "packager": "", + "build_time": null, + "download_size": null, + "install_size": null, + "group": "", + "source_rpm": "", + "reason": "", + "provides": [], + "requires": [], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": [] + } + ] + ], + "repos": { + "baseos": { + "id": "baseos", + "name": "BaseOS", + "baseurl": ["https://example.com/baseos"], + "metalink": null, + "mirrorlist": null, + "gpgcheck": false, + "repo_gpgcheck": false, + "gpgkey": null, + "sslverify": null, + "sslcacert": null, + "sslclientkey": null, + "sslclientcert": null, + "metadata_expire": "", + "module_hotfixes": null, + "rhsm": false + } + }, + "modules": {} + }`, + wantTransactions: []int{1, 1}, + wantRepos: 1, + wantSolver: "dnf5", + wantModules: 0, + }, + { + name: "multiple repos", + input: `{ + "solver": "dnf5", + "transactions": [ + [ + { + "name": "pkg1", + "epoch": 0, + "version": "1.0", + "release": "1.el9", + "arch": "x86_64", + "repo_id": "baseos", + "location": "Packages/pkg1.rpm", + "remote_locations": ["https://example.com/pkg1.rpm"], + "checksum": {"algorithm": "sha256", "value": "aaaa"}, + "header_checksum": null, + "license": "MIT", + "summary": "Package 1", + "description": "Package 1 description", + "url": "", + "vendor": "", + "packager": "", + "build_time": null, + "download_size": null, + "install_size": null, + "group": "", + "source_rpm": "", + "reason": "", + "provides": [], + "requires": [], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": [] + }, + { + "name": "pkg2", + "epoch": 0, + "version": "2.0", + "release": "1.el9", + "arch": "x86_64", + "repo_id": "appstream", + "location": "Packages/pkg2.rpm", + "remote_locations": ["https://example.com/pkg2.rpm"], + "checksum": {"algorithm": "sha256", "value": "bbbb"}, + "header_checksum": null, + "license": "MIT", + "summary": "Package 2", + "description": "Package 2 description", + "url": "", + "vendor": "", + "packager": "", + "build_time": null, + "download_size": null, + "install_size": null, + "group": "", + "source_rpm": "", + "reason": "", + "provides": [], + "requires": [], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": [] + } + ] + ], + "repos": { + "baseos": { + "id": "baseos", + "name": "BaseOS", + "baseurl": ["https://example.com/baseos"], + "metalink": null, + "mirrorlist": null, + "gpgcheck": true, + "repo_gpgcheck": false, + "gpgkey": null, + "sslverify": null, + "sslcacert": null, + "sslclientkey": null, + "sslclientcert": null, + "metadata_expire": "", + "module_hotfixes": null, + "rhsm": false + }, + "appstream": { + "id": "appstream", + "name": "AppStream", + "baseurl": ["https://example.com/appstream"], + "metalink": null, + "mirrorlist": null, + "gpgcheck": false, + "repo_gpgcheck": false, + "gpgkey": null, + "sslverify": null, + "sslcacert": null, + "sslclientkey": null, + "sslclientcert": null, + "metadata_expire": "", + "module_hotfixes": null, + "rhsm": false + } + }, + "modules": {} + }`, + wantTransactions: []int{2}, + wantRepos: 2, + wantSolver: "dnf5", + wantModules: 0, + }, + { + name: "invalid JSON", + input: `{invalid json`, + wantErr: true, + }, + { + name: "repo not found error", + input: `{ + "solver": "dnf5", + "transactions": [ + [ + { + "name": "pkg1", + "epoch": 0, + "version": "1.0", + "release": "1.el9", + "arch": "x86_64", + "repo_id": "unknown-repo", + "location": "Packages/pkg1.rpm", + "remote_locations": ["https://example.com/pkg1.rpm"], + "checksum": {"algorithm": "sha256", "value": "aaaa"}, + "header_checksum": null, + "license": "MIT", + "summary": "Package 1", + "description": "Package 1 description", + "url": "", + "vendor": "", + "packager": "", + "build_time": null, + "download_size": null, + "install_size": null, + "group": "", + "source_rpm": "", + "reason": "", + "provides": [], + "requires": [], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": [] + } + ] + ], + "repos": {}, + "modules": {} + }`, + wantErr: true, + }, + } + + v2Handler := newV2Handler() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + result, err := v2Handler.parseDepsolveResult([]byte(tt.input)) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, result) + + // Check transactions + require.Len(t, result.Transactions, len(tt.wantTransactions), "transaction count mismatch") + for i, wantPkgCount := range tt.wantTransactions { + assert.Len(t, result.Transactions[i], wantPkgCount, "transaction %d package count", i) + } + + // Check flattened packages (sum of all transaction packages) + wantTotalPkgs := 0 + for _, count := range tt.wantTransactions { + wantTotalPkgs += count + } + assert.Len(t, result.Packages, wantTotalPkgs, "total packages count") + + // Check other fields + assert.Len(t, result.Repos, tt.wantRepos) + assert.Equal(t, tt.wantSolver, result.Solver) + assert.Len(t, result.Modules, tt.wantModules) + }) + } +} + +// TestV2HandlerParseDumpSearchResult tests both parseDumpResult and parseSearchResult +// since they share the same underlying parsing logic via parsePackageListResult. +func TestV2HandlerParseDumpSearchResult(t *testing.T) { + testCases := []struct { + name string + input string + wantPkgLen int + wantRepoLen int + wantSolver string + wantErrMsg string + }{ + { + name: "successful result with multiple packages", + input: `{ + "solver": "dnf5", + "packages": [ + { + "name": "vim", + "epoch": 0, + "version": "8.2", + "release": "1.el9", + "arch": "x86_64", + "repo_id": "appstream", + "location": "Packages/vim-8.2-1.el9.x86_64.rpm", + "remote_locations": ["https://example.com/vim.rpm"], + "checksum": {"algorithm": "sha256", "value": "cccc"}, + "header_checksum": null, + "license": "Vim", + "summary": "The VIM editor", + "description": "VIM (VIsual editor iMproved)", + "url": "https://www.vim.org/", + "vendor": "", + "packager": "", + "build_time": "2023-06-01T12:00:00Z", + "download_size": 1500000, + "install_size": 3000000, + "group": "", + "source_rpm": "vim-8.2-1.el9.src.rpm", + "reason": "", + "provides": [], + "requires": [], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": [] + }, + { + "name": "emacs", + "epoch": 0, + "version": "27.2", + "release": "1.el9", + "arch": "x86_64", + "repo_id": "appstream", + "location": "Packages/emacs-27.2-1.el9.x86_64.rpm", + "remote_locations": ["https://example.com/emacs.rpm"], + "checksum": {"algorithm": "sha256", "value": "dddd"}, + "header_checksum": null, + "license": "GPLv3+", + "summary": "GNU Emacs", + "description": "GNU Emacs editor", + "url": "https://www.gnu.org/software/emacs/", + "vendor": "", + "packager": "", + "build_time": "2023-06-01T12:00:00Z", + "download_size": 2500000, + "install_size": 5000000, + "group": "", + "source_rpm": "emacs-27.2-1.el9.src.rpm", + "reason": "", + "provides": [], + "requires": [], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": [] + } + ], + "repos": { + "appstream": { + "id": "appstream", + "name": "AppStream", + "baseurl": ["https://example.com/appstream"], + "metalink": null, + "mirrorlist": null, + "gpgcheck": false, + "repo_gpgcheck": false, + "gpgkey": null, + "sslverify": null, + "sslcacert": null, + "sslclientkey": null, + "sslclientcert": null, + "metadata_expire": "", + "module_hotfixes": null, + "rhsm": false + } + } + }`, + wantPkgLen: 2, + wantRepoLen: 1, + wantSolver: "dnf5", + }, + { + name: "empty packages", + input: `{ + "solver": "dnf5", + "packages": [], + "repos": {} + }`, + wantPkgLen: 0, + wantRepoLen: 0, + wantSolver: "dnf5", + }, + { + name: "invalid JSON", + input: `{invalid json`, + wantErrMsg: "decoding", + }, + { + name: "repo not found", + input: `{ + "solver": "dnf5", + "packages": [ + { + "name": "foo", + "epoch": 0, + "version": "1.0", + "release": "1.el9", + "arch": "x86_64", + "repo_id": "missing_repo", + "location": "Packages/foo-1.0-1.el9.x86_64.rpm", + "remote_locations": [], + "checksum": null, + "header_checksum": null, + "license": "", + "summary": "", + "description": "", + "url": "", + "vendor": "", + "packager": "", + "build_time": "", + "download_size": 0, + "install_size": 0, + "group": "", + "source_rpm": "", + "reason": "", + "provides": [], + "requires": [], + "requires_pre": [], + "conflicts": [], + "obsoletes": [], + "regular_requires": [], + "recommends": [], + "suggests": [], + "enhances": [], + "supplements": [], + "files": [] + } + ], + "repos": {} + }`, + wantErrMsg: "repo ID not found", + }, + } + + v2Handler := newV2Handler() + + // Test both parsers with the same test cases + parsers := []struct { + name string + parse func([]byte) (pkgLen, repoLen int, solver string, err error) + }{ + { + name: "parseDumpResult", + parse: func(b []byte) (int, int, string, error) { + r, err := v2Handler.parseDumpResult(b) + if err != nil { + return 0, 0, "", err + } + return len(r.Packages), len(r.Repos), r.Solver, nil + }, + }, + { + name: "parseSearchResult", + parse: func(b []byte) (int, int, string, error) { + r, err := v2Handler.parseSearchResult(b) + if err != nil { + return 0, 0, "", err + } + return len(r.Packages), len(r.Repos), r.Solver, nil + }, + }, + } + + for _, parser := range parsers { + t.Run(parser.name, func(t *testing.T) { + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + pkgLen, repoLen, solver, err := parser.parse([]byte(tt.input)) + if tt.wantErrMsg != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErrMsg) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantPkgLen, pkgLen) + assert.Equal(t, tt.wantRepoLen, repoLen) + assert.Equal(t, tt.wantSolver, solver) + }) + } + }) + } +} + +func TestV2HandlerToRPMMDPackageWithMTLS(t *testing.T) { + v2Handler := newV2Handler() + + repo := v2Repository{ + ID: "mtls-repo", + Name: "MTLS Repo", + SSLClientKey: "/path/to/key", + SSLClientCert: "/path/to/cert", + } + + pkg := v2Package{ + Name: "test-pkg", + Epoch: 0, + Version: "1.0", + Release: "1.el9", + Arch: "x86_64", + RepoID: "mtls-repo", + Location: "Packages/test-pkg.rpm", + RemoteLocations: []string{"https://example.com/test-pkg.rpm"}, + Checksum: &v2Checksum{Algorithm: "sha256", Value: "aaaa"}, + Provides: []v2Dependency{}, + Requires: []v2Dependency{}, + RequiresPre: []v2Dependency{}, + Conflicts: []v2Dependency{}, + Obsoletes: []v2Dependency{}, + RegularRequires: []v2Dependency{}, + Recommends: []v2Dependency{}, + Suggests: []v2Dependency{}, + Enhances: []v2Dependency{}, + Supplements: []v2Dependency{}, + Files: []string{}, + } + + rpmPkg, err := v2Handler.toRPMMDPackage(pkg, repo) + require.NoError(t, err) + + assert.Equal(t, "org.osbuild.mtls", rpmPkg.Secrets) +} + +func TestV2HandlerToRPMMDRelDepList(t *testing.T) { + v2Handler := newV2Handler() + + testCases := []struct { + name string + deps []v2Dependency + expected rpmmd.RelDepList + }{ + { + name: "empty list", + deps: []v2Dependency{}, + expected: nil, + }, + { + name: "nil list", + deps: nil, + expected: nil, + }, + { + name: "dependencies with relations", + deps: []v2Dependency{ + {Name: "bash", Relation: ">=", Version: "5.0"}, + {Name: "glibc", Relation: "=", Version: "2.34"}, + }, + expected: rpmmd.RelDepList{ + {Name: "bash", Relationship: ">=", Version: "5.0"}, + {Name: "glibc", Relationship: "=", Version: "2.34"}, + }, + }, + { + name: "dependencies without relations", + deps: []v2Dependency{ + {Name: "libc.so.6()(64bit)"}, + {Name: "/bin/sh"}, + }, + expected: rpmmd.RelDepList{ + {Name: "libc.so.6()(64bit)"}, + {Name: "/bin/sh"}, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + result := v2Handler.toRPMMDRelDepList(tt.deps) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestV2HandlerParseDepsolveResultWithSBOM(t *testing.T) { + v2Handler := newV2Handler() + + input := `{ + "solver": "dnf5", + "transactions": [], + "repos": {}, + "modules": {}, + "sbom": {"spdxVersion": "SPDX-2.3", "name": "test"} + }` + + result, err := v2Handler.parseDepsolveResult([]byte(input)) + require.NoError(t, err) + require.NotNil(t, result) + require.NotNil(t, result.SBOMRaw) + + // Verify SBOM is preserved as raw JSON + var sbomData map[string]interface{} + err = json.Unmarshal(result.SBOMRaw, &sbomData) + require.NoError(t, err) + assert.Equal(t, "SPDX-2.3", sbomData["spdxVersion"]) + assert.Equal(t, "test", sbomData["name"]) +} + +func TestV2HandlerParseDepsolveResultWithModules(t *testing.T) { + v2Handler := newV2Handler() + + input := `{ + "solver": "dnf5", + "transactions": [], + "repos": {}, + "modules": { + "nodejs": { + "module-file": { + "path": "/etc/dnf/modules.d/nodejs.module", + "data": { + "name": "nodejs", + "stream": "18", + "profiles": ["default"], + "state": "enabled" + } + }, + "failsafe-file": { + "path": "/etc/dnf/modules.d/nodejs.failsafe", + "data": "failsafe data" + } + } + } + }` + + result, err := v2Handler.parseDepsolveResult([]byte(input)) + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result.Modules, 1) + + mod := result.Modules[0] + assert.Equal(t, "/etc/dnf/modules.d/nodejs.module", mod.ModuleConfigFile.Path) + assert.Equal(t, "nodejs", mod.ModuleConfigFile.Data.Name) + assert.Equal(t, "18", mod.ModuleConfigFile.Data.Stream) + assert.Equal(t, []string{"default"}, mod.ModuleConfigFile.Data.Profiles) + assert.Equal(t, "enabled", mod.ModuleConfigFile.Data.State) + assert.Equal(t, "/etc/dnf/modules.d/nodejs.failsafe", mod.FailsafeFile.Path) + assert.Equal(t, "failsafe data", mod.FailsafeFile.Data) +} + +// TestV2HandlerParseDepsolveResultDetails verifies that parseDepsolveResult +// correctly parses all package and repository fields into rpmmd types. +func TestV2HandlerParseDepsolveResultDetails(t *testing.T) { + v2Handler := newV2Handler() + + result, err := v2Handler.parseDepsolveResult([]byte(testParseDetailsInput)) + require.NoError(t, err) + require.NotNil(t, result) + + // Verify structure + require.Len(t, result.Transactions, 1) + require.Len(t, result.Transactions[0], 1) + require.Len(t, result.Packages, 1) + require.Len(t, result.Repos, 1) + assert.Equal(t, "dnf5", result.Solver) + + // Verify package - single assert compares all fields + assert.Equal(t, testExpectedPackage, result.Packages[0]) + + // Verify repo - single assert compares all fields + assert.Equal(t, testExpectedRepo, result.Repos[0]) +} + +// TestV2HandlerParseDumpSearchResultDetails verifies that parseDumpResult and +// parseSearchResult correctly parse all package and repository fields into rpmmd types. +func TestV2HandlerParseDumpSearchResultDetails(t *testing.T) { + v2Handler := newV2Handler() + + // Test parseDumpResult + t.Run("parseDumpResult", func(t *testing.T) { + result, err := v2Handler.parseDumpResult([]byte(testParseDetailsDumpSearchInput)) + require.NoError(t, err) + require.NotNil(t, result) + + require.Len(t, result.Packages, 1) + require.Len(t, result.Repos, 1) + assert.Equal(t, "dnf5", result.Solver) + + // Verify package - single assert compares all fields + assert.Equal(t, testExpectedPackage, result.Packages[0]) + + // Verify repo - single assert compares all fields + assert.Equal(t, testExpectedRepo, result.Repos[0]) + }) + + // Test parseSearchResult + t.Run("parseSearchResult", func(t *testing.T) { + result, err := v2Handler.parseSearchResult([]byte(testParseDetailsDumpSearchInput)) + require.NoError(t, err) + require.NotNil(t, result) + + require.Len(t, result.Packages, 1) + require.Len(t, result.Repos, 1) + assert.Equal(t, "dnf5", result.Solver) + + // Verify package - single assert compares all fields + assert.Equal(t, testExpectedPackage, result.Packages[0]) + + // Verify repo - single assert compares all fields + assert.Equal(t, testExpectedRepo, result.Repos[0]) + }) +} diff --git a/pkg/osbuild/rhsm_stage.go b/pkg/osbuild/rhsm_stage.go index 49a2aa8aa7..4dd68333a8 100644 --- a/pkg/osbuild/rhsm_stage.go +++ b/pkg/osbuild/rhsm_stage.go @@ -116,17 +116,13 @@ func NewRHSMStageOptions(config *subscription.RHSMConfig) *RHSMStageOptions { if subManConfRhsmManageRepos != nil || subManConfRhsmAutoEnableYumPlugins != nil { options.SubMan.Rhsm = &SubManConfigRHSMSection{} - if subManConfRhsmManageRepos != nil { - options.SubMan.Rhsm.ManageRepos = common.ToPtr(*subManConfRhsmManageRepos) - } - if subManConfRhsmAutoEnableYumPlugins != nil { - options.SubMan.Rhsm.AutoEnableYumPlugins = common.ToPtr(*subManConfRhsmAutoEnableYumPlugins) - } + options.SubMan.Rhsm.ManageRepos = common.ClonePtr(subManConfRhsmManageRepos) + options.SubMan.Rhsm.AutoEnableYumPlugins = common.ClonePtr(subManConfRhsmAutoEnableYumPlugins) } if subManConfRhsmcertdAutoReg != nil { options.SubMan.Rhsmcertd = &SubManConfigRHSMCERTDSection{ - AutoRegistration: common.ToPtr(*subManConfRhsmcertdAutoReg), + AutoRegistration: common.ClonePtr(subManConfRhsmcertdAutoReg), } } } diff --git a/pkg/rpmmd/package.go b/pkg/rpmmd/package.go index 395f0151b6..6af27bd210 100644 --- a/pkg/rpmmd/package.go +++ b/pkg/rpmmd/package.go @@ -125,6 +125,12 @@ type Package struct { IgnoreSSL bool } +// FullNEVRA returns the package's Name-Epoch:Version-Release.Arch string. +// Epoch is never omitted. +func (p Package) FullNEVRA() string { + return fmt.Sprintf("%s-%d:%s-%s.%s", p.Name, p.Epoch, p.Version, p.Release, p.Arch) +} + // EVRA returns the package's Epoch:Version-Release.Arch string. // If the package Epoch is 0, it is omitted and only Version-Release.Arch is returned. func (p Package) EVRA() string { diff --git a/pkg/rpmmd/package_test.go b/pkg/rpmmd/package_test.go index ca746d99e8..b7be6cbd70 100644 --- a/pkg/rpmmd/package_test.go +++ b/pkg/rpmmd/package_test.go @@ -73,3 +73,8 @@ func TestPackageGetEVRA(t *testing.T) { assert.Equal(t, "3.3a-3.fc38.x86_64", packageList[0].EVRA()) assert.Equal(t, "1:2.06-94.fc38.noarch", packageList[1].EVRA()) } + +func TestPackageGetFullNEVRA(t *testing.T) { + assert.Equal(t, "tmux-0:3.3a-3.fc38.x86_64", packageList[0].FullNEVRA()) + assert.Equal(t, "grub2-1:2.06-94.fc38.noarch", packageList[1].FullNEVRA()) +}