diff --git a/internal/operator-controller/bundle/versionrelease.go b/internal/operator-controller/bundle/versionrelease.go new file mode 100644 index 0000000000..f9dd91a362 --- /dev/null +++ b/internal/operator-controller/bundle/versionrelease.go @@ -0,0 +1,89 @@ +package bundle + +import ( + "errors" + "fmt" + "strings" + + bsemver "github.com/blang/semver/v4" + + slicesutil "github.com/operator-framework/operator-controller/internal/shared/util/slices" +) + +func NewLegacyRegistryV1VersionRelease(vStr string) (*VersionRelease, error) { + vers, err := bsemver.Parse(vStr) + if err != nil { + return nil, err + } + + rel, err := NewRelease(strings.Join(vers.Build, ".")) + if err != nil { + return nil, err + } + vers.Build = nil + + return &VersionRelease{ + Version: vers, + Release: rel, + }, nil +} + +type VersionRelease struct { + Version bsemver.Version + Release Release +} + +func (vr *VersionRelease) Compare(other VersionRelease) int { + if vCmp := vr.Version.Compare(other.Version); vCmp != 0 { + return vCmp + } + return vr.Release.Compare(other.Release) +} + +func (vr *VersionRelease) AsLegacyRegistryV1Version() bsemver.Version { + return bsemver.Version{ + Major: vr.Version.Major, + Minor: vr.Version.Minor, + Patch: vr.Version.Patch, + Pre: vr.Version.Pre, + Build: slicesutil.Map(vr.Release, func(i bsemver.PRVersion) string { return i.String() }), + } +} + +type Release []bsemver.PRVersion + +func (r Release) Compare(other Release) int { + if len(r) == 0 && len(other) > 0 { + return -1 + } + if len(other) == 0 && len(r) > 0 { + return 1 + } + a := bsemver.Version{Pre: r} + b := bsemver.Version{Pre: other} + return a.Compare(b) +} + +func NewRelease(relStr string) (Release, error) { + if relStr == "" { + return nil, nil + } + + var ( + segments = strings.Split(relStr, ".") + r = make(Release, 0, len(segments)) + errs []error + ) + for i, segment := range segments { + prVer, err := bsemver.NewPRVersion(segment) + if err != nil { + errs = append(errs, fmt.Errorf("segment %d: %v", i, err)) + continue + } + r = append(r, prVer) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid release %q: %v", relStr, err) + } + return r, nil +} diff --git a/internal/operator-controller/bundle/versionrelease_test.go b/internal/operator-controller/bundle/versionrelease_test.go new file mode 100644 index 0000000000..77542603bd --- /dev/null +++ b/internal/operator-controller/bundle/versionrelease_test.go @@ -0,0 +1,121 @@ +package bundle_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" +) + +func TestLegacyRegistryV1VersionRelease_Compare(t *testing.T) { + type testCase struct { + name string + v1 string + v2 string + expect int + } + for _, tc := range []testCase{ + { + name: "lower major version", + v1: "1.0.0-0+0", + v2: "2.0.0-0+0", + expect: -1, + }, + { + name: "lower minor version", + v1: "0.1.0-0+0", + v2: "0.2.0-0+0", + expect: -1, + }, + { + name: "lower patch version", + v1: "0.0.1-0+0", + v2: "0.0.2-0+0", + expect: -1, + }, + { + name: "lower prerelease version", + v1: "0.0.0-1+0", + v2: "0.0.0-2+0", + expect: -1, + }, + { + name: "lower build metadata", + v1: "0.0.0-0+1", + v2: "0.0.0-0+2", + expect: -1, + }, + { + name: "same major version", + v1: "1.0.0-0+0", + v2: "1.0.0-0+0", + expect: 0, + }, + { + name: "same minor version", + v1: "0.1.0-0+0", + v2: "0.1.0-0+0", + expect: 0, + }, + { + name: "same patch version", + v1: "0.0.1-0+0", + v2: "0.0.1-0+0", + expect: 0, + }, + { + name: "same prerelease version", + v1: "0.0.0-1+0", + v2: "0.0.0-1+0", + expect: 0, + }, + { + name: "same build metadata", + v1: "0.0.0-0+1", + v2: "0.0.0-0+1", + expect: 0, + }, + { + name: "higher major version", + v1: "2.0.0-0+0", + v2: "1.0.0-0+0", + expect: 1, + }, + { + name: "higher minor version", + v1: "0.2.0-0+0", + v2: "0.1.0-0+0", + expect: 1, + }, + { + name: "higher patch version", + v1: "0.0.2-0+0", + v2: "0.0.1-0+0", + expect: 1, + }, + { + name: "higher prerelease version", + v1: "0.0.0-2+0", + v2: "0.0.0-1+0", + expect: 1, + }, + { + name: "higher build metadata", + v1: "0.0.0-0+2", + v2: "0.0.0-0+1", + expect: 1, + }, + } { + t.Run(tc.name, func(t *testing.T) { + vr1, err1 := bundle.NewLegacyRegistryV1VersionRelease(tc.v1) + vr2, err2 := bundle.NewLegacyRegistryV1VersionRelease(tc.v2) + require.NoError(t, err1) + require.NoError(t, err2) + + actual := vr1.Compare(*vr2) + assert.Equal(t, tc.expect, actual) + }) + } +} diff --git a/internal/operator-controller/bundleutil/bundle.go b/internal/operator-controller/bundleutil/bundle.go index e123680687..2771c52593 100644 --- a/internal/operator-controller/bundleutil/bundle.go +++ b/internal/operator-controller/bundleutil/bundle.go @@ -10,20 +10,26 @@ import ( "github.com/operator-framework/operator-registry/alpha/property" ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" ) -func GetVersion(b declcfg.Bundle) (*bsemver.Version, error) { +func GetVersionAndRelease(b declcfg.Bundle) (*bundle.VersionRelease, error) { for _, p := range b.Properties { if p.Type == property.TypePackage { var pkg property.Package if err := json.Unmarshal(p.Value, &pkg); err != nil { return nil, fmt.Errorf("error unmarshalling package property: %w", err) } - vers, err := bsemver.Parse(pkg.Version) + + // TODO: For now, we assume that all bundles are registry+v1 bundles. + // In the future, when we support other bundle formats, we should stop + // using the legacy mechanism (i.e. using build metadata in the version) + // to determine the bundle's release. + vr, err := bundle.NewLegacyRegistryV1VersionRelease(pkg.Version) if err != nil { return nil, err } - return &vers, nil + return vr, nil } } return nil, fmt.Errorf("no package property found in bundle %q", b.Name) diff --git a/internal/operator-controller/bundleutil/bundle_test.go b/internal/operator-controller/bundleutil/bundle_test.go index 77b7e3bbe6..61f71547f5 100644 --- a/internal/operator-controller/bundleutil/bundle_test.go +++ b/internal/operator-controller/bundleutil/bundle_test.go @@ -12,7 +12,7 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil" ) -func TestGetVersion(t *testing.T) { +func TestGetVersionAndRelease(t *testing.T) { tests := []struct { name string pkgProperty *property.Property @@ -22,7 +22,7 @@ func TestGetVersion(t *testing.T) { name: "valid version", pkgProperty: &property.Property{ Type: property.TypePackage, - Value: json.RawMessage(`{"version": "1.0.0"}`), + Value: json.RawMessage(`{"version": "1.0.0-pre+1.alpha.2"}`), }, wantErr: false, }, @@ -34,6 +34,14 @@ func TestGetVersion(t *testing.T) { }, wantErr: true, }, + { + name: "invalid release", + pkgProperty: &property.Property{ + Type: property.TypePackage, + Value: json.RawMessage(`{"version": "1.0.0+001"}`), + }, + wantErr: true, + }, { name: "invalid json", pkgProperty: &property.Property{ @@ -61,7 +69,7 @@ func TestGetVersion(t *testing.T) { Properties: properties, } - _, err := bundleutil.GetVersion(bundle) + _, err := bundleutil.GetVersionAndRelease(bundle) if tc.wantErr { require.Error(t, err) } else { diff --git a/internal/operator-controller/catalogmetadata/compare/compare.go b/internal/operator-controller/catalogmetadata/compare/compare.go index 4c52eda4e1..c95c11bb27 100644 --- a/internal/operator-controller/catalogmetadata/compare/compare.go +++ b/internal/operator-controller/catalogmetadata/compare/compare.go @@ -1,25 +1,71 @@ package compare import ( + "slices" + "strings" + + mmsemver "github.com/Masterminds/semver/v3" + bsemver "github.com/blang/semver/v4" "k8s.io/apimachinery/pkg/util/sets" "github.com/operator-framework/operator-registry/alpha/declcfg" "github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil" + slicesutil "github.com/operator-framework/operator-controller/internal/shared/util/slices" ) -// ByVersion is a sort "less" function that orders bundles -// in inverse version order (higher versions on top). -func ByVersion(b1, b2 declcfg.Bundle) int { - v1, err1 := bundleutil.GetVersion(b1) - v2, err2 := bundleutil.GetVersion(b2) - if err1 != nil || err2 != nil { - return compareErrors(err1, err2) +// NewVersionRange returns a function that tests whether a semver version is in the +// provided versionRange. The versionRange provided to this function can be any valid semver +// version string or any range that matches the syntax defined in the Masterminds semver library. +// +// When the provided version range is a valid semver version that includes build metadata, then the +// returned function will only match an identical version with the same build metadata. +// +// When the provided version range is a valid semver version that does NOT include build metadata, +// then the returned function will match any version that matches the semver version, ignoring the +// build metadata of matched versions. +// +// Otherwise, Masterminds semver constraints semantics are used to match versions. +// See https://github.com/Masterminds/semver#checking-version-constraints +func NewVersionRange(versionRange string) (bsemver.Range, error) { + if versionPin, err := bsemver.Parse(versionRange); err == nil && len(versionPin.Build) > 0 { + return exactVersionMatcher(versionPin), nil } + return newMastermindsRange(versionRange) +} - // Check for "greater than" because - // we want higher versions on top - return v2.Compare(*v1) +func exactVersionMatcher(pin bsemver.Version) bsemver.Range { + return func(v bsemver.Version) bool { + return pin.Compare(v) == 0 && slices.Compare(pin.Build, v.Build) == 0 + } +} + +func newMastermindsRange(versionRange string) (bsemver.Range, error) { + constraint, err := mmsemver.NewConstraint(versionRange) + if err != nil { + return nil, err + } + return func(in bsemver.Version) bool { + pre := slicesutil.Map(in.Pre, func(pr bsemver.PRVersion) string { return pr.String() }) + mmVer := mmsemver.New(in.Major, in.Minor, in.Patch, strings.Join(pre, "."), strings.Join(in.Build, ".")) + return constraint.Check(mmVer) + }, nil +} + +// ByVersionAndRelease is a comparison function that compares bundles by +// version and release. Bundles with lower versions/releases are +// considered less than bundles with higher versions/releases. +func ByVersionAndRelease(b1, b2 declcfg.Bundle) int { + vr1, err1 := bundleutil.GetVersionAndRelease(b1) + vr2, err2 := bundleutil.GetVersionAndRelease(b2) + + // We don't really expect errors, because we expect well-formed/validated + // FBC as input. However, just in case we'll check the errors and sort + // invalid bundles as "lower" than valid bundles. + if err1 != nil || err2 != nil { + return compareErrors(err2, err1) + } + return vr2.Compare(*vr1) } func ByDeprecationFunc(deprecation declcfg.Deprecation) func(a, b declcfg.Bundle) int { @@ -42,16 +88,16 @@ func ByDeprecationFunc(deprecation declcfg.Deprecation) func(a, b declcfg.Bundle } } -// compareErrors returns 0 if both errors are either nil or not nil -// -1 if err1 is nil and err2 is not nil -// +1 if err1 is not nil and err2 is nil +// compareErrors returns 0 if both errors are either nil or not nil, +// -1 if err1 is not nil and err2 is nil, and +// +1 if err1 is nil and err2 is not nil +// The semantic is that errors are "less than" non-errors. func compareErrors(err1 error, err2 error) int { if err1 != nil && err2 == nil { - return 1 + return -1 } - if err1 == nil && err2 != nil { - return -1 + return 1 } return 0 } diff --git a/internal/operator-controller/catalogmetadata/compare/compare_test.go b/internal/operator-controller/catalogmetadata/compare/compare_test.go index c5d1735dc2..da738d55fc 100644 --- a/internal/operator-controller/catalogmetadata/compare/compare_test.go +++ b/internal/operator-controller/catalogmetadata/compare/compare_test.go @@ -5,7 +5,9 @@ import ( "slices" "testing" + bsemver "github.com/blang/semver/v4" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/operator-framework/operator-registry/alpha/declcfg" "github.com/operator-framework/operator-registry/alpha/property" @@ -13,7 +15,49 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/compare" ) -func TestByVersion(t *testing.T) { +func TestNewVersionRange(t *testing.T) { + type testCase struct { + name string + versionRange string + inputVersion bsemver.Version + expect bool + } + for _, tc := range []testCase{ + { + versionRange: "1.0.0+1", + inputVersion: bsemver.MustParse("1.0.0"), + expect: false, + }, + { + versionRange: "1.0.0+1", + inputVersion: bsemver.MustParse("1.0.0+2"), + expect: false, + }, + { + versionRange: "1.0.0+1", + inputVersion: bsemver.MustParse("1.0.0+1"), + expect: true, + }, + { + versionRange: "1.0.0", + inputVersion: bsemver.MustParse("1.0.0"), + expect: true, + }, + { + versionRange: "1.0.0", + inputVersion: bsemver.MustParse("1.0.0+1"), + expect: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + versionRange, err := compare.NewVersionRange(tc.versionRange) + require.NoError(t, err) + assert.Equal(t, tc.expect, versionRange(tc.inputVersion)) + }) + } +} + +func TestByVersionAndRelease(t *testing.T) { b1 := declcfg.Bundle{ Name: "package1.v1.0.0", Properties: []property.Property{ @@ -32,12 +76,21 @@ func TestByVersion(t *testing.T) { }, }, } - b3 := declcfg.Bundle{ - Name: "package1.v1.0.0-alpha+001", + b3_1 := declcfg.Bundle{ + Name: "package1.v1.0.0-alpha+1", + Properties: []property.Property{ + { + Type: property.TypePackage, + Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0-alpha+1"}`), + }, + }, + } + b3_2 := declcfg.Bundle{ + Name: "package1.v1.0.0-alpha+2", Properties: []property.Property{ { Type: property.TypePackage, - Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0-alpha+001"}`), + Value: json.RawMessage(`{"packageName": "package1", "version": "1.0.0-alpha+2"}`), }, }, } @@ -55,15 +108,15 @@ func TestByVersion(t *testing.T) { } t.Run("all bundles valid", func(t *testing.T) { - toSort := []declcfg.Bundle{b3, b2, b1} - slices.SortStableFunc(toSort, compare.ByVersion) - assert.Equal(t, []declcfg.Bundle{b1, b3, b2}, toSort) + toSort := []declcfg.Bundle{b3_1, b2, b3_2, b1} + slices.SortStableFunc(toSort, compare.ByVersionAndRelease) + assert.Equal(t, []declcfg.Bundle{b1, b3_2, b3_1, b2}, toSort) }) t.Run("some bundles are missing version", func(t *testing.T) { - toSort := []declcfg.Bundle{b3, b4noVersion, b2, b5empty, b1} - slices.SortStableFunc(toSort, compare.ByVersion) - assert.Equal(t, []declcfg.Bundle{b1, b3, b2, b4noVersion, b5empty}, toSort) + toSort := []declcfg.Bundle{b3_1, b4noVersion, b2, b3_2, b5empty, b1} + slices.SortStableFunc(toSort, compare.ByVersionAndRelease) + assert.Equal(t, []declcfg.Bundle{b1, b3_2, b3_1, b2, b4noVersion, b5empty}, toSort) }) } diff --git a/internal/operator-controller/catalogmetadata/filter/bundle_predicates.go b/internal/operator-controller/catalogmetadata/filter/bundle_predicates.go index ecea3783b7..98875c424b 100644 --- a/internal/operator-controller/catalogmetadata/filter/bundle_predicates.go +++ b/internal/operator-controller/catalogmetadata/filter/bundle_predicates.go @@ -1,29 +1,32 @@ package filter import ( - mmsemver "github.com/Masterminds/semver/v3" + bsemver "github.com/blang/semver/v4" "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil" "github.com/operator-framework/operator-controller/internal/shared/util/filter" ) -func InMastermindsSemverRange(semverRange *mmsemver.Constraints) filter.Predicate[declcfg.Bundle] { +func ExactVersionRelease(expect bundle.VersionRelease) filter.Predicate[declcfg.Bundle] { return func(b declcfg.Bundle) bool { - bVersion, err := bundleutil.GetVersion(b) + actual, err := bundleutil.GetVersionAndRelease(b) if err != nil { return false } - // No error should occur here because the simple version was successfully parsed by blang - // We are unaware of any tests cases that would cause one to fail but not the other - // This will cause code coverage to drop for this line. We don't ignore the error because - // there might be that one extreme edge case that might cause one to fail but not the other - mVersion, err := mmsemver.NewVersion(bVersion.String()) + return expect.Compare(*actual) == 0 + } +} + +func InSemverRange(versionRange bsemver.Range) filter.Predicate[declcfg.Bundle] { + return func(b declcfg.Bundle) bool { + vr, err := bundleutil.GetVersionAndRelease(b) if err != nil { return false } - return semverRange.Check(mVersion) + return versionRange(vr.Version) } } diff --git a/internal/operator-controller/catalogmetadata/filter/bundle_predicates_test.go b/internal/operator-controller/catalogmetadata/filter/bundle_predicates_test.go index da47b961f7..4839190cf0 100644 --- a/internal/operator-controller/catalogmetadata/filter/bundle_predicates_test.go +++ b/internal/operator-controller/catalogmetadata/filter/bundle_predicates_test.go @@ -4,17 +4,17 @@ import ( "encoding/json" "testing" - mmsemver "github.com/Masterminds/semver/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/operator-framework/operator-registry/alpha/declcfg" "github.com/operator-framework/operator-registry/alpha/property" + "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/compare" "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/filter" ) -func TestInMastermindsSemverRange(t *testing.T) { +func TestInSemverRange(t *testing.T) { b1 := declcfg.Bundle{ Properties: []property.Property{ { @@ -40,10 +40,10 @@ func TestInMastermindsSemverRange(t *testing.T) { }, } - vRange, err := mmsemver.NewConstraint(">=1.0.0") + vRange, err := compare.NewVersionRange(">=1.0.0") require.NoError(t, err) - f := filter.InMastermindsSemverRange(vRange) + f := filter.InSemverRange(vRange) assert.True(t, f(b1)) assert.False(t, f(b2)) diff --git a/internal/operator-controller/catalogmetadata/filter/successors.go b/internal/operator-controller/catalogmetadata/filter/successors.go index c4abb32589..975c8cb39f 100644 --- a/internal/operator-controller/catalogmetadata/filter/successors.go +++ b/internal/operator-controller/catalogmetadata/filter/successors.go @@ -3,24 +3,24 @@ package filter import ( "fmt" - mmsemver "github.com/Masterminds/semver/v3" bsemver "github.com/blang/semver/v4" "github.com/operator-framework/operator-registry/alpha/declcfg" ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" "github.com/operator-framework/operator-controller/internal/shared/util/filter" ) func SuccessorsOf(installedBundle ocv1.BundleMetadata, channels ...declcfg.Channel) (filter.Predicate[declcfg.Bundle], error) { - installedBundleVersion, err := mmsemver.NewVersion(installedBundle.Version) + // TODO: We do not have an explicit field in our BundleMetadata for a bundle's release value. + // Legacy registry+v1 bundles embed the release value inside their versions as build metadata + // (in violation of the semver spec). If/when we add explicit release metadata to bundles and/or + // we support a new bundle format, we need to revisit the assumption that all bundles are + // registry+v1 and embed release in build metadata. + installedVersionRelease, err := bundle.NewLegacyRegistryV1VersionRelease(installedBundle.Version) if err != nil { - return nil, fmt.Errorf("parsing installed bundle %q version %q: %w", installedBundle.Name, installedBundle.Version, err) - } - - installedVersionConstraint, err := mmsemver.NewConstraint(installedBundleVersion.String()) - if err != nil { - return nil, fmt.Errorf("parsing installed version constraint %q: %w", installedBundleVersion.String(), err) + return nil, fmt.Errorf("failed to get version and release of installed bundle: %v", err) } successorsPredicate, err := legacySuccessor(installedBundle, channels...) @@ -31,7 +31,7 @@ func SuccessorsOf(installedBundle ocv1.BundleMetadata, channels ...declcfg.Chann // We need either successors or current version (no upgrade) return filter.Or( successorsPredicate, - InMastermindsSemverRange(installedVersionConstraint), + ExactVersionRelease(*installedVersionRelease), ), nil } diff --git a/internal/operator-controller/catalogmetadata/filter/successors_test.go b/internal/operator-controller/catalogmetadata/filter/successors_test.go index 0d3fb45d2b..d22a1fdb2f 100644 --- a/internal/operator-controller/catalogmetadata/filter/successors_test.go +++ b/internal/operator-controller/catalogmetadata/filter/successors_test.go @@ -36,6 +36,7 @@ func TestSuccessorsPredicate(t *testing.T) { { Name: "test-package.v2.2.0", Replaces: "test-package.v2.1.0", + Skips: []string{"test-package.v2.0.0+1"}, }, { Name: "test-package.v2.2.1", @@ -64,6 +65,14 @@ func TestSuccessorsPredicate(t *testing.T) { property.MustBuildPackage(testPackageName, "2.0.0"), }, }, + "test-package.v2.0.0+1": { + Name: "test-package.v2.0.0+1", + Package: testPackageName, + Image: "registry.io/repo/test-package@v2.0.0+1", + Properties: []property.Property{ + property.MustBuildPackage(testPackageName, "2.0.0+1"), + }, + }, "test-package.v2.1.0": { Name: "test-package.v2.1.0", Package: testPackageName, @@ -144,6 +153,22 @@ func TestSuccessorsPredicate(t *testing.T) { bundleSet["test-package.v2.3.0"], }, }, + { + name: "installed bundle matcher is exact", + installedBundle: bundleutil.MetadataFor("test-package.v2.0.0+1", bsemver.MustParse("2.0.0+1")), + expectedResult: []declcfg.Bundle{ + // Must only have two bundle: + // - the one which is skips the current version + // - the current version (to allow to stay on the current version) + // + // We specifically _do not_ want to see test-package.v2.1.0 here because: + // - the successor determination is based on an exact match of the version, including build metadata if present + // - 2.1.0 updates from 2.0.0, not 2.0.0+1. Semver would say that both of these are the same. In our case, + // for registry+v1 only, they are not the same. + bundleSet["test-package.v2.2.0"], + bundleSet["test-package.v2.0.0+1"], + }, + }, { name: "installed bundle not found", installedBundle: ocv1.BundleMetadata{ @@ -164,7 +189,7 @@ func TestSuccessorsPredicate(t *testing.T) { result := filter.InPlace(allBundles, successors) // sort before comparison for stable order - slices.SortFunc(result, compare.ByVersion) + slices.SortFunc(result, compare.ByVersionAndRelease) gocmpopts := []cmp.Option{ cmpopts.IgnoreUnexported(declcfg.Bundle{}), diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go index 7bcedde656..f658d80b68 100644 --- a/internal/operator-controller/controllers/clusterextension_controller.go +++ b/internal/operator-controller/controllers/clusterextension_controller.go @@ -261,9 +261,13 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl // all catalogs? SetDeprecationStatus(ext, resolvedBundle.Name, resolvedDeprecation) resolvedRevisionMetadata = &RevisionMetadata{ - Package: resolvedBundle.Package, - Image: resolvedBundle.Image, - BundleMetadata: bundleutil.MetadataFor(resolvedBundle.Name, *resolvedBundleVersion), + Package: resolvedBundle.Package, + Image: resolvedBundle.Image, + // TODO: Right now, operator-controller only supports registry+v1 bundles and has no concept + // of a "release" field. If/when we add a release field concept or a new bundle format + // we need to re-evaluate use of `AsLegacyRegistryV1Version` so that we avoid propagating + // registry+v1's semver spec violations of treating build metadata as orderable. + BundleMetadata: bundleutil.MetadataFor(resolvedBundle.Name, resolvedBundleVersion.AsLegacyRegistryV1Version()), } } else { resolvedRevisionMetadata = revisionStates.RollingOut[0] diff --git a/internal/operator-controller/controllers/clusterextension_controller_test.go b/internal/operator-controller/controllers/clusterextension_controller_test.go index 437f62dcec..c73f74d93b 100644 --- a/internal/operator-controller/controllers/clusterextension_controller_test.go +++ b/internal/operator-controller/controllers/clusterextension_controller_test.go @@ -29,6 +29,7 @@ import ( ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/authentication" + "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/conditionsets" "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" "github.com/operator-framework/operator-controller/internal/operator-controller/finalizers" @@ -124,7 +125,7 @@ func TestClusterExtensionShortCircuitsReconcileDuringDeletion(t *testing.T) { func TestClusterExtensionResolutionFails(t *testing.T) { pkgName := fmt.Sprintf("non-existent-%s", rand.String(6)) cl, reconciler := newClientAndReconciler(t) - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { + reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { return nil, nil, nil, fmt.Errorf("no package %q found", pkgName) }) ctx := context.Background() @@ -228,13 +229,15 @@ func TestClusterExtensionResolutionSuccessfulUnpackFails(t *testing.T) { t.Log("It sets resolution success status") t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") + reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + vr := &bundle.VersionRelease{ + Version: bsemver.MustParse("1.0.0"), + } return &declcfg.Bundle{ Name: "prometheus.v1.0.0", Package: "prometheus", Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil + }, vr, nil, nil }) res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) require.Equal(t, ctrl.Result{}, res) @@ -308,13 +311,15 @@ func TestClusterExtensionResolutionAndUnpackSuccessfulApplierFails(t *testing.T) t.Log("It sets resolution success status") t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") + reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + vr := &bundle.VersionRelease{ + Version: bsemver.MustParse("1.0.0"), + } return &declcfg.Bundle{ Name: "prometheus.v1.0.0", Package: "prometheus", Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil + }, vr, nil, nil }) reconciler.Applier = &MockApplier{ err: errors.New("apply failure"), @@ -439,13 +444,15 @@ func TestClusterExtensionApplierFailsWithBundleInstalled(t *testing.T) { t.Log("It sets resolution success status") t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") + reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + vr := &bundle.VersionRelease{ + Version: bsemver.MustParse("1.0.0"), + } return &declcfg.Bundle{ Name: "prometheus.v1.0.0", Package: "prometheus", Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil + }, vr, nil, nil }) reconciler.RevisionStatesGetter = &MockRevisionStatesGetter{ @@ -534,13 +541,15 @@ func TestClusterExtensionManagerFailed(t *testing.T) { t.Log("It sets resolution success status") t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") + reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + vr := &bundle.VersionRelease{ + Version: bsemver.MustParse("1.0.0"), + } return &declcfg.Bundle{ Name: "prometheus.v1.0.0", Package: "prometheus", Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil + }, vr, nil, nil }) reconciler.Applier = &MockApplier{ installCompleted: true, @@ -611,13 +620,15 @@ func TestClusterExtensionManagedContentCacheWatchFail(t *testing.T) { t.Log("It sets resolution success status") t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") + reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + vr := &bundle.VersionRelease{ + Version: bsemver.MustParse("1.0.0"), + } return &declcfg.Bundle{ Name: "prometheus.v1.0.0", Package: "prometheus", Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil + }, vr, nil, nil }) reconciler.Applier = &MockApplier{ installCompleted: true, @@ -687,13 +698,15 @@ func TestClusterExtensionInstallationSucceeds(t *testing.T) { t.Log("It sets resolution success status") t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") + reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + vr := &bundle.VersionRelease{ + Version: bsemver.MustParse("1.0.0"), + } return &declcfg.Bundle{ Name: "prometheus.v1.0.0", Package: "prometheus", Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil + }, vr, nil, nil }) reconciler.Applier = &MockApplier{ installCompleted: true, @@ -761,13 +774,15 @@ func TestClusterExtensionDeleteFinalizerFails(t *testing.T) { require.NoError(t, err) t.Log("It sets resolution success status") t.Log("By running reconcile") - reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - v := bsemver.MustParse("1.0.0") + reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + vr := &bundle.VersionRelease{ + Version: bsemver.MustParse("1.0.0"), + } return &declcfg.Bundle{ Name: "prometheus.v1.0.0", Package: "prometheus", Image: "quay.io/operatorhubio/prometheus@fake1.0.0", - }, &v, nil, nil + }, vr, nil, nil }) fakeFinalizer := "fake.testfinalizer.io" finalizersMessage := "still have finalizers" diff --git a/internal/operator-controller/resolve/catalog.go b/internal/operator-controller/resolve/catalog.go index 8cd1ebe81d..f0d4da6fab 100644 --- a/internal/operator-controller/resolve/catalog.go +++ b/internal/operator-controller/resolve/catalog.go @@ -7,7 +7,6 @@ import ( "sort" "strings" - mmsemver "github.com/Masterminds/semver/v3" bsemver "github.com/blang/semver/v4" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -18,10 +17,12 @@ import ( "github.com/operator-framework/operator-registry/alpha/declcfg" ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil" "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/compare" "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/filter" filterutil "github.com/operator-framework/operator-controller/internal/shared/util/filter" + slicesutil "github.com/operator-framework/operator-controller/internal/shared/util/slices" ) type ValidationFunc func(*declcfg.Bundle) error @@ -38,7 +39,7 @@ type foundBundle struct { } // Resolve returns a Bundle from a catalog that needs to get installed on the cluster. -func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { +func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { l := log.FromContext(ctx) packageName := ext.Spec.Source.Catalog.PackageName versionRange := ext.Spec.Source.Catalog.Version @@ -58,9 +59,9 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio } } - var versionRangeConstraints *mmsemver.Constraints + var versionRangeConstraints bsemver.Range if versionRange != "" { - versionRangeConstraints, err = mmsemver.NewConstraint(versionRange) + versionRangeConstraints, err = compare.NewVersionRange(versionRange) if err != nil { return nil, nil, nil, fmt.Errorf("desired version range %q is invalid: %w", versionRange, err) } @@ -106,7 +107,7 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio } if versionRangeConstraints != nil { - predicates = append(predicates, filter.InMastermindsSemverRange(versionRangeConstraints)) + predicates = append(predicates, filter.InSemverRange(versionRangeConstraints)) } if ext.Spec.Source.Catalog.UpgradeConstraintPolicy != ocv1.UpgradeConstraintPolicySelfCertified && installedBundle != nil { @@ -140,7 +141,7 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio if lessDep := byDeprecation(a, b); lessDep != 0 { return lessDep } - return compare.ByVersion(a, b) + return compare.ByVersionAndRelease(a, b) }) thisBundle := packageFBC.Bundles[0] @@ -189,7 +190,7 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio } } resolvedBundle := resolvedBundles[0].bundle - resolvedBundleVersion, err := bundleutil.GetVersion(*resolvedBundle) + resolvedBundleVersion, err := bundleutil.GetVersionAndRelease(*resolvedBundle) if err != nil { return nil, nil, nil, fmt.Errorf("error getting resolved bundle version for bundle %q: %w", resolvedBundle.Name, err) } @@ -282,7 +283,7 @@ func CatalogWalker( return false }) - availableCatalogNames := mapSlice(catalogs, func(c ocv1.ClusterCatalog) string { return c.Name }) + availableCatalogNames := slicesutil.Map(catalogs, func(c ocv1.ClusterCatalog) string { return c.Name }) l.Info("using ClusterCatalogs for resolution", "catalogs", availableCatalogNames) for i := range catalogs { @@ -306,11 +307,3 @@ func isFBCEmpty(fbc *declcfg.DeclarativeConfig) bool { } return len(fbc.Packages) == 0 && len(fbc.Channels) == 0 && len(fbc.Bundles) == 0 && len(fbc.Deprecations) == 0 && len(fbc.Others) == 0 } - -func mapSlice[I any, O any](in []I, f func(I) O) []O { - out := make([]O, len(in)) - for i := range in { - out[i] = f(in[i]) - } - return out -} diff --git a/internal/operator-controller/resolve/catalog_test.go b/internal/operator-controller/resolve/catalog_test.go index 21232bc4df..2ec3192b69 100644 --- a/internal/operator-controller/resolve/catalog_test.go +++ b/internal/operator-controller/resolve/catalog_test.go @@ -19,6 +19,7 @@ import ( "github.com/operator-framework/operator-registry/alpha/property" ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" ) func TestInvalidClusterExtensionVersionRange(t *testing.T) { @@ -89,7 +90,7 @@ func TestPackageExists(t *testing.T) { gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil) require.NoError(t, err) assert.Equal(t, genBundle(pkgName, "3.0.0"), *gotBundle) - assert.Equal(t, bsemver.MustParse("3.0.0"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("3.0.0")}, *gotVersion) assert.Equal(t, ptr.To(packageDeprecation(pkgName)), gotDeprecation) } @@ -156,7 +157,7 @@ func TestVersionExists(t *testing.T) { gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil) require.NoError(t, err) assert.Equal(t, genBundle(pkgName, "1.0.2"), *gotBundle) - assert.Equal(t, bsemver.MustParse("1.0.2"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("1.0.2")}, *gotVersion) assert.Equal(t, ptr.To(packageDeprecation(pkgName)), gotDeprecation) } @@ -197,7 +198,7 @@ func TestChannelExists(t *testing.T) { gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil) require.NoError(t, err) assert.Equal(t, genBundle(pkgName, "1.0.2"), *gotBundle) - assert.Equal(t, bsemver.MustParse("1.0.2"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("1.0.2")}, *gotVersion) assert.Equal(t, ptr.To(packageDeprecation(pkgName)), gotDeprecation) } @@ -257,7 +258,7 @@ func TestChannelAndVersionExist(t *testing.T) { gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil) require.NoError(t, err) assert.Equal(t, genBundle(pkgName, "0.1.0"), *gotBundle) - assert.Equal(t, bsemver.MustParse("0.1.0"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("0.1.0")}, *gotVersion) assert.Equal(t, ptr.To(packageDeprecation(pkgName)), gotDeprecation) } @@ -279,7 +280,7 @@ func TestPreferNonDeprecated(t *testing.T) { gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil) require.NoError(t, err) assert.Equal(t, genBundle(pkgName, "0.1.0"), *gotBundle) - assert.Equal(t, bsemver.MustParse("0.1.0"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("0.1.0")}, *gotVersion) assert.Equal(t, ptr.To(packageDeprecation(pkgName)), gotDeprecation) } @@ -301,7 +302,7 @@ func TestAcceptDeprecated(t *testing.T) { gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil) require.NoError(t, err) assert.Equal(t, genBundle(pkgName, "1.0.1"), *gotBundle) - assert.Equal(t, bsemver.MustParse("1.0.1"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("1.0.1")}, *gotVersion) assert.Equal(t, ptr.To(packageDeprecation(pkgName)), gotDeprecation) } @@ -385,7 +386,7 @@ func TestPackageVariationsBetweenCatalogs(t *testing.T) { require.NoError(t, err) // We choose the only non-deprecated package assert.Equal(t, genBundle(pkgName, "1.0.2").Name, gotBundle.Name) - assert.Equal(t, bsemver.MustParse("1.0.2"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("1.0.2")}, *gotVersion) assert.Equal(t, (*declcfg.Deprecation)(nil), gotDeprecation) }) @@ -417,7 +418,7 @@ func TestPackageVariationsBetweenCatalogs(t *testing.T) { require.NoError(t, err) // Bundles within one catalog for a package will be sorted by semver and deprecation and the best is returned assert.Equal(t, genBundle(pkgName, "1.0.5").Name, gotBundle.Name) - assert.Equal(t, bsemver.MustParse("1.0.5"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("1.0.5")}, *gotVersion) assert.Equal(t, (*declcfg.Deprecation)(nil), gotDeprecation) }) } @@ -445,7 +446,7 @@ func TestUpgradeFoundLegacy(t *testing.T) { gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, installedBundle) require.NoError(t, err) assert.Equal(t, genBundle(pkgName, "1.0.2"), *gotBundle) - assert.Equal(t, bsemver.MustParse("1.0.2"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("1.0.2")}, *gotVersion) assert.Equal(t, ptr.To(packageDeprecation(pkgName)), gotDeprecation) } @@ -497,7 +498,7 @@ func TestDowngradeFound(t *testing.T) { gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, installedBundle) require.NoError(t, err) assert.Equal(t, genBundle(pkgName, "0.1.0"), *gotBundle) - assert.Equal(t, bsemver.MustParse("0.1.0"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("0.1.0")}, *gotVersion) assert.Equal(t, ptr.To(packageDeprecation(pkgName)), gotDeprecation) } @@ -854,7 +855,7 @@ func TestUnequalPriority(t *testing.T) { ce := buildFooClusterExtension(pkgName, []string{}, "", ocv1.UpgradeConstraintPolicyCatalogProvided) _, gotVersion, _, err := r.Resolve(context.Background(), ce, nil) require.NoError(t, err) - require.Equal(t, bsemver.MustParse("1.0.0"), *gotVersion) + require.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("1.0.0")}, *gotVersion) } func TestMultiplePriority(t *testing.T) { @@ -899,7 +900,7 @@ func TestMultipleChannels(t *testing.T) { gotBundle, gotVersion, gotDeprecation, err := r.Resolve(context.Background(), ce, nil) require.NoError(t, err) assert.Equal(t, genBundle(pkgName, "2.0.0"), *gotBundle) - assert.Equal(t, bsemver.MustParse("2.0.0"), *gotVersion) + assert.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("2.0.0")}, *gotVersion) assert.Equal(t, ptr.To(packageDeprecation(pkgName)), gotDeprecation) } @@ -972,5 +973,5 @@ func TestSomeCatalogsDisabled(t *testing.T) { gotBundle, gotVersion, _, err := r.Resolve(context.Background(), ce, nil) require.NoError(t, err) require.NotNil(t, gotBundle) - require.Equal(t, bsemver.MustParse("3.0.0"), *gotVersion) + require.Equal(t, bundle.VersionRelease{Version: bsemver.MustParse("3.0.0")}, *gotVersion) } diff --git a/internal/operator-controller/resolve/resolver.go b/internal/operator-controller/resolve/resolver.go index 625111d631..1fbde0fdea 100644 --- a/internal/operator-controller/resolve/resolver.go +++ b/internal/operator-controller/resolve/resolver.go @@ -3,19 +3,18 @@ package resolve import ( "context" - bsemver "github.com/blang/semver/v4" - "github.com/operator-framework/operator-registry/alpha/declcfg" ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" ) type Resolver interface { - Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) + Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) } -type Func func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) +type Func func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) -func (f Func) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { +func (f Func) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { return f(ctx, ext, installedBundle) } diff --git a/internal/shared/util/slices/slices.go b/internal/shared/util/slices/slices.go new file mode 100644 index 0000000000..3c750a1ad8 --- /dev/null +++ b/internal/shared/util/slices/slices.go @@ -0,0 +1,9 @@ +package slices + +func Map[I any, O any](in []I, f func(I) O) []O { + out := make([]O, len(in)) + for i := range in { + out[i] = f(in[i]) + } + return out +}