diff --git a/pkg/controller/registry/resolver/cache/predicates.go b/pkg/controller/registry/resolver/cache/predicates.go index 9bdf097dc94..25200917e93 100644 --- a/pkg/controller/registry/resolver/cache/predicates.go +++ b/pkg/controller/registry/resolver/cache/predicates.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/blang/semver/v4" - opregistry "github.com/operator-framework/operator-registry/pkg/registry" ) @@ -282,6 +281,32 @@ func (p orPredicate) String() string { return b.String() } +func Not(p ...Predicate) Predicate { + return notPredicate{ + predicates: p, + } +} + +type notPredicate struct { + predicates []Predicate +} + +func (p notPredicate) Test(o *Entry) bool { + // !pred && !pred is equivalent to !(pred || pred). + return !orPredicate{p.predicates}.Test(o) +} + +func (p notPredicate) String() string { + var b bytes.Buffer + for i, predicate := range p.predicates { + b.WriteString(predicate.String()) + if i != len(p.predicates)-1 { + b.WriteString(" and not ") + } + } + return b.String() +} + type booleanPredicate struct { result bool } diff --git a/pkg/controller/registry/resolver/constraints/constraint.go b/pkg/controller/registry/resolver/constraints/constraint.go new file mode 100644 index 00000000000..63e078d1ffd --- /dev/null +++ b/pkg/controller/registry/resolver/constraints/constraint.go @@ -0,0 +1,69 @@ +package constraints + +import ( + "bytes" + "encoding/json" + "fmt" +) + +const OLMConstraintType = "olm.constraint" + +// Constraint holds parsed, potentially nested dependency constraints. +type Constraint struct { + Message string `json:"message"` + + Package *PackageConstraint `json:"package,omitempty"` + GVK *GVKConstraint `json:"gvk,omitempty"` + + All *CompoundConstraint `json:"all,omitempty"` + Any *CompoundConstraint `json:"any,omitempty"` + None *CompoundConstraint `json:"none,omitempty"` +} + +// CompoundConstraint holds a list of potentially nested constraints +// over which a boolean operation is applied. +type CompoundConstraint struct { + Constraints []Constraint `json:"constraints"` +} + +// GVKConstraint defines a GVK constraint. +type GVKConstraint struct { + Group string `json:"group"` + Kind string `json:"kind"` + Version string `json:"version"` +} + +// PackageConstraint defines a package constraint. +type PackageConstraint struct { + // Name of the package. + Name string `json:"name"` + // VersionRange required for the package. + VersionRange string `json:"versionRange"` +} + +// maxConstraintSize defines the maximum raw size in bytes of an olm.constraint. +// 64Kb seems reasonable, since this number allows for long description strings +// and either few deep nestings or shallow nestings and long constraints lists, +// but not both. +const maxConstraintSize = 2 << 16 + +// ErrMaxConstraintSizeExceeded is returned when a constraint's size > maxConstraintSize. +var ErrMaxConstraintSizeExceeded = fmt.Errorf("olm.constraint value is greater than max constraint size %d bytes", maxConstraintSize) + +// Parse parses an olm.constraint property's value recursively into a Constraint. +// Unknown value schemas result in an error. A too-large value results in an error. +func Parse(v json.RawMessage) (c Constraint, err error) { + // There is no way to explicitly limit nesting depth. + // From https://github.com/golang/go/issues/31789#issuecomment-538134396, + // the recommended approach is to error out if raw input size + // is greater than some threshold. + if len(v) > maxConstraintSize { + return c, ErrMaxConstraintSizeExceeded + } + + d := json.NewDecoder(bytes.NewBuffer(v)) + d.DisallowUnknownFields() + err = d.Decode(&c) + + return +} diff --git a/pkg/controller/registry/resolver/constraints/constraint_test.go b/pkg/controller/registry/resolver/constraints/constraint_test.go new file mode 100644 index 00000000000..c2bc0138ac2 --- /dev/null +++ b/pkg/controller/registry/resolver/constraints/constraint_test.go @@ -0,0 +1,274 @@ +package constraints + +import ( + "encoding/json" + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParse(t *testing.T) { + type spec struct { + name string + input json.RawMessage + expConstraint Constraint + expError string + } + + specs := []spec{ + { + name: "Valid/BasicGVK", + input: json.RawMessage(inputBasicGVK), + expConstraint: Constraint{ + Message: "blah", + GVK: &GVKConstraint{Group: "example.com", Version: "v1", Kind: "Foo"}, + }, + }, + { + name: "Valid/BasicPackage", + input: json.RawMessage(inputBasicPackage), + expConstraint: Constraint{ + Message: "blah", + Package: &PackageConstraint{Name: "foo", VersionRange: ">=1.0.0"}, + }, + }, + { + name: "Valid/BasicAll", + input: json.RawMessage(fmt.Sprintf(inputBasicCompoundTmpl, "all")), + expConstraint: Constraint{ + Message: "blah", + All: &CompoundConstraint{ + Constraints: []Constraint{ + { + Message: "blah blah", + Package: &PackageConstraint{Name: "fuz", VersionRange: ">=1.0.0"}, + }, + }, + }, + }, + }, + { + name: "Valid/BasicAny", + input: json.RawMessage(fmt.Sprintf(inputBasicCompoundTmpl, "any")), + expConstraint: Constraint{ + Message: "blah", + Any: &CompoundConstraint{ + Constraints: []Constraint{ + { + Message: "blah blah", + Package: &PackageConstraint{Name: "fuz", VersionRange: ">=1.0.0"}, + }, + }, + }, + }, + }, + { + name: "Valid/BasicNone", + input: json.RawMessage(fmt.Sprintf(inputBasicCompoundTmpl, "none")), + expConstraint: Constraint{ + Message: "blah", + None: &CompoundConstraint{ + Constraints: []Constraint{ + { + Message: "blah blah", + Package: &PackageConstraint{Name: "fuz", VersionRange: ">=1.0.0"}, + }, + }, + }, + }, + }, + { + name: "Valid/Complex", + input: json.RawMessage(inputComplex), + expConstraint: Constraint{ + Message: "blah", + All: &CompoundConstraint{ + Constraints: []Constraint{ + {Package: &PackageConstraint{Name: "fuz", VersionRange: ">=1.0.0"}}, + {GVK: &GVKConstraint{Group: "fals.example.com", Kind: "Fal", Version: "v1"}}, + { + Message: "foo and buf must be stable versions", + All: &CompoundConstraint{ + Constraints: []Constraint{ + {Package: &PackageConstraint{Name: "foo", VersionRange: ">=1.0.0"}}, + {Package: &PackageConstraint{Name: "buf", VersionRange: ">=1.0.0"}}, + {GVK: &GVKConstraint{Group: "foos.example.com", Kind: "Foo", Version: "v1"}}, + }, + }, + }, + { + Message: "blah blah", + Any: &CompoundConstraint{ + Constraints: []Constraint{ + {GVK: &GVKConstraint{Group: "foos.example.com", Kind: "Foo", Version: "v1beta1"}}, + {GVK: &GVKConstraint{Group: "foos.example.com", Kind: "Foo", Version: "v1beta2"}}, + {GVK: &GVKConstraint{Group: "foos.example.com", Kind: "Foo", Version: "v1"}}, + }, + }, + }, + { + None: &CompoundConstraint{ + Constraints: []Constraint{ + {GVK: &GVKConstraint{Group: "bazs.example.com", Kind: "Baz", Version: "v1alpha1"}}, + }, + }, + }, + }, + }, + }, + }, + { + name: "Invalid/TooLarge", + input: func(t *testing.T) json.RawMessage { + p := make([]byte, maxConstraintSize+1) + _, err := rand.Read(p) + require.NoError(t, err) + return json.RawMessage(p) + }(t), + expError: ErrMaxConstraintSizeExceeded.Error(), + }, + { + name: "Invalid/UnknownField", + input: json.RawMessage( + `{"message": "something", "arbitrary": {"key": "value"}}`, + ), + expError: `json: unknown field "arbitrary"`, + }, + } + + for _, s := range specs { + t.Run(s.name, func(t *testing.T) { + constraint, err := Parse(s.input) + if s.expError == "" { + require.NoError(t, err) + require.Equal(t, s.expConstraint, constraint) + } else { + require.EqualError(t, err, s.expError) + } + }) + } +} + +const ( + inputBasicGVK = `{ + "message": "blah", + "gvk": { + "group": "example.com", + "version": "v1", + "kind": "Foo" + } + }` + + inputBasicPackage = `{ + "message": "blah", + "package": { + "name": "foo", + "versionRange": ">=1.0.0" + } + }` + + inputBasicCompoundTmpl = `{ +"message": "blah", +"%s": { + "constraints": [ + { + "message": "blah blah", + "package": { + "name": "fuz", + "versionRange": ">=1.0.0" + } + } + ] +}} +` + + inputComplex = `{ +"message": "blah", +"all": { + "constraints": [ + { + "package": { + "name": "fuz", + "versionRange": ">=1.0.0" + } + }, + { + "gvk": { + "group": "fals.example.com", + "version": "v1", + "kind": "Fal" + } + }, + { + "message": "foo and buf must be stable versions", + "all": { + "constraints": [ + { + "package": { + "name": "foo", + "versionRange": ">=1.0.0" + } + }, + { + "package": { + "name": "buf", + "versionRange": ">=1.0.0" + } + }, + { + "gvk": { + "group": "foos.example.com", + "version": "v1", + "kind": "Foo" + } + } + ] + } + }, + { + "message": "blah blah", + "any": { + "constraints": [ + { + "gvk": { + "group": "foos.example.com", + "version": "v1beta1", + "kind": "Foo" + } + }, + { + "gvk": { + "group": "foos.example.com", + "version": "v1beta2", + "kind": "Foo" + } + }, + { + "gvk": { + "group": "foos.example.com", + "version": "v1", + "kind": "Foo" + } + } + ] + } + }, + { + "none": { + "constraints": [ + { + "gvk": { + "group": "bazs.example.com", + "version": "v1alpha1", + "kind": "Baz" + } + } + ] + } + } + ] +}} +` +) diff --git a/pkg/controller/registry/resolver/resolver.go b/pkg/controller/registry/resolver/resolver.go index 5f865a6db9f..261fb5cfcef 100644 --- a/pkg/controller/registry/resolver/resolver.go +++ b/pkg/controller/registry/resolver/resolver.go @@ -14,6 +14,7 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" v1alpha1listers "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/constraints" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/projection" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/solver" "github.com/operator-framework/operator-registry/pkg/api" @@ -27,12 +28,14 @@ type OperatorResolver interface { type SatResolver struct { cache cache.OperatorCacheProvider log logrus.FieldLogger + pc *predicateConverter } func NewDefaultSatResolver(rcp cache.SourceProvider, catsrcLister v1alpha1listers.CatalogSourceLister, logger logrus.FieldLogger) *SatResolver { return &SatResolver{ cache: cache.New(rcp, cache.WithLogger(logger), cache.WithCatalogSourceLister(catsrcLister)), log: logger, + pc: &predicateConverter{}, } } @@ -337,7 +340,7 @@ func (r *SatResolver) getBundleInstallables(preferredNamespace string, bundleSta visited[bundle] = &bundleInstallable - dependencyPredicates, err := DependencyPredicates(bundle.Properties) + dependencyPredicates, err := r.pc.convertDependencyProperties(bundle.Properties) if err != nil { errs = append(errs, err) continue @@ -733,10 +736,14 @@ func sortChannel(bundles []*cache.Entry) ([]*cache.Entry, error) { return chains[0], nil } -func DependencyPredicates(properties []*api.Property) ([]cache.Predicate, error) { +// predicateConverter configures olm.constraint value -> predicate conversion for the resolver. +type predicateConverter struct{} + +// convertDependencyProperties converts all known constraint properties to predicates. +func (pc *predicateConverter) convertDependencyProperties(properties []*api.Property) ([]cache.Predicate, error) { var predicates []cache.Predicate for _, property := range properties { - predicate, err := predicateForProperty(property) + predicate, err := pc.predicateForProperty(property) if err != nil { return nil, err } @@ -748,18 +755,80 @@ func DependencyPredicates(properties []*api.Property) ([]cache.Predicate, error) return predicates, nil } -func predicateForProperty(property *api.Property) (cache.Predicate, error) { +func (pc *predicateConverter) predicateForProperty(property *api.Property) (cache.Predicate, error) { if property == nil { return nil, nil } - p, ok := predicates[property.Type] + + // olm.constraint holds all constraint types except legacy types, + // so defer error handling to its parser. + if property.Type == constraints.OLMConstraintType { + return pc.predicateForConstraintProperty(property.Value) + } + + // Legacy behavior dictates that unknown properties are ignored. See enhancement for details: + // https://github.com/operator-framework/enhancements/blob/master/enhancements/compound-bundle-constraints.md + p, ok := legacyPredicateParsers[property.Type] if !ok { return nil, nil } return p(property.Value) } -var predicates = map[string]func(string) (cache.Predicate, error){ +func (pc *predicateConverter) predicateForConstraintProperty(value string) (cache.Predicate, error) { + constraint, err := constraints.Parse(json.RawMessage([]byte(value))) + if err != nil { + return nil, fmt.Errorf("parse olm.constraint: %v", err) + } + + preds, err := pc.convertConstraints(constraint) + if err != nil { + return nil, fmt.Errorf("convert olm.constraint to resolver predicate: %v", err) + } + return preds[0], nil +} + +// convertConstraints creates predicates from each element of constraints, recursing on compound constraints. +// New constraint types added to the constraints package must be handled here. +func (pc *predicateConverter) convertConstraints(constraints ...constraints.Constraint) ([]cache.Predicate, error) { + + preds := make([]cache.Predicate, len(constraints)) + for i, constraint := range constraints { + + var err error + switch { + case constraint.GVK != nil: + preds[i] = cache.ProvidingAPIPredicate(opregistry.APIKey{ + Group: constraint.GVK.Group, + Version: constraint.GVK.Version, + Kind: constraint.GVK.Kind, + }) + case constraint.Package != nil: + preds[i], err = newPackageRequiredPredicate(constraint.Package.Name, constraint.Package.VersionRange) + case constraint.All != nil: + subs, perr := pc.convertConstraints(constraint.All.Constraints...) + preds[i], err = cache.And(subs...), perr + case constraint.Any != nil: + subs, perr := pc.convertConstraints(constraint.Any.Constraints...) + preds[i], err = cache.Or(subs...), perr + case constraint.None != nil: + subs, perr := pc.convertConstraints(constraint.None.Constraints...) + preds[i], err = cache.Not(subs...), perr + default: + // Unknown constraint types are handled by constraints.Parse(), + // but parsed constraints may be empty. + return nil, fmt.Errorf("constraint is empty") + } + if err != nil { + return nil, err + } + + } + + return preds, nil +} + +var legacyPredicateParsers = map[string]func(string) (cache.Predicate, error){ "olm.gvk.required": predicateForRequiredGVKProperty, "olm.package.required": predicateForRequiredPackageProperty, "olm.label.required": predicateForRequiredLabelProperty, @@ -789,11 +858,15 @@ func predicateForRequiredPackageProperty(value string) (cache.Predicate, error) if err := json.Unmarshal([]byte(value), &pkg); err != nil { return nil, err } - ver, err := semver.ParseRange(pkg.VersionRange) + return newPackageRequiredPredicate(pkg.PackageName, pkg.VersionRange) +} + +func newPackageRequiredPredicate(name, verRange string) (cache.Predicate, error) { + ver, err := semver.ParseRange(verRange) if err != nil { return nil, err } - return cache.And(cache.PkgPredicate(pkg.PackageName), cache.VersionInRangePredicate(ver, pkg.VersionRange)), nil + return cache.And(cache.PkgPredicate(name), cache.VersionInRangePredicate(ver, verRange)), nil } func predicateForRequiredLabelProperty(value string) (cache.Predicate, error) { diff --git a/pkg/controller/registry/resolver/resolver_test.go b/pkg/controller/registry/resolver/resolver_test.go index 30f633f47bb..13df1ac8686 100644 --- a/pkg/controller/registry/resolver/resolver_test.go +++ b/pkg/controller/registry/resolver/resolver_test.go @@ -3,7 +3,9 @@ package resolver import ( "errors" "fmt" + "math/rand" "testing" + "time" "github.com/blang/semver/v4" "github.com/sirupsen/logrus" @@ -18,13 +20,20 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" listersv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/cache" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/constraints" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/solver" "github.com/operator-framework/operator-registry/pkg/api" opregistry "github.com/operator-framework/operator-registry/pkg/registry" ) +var testGVKKey = opregistry.APIKey{Group: "g", Version: "v", Kind: "k", Plural: "ks"} + +func init() { + rand.Seed(time.Now().UnixNano()) +} + func TestSolveOperators(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{Group: "g", Version: "v", Kind: "k", Plural: "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet const namespace = "test-namespace" @@ -86,7 +95,7 @@ func TestPropertiesAnnotationHonored(t *testing.T) { const ( namespace = "olm" ) - community := cache.SourceKey{"community", namespace} + community := cache.SourceKey{Name: "community", Namespace: namespace} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", nil, nil, nil, nil) csv.Annotations = map[string]string{"operatorframework.io/properties": `{"properties":[{"type":"olm.package","value":{"packageName":"packageA","version":"1.0.0"}}]}`} @@ -116,11 +125,11 @@ func TestPropertiesAnnotationHonored(t *testing.T) { } func TestSolveOperators_MultipleChannels(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil) csvs := []*v1alpha1.ClusterServiceVersion{csv} @@ -153,11 +162,11 @@ func TestSolveOperators_MultipleChannels(t *testing.T) { } func TestSolveOperators_FindLatestVersion(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil) csvs := []*v1alpha1.ClusterServiceVersion{csv} @@ -200,11 +209,11 @@ func TestSolveOperators_FindLatestVersion(t *testing.T) { } func TestSolveOperators_FindLatestVersionWithDependencies(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil) csvs := []*v1alpha1.ClusterServiceVersion{csv} @@ -259,11 +268,11 @@ func TestSolveOperators_FindLatestVersionWithDependencies(t *testing.T) { } func TestSolveOperators_FindLatestVersionWithNestedDependencies(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil) csvs := []*v1alpha1.ClusterServiceVersion{csv} @@ -367,7 +376,7 @@ func TestSolveOperators_CatsrcPrioritySorting(t *testing.T) { } namespace := "olm" - customCatalog := cache.SourceKey{"community", namespace} + customCatalog := cache.SourceKey{Name: "community", Namespace: namespace} newSub := newSub(namespace, "packageA", "alpha", customCatalog) subs := []*v1alpha1.Subscription{newSub} @@ -502,12 +511,12 @@ func TestSolveOperators_CatsrcPrioritySorting(t *testing.T) { } -func TestSolveOperators_WithDependencies(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} +func TestSolveOperators_WithPackageDependencies(t *testing.T) { + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil) csvs := []*v1alpha1.ClusterServiceVersion{csv} @@ -551,11 +560,11 @@ func TestSolveOperators_WithDependencies(t *testing.T) { } func TestSolveOperators_WithGVKDependencies(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - community := cache.SourceKey{"community", namespace} + community := cache.SourceKey{Name: "community", Namespace: namespace} csvs := []*v1alpha1.ClusterServiceVersion{ existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", nil, nil, nil, nil), @@ -601,7 +610,7 @@ func TestSolveOperators_WithGVKDependencies(t *testing.T) { func TestSolveOperators_WithLabelDependencies(t *testing.T) { namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} newSub := newSub(namespace, "packageA", "alpha", catalog) subs := []*v1alpha1.Subscription{newSub} @@ -652,7 +661,7 @@ func TestSolveOperators_WithLabelDependencies(t *testing.T) { func TestSolveOperators_WithUnsatisfiableLabelDependencies(t *testing.T) { namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} newSub := newSub(namespace, "packageA", "alpha", catalog) subs := []*v1alpha1.Subscription{newSub} @@ -681,11 +690,11 @@ func TestSolveOperators_WithUnsatisfiableLabelDependencies(t *testing.T) { } func TestSolveOperators_WithNestedGVKDependencies(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil) csvs := []*v1alpha1.ClusterServiceVersion{csv} @@ -757,10 +766,315 @@ func TestSolveOperators_WithNestedGVKDependencies(t *testing.T) { } } +type operatorGenerator struct { + name, version string + replaces string + pkg, channel, defaultChannel string + catName, catNamespace string + requiredAPIs, providedAPIs cache.APISet + properties []*api.Property + deprecated bool +} + +func (g operatorGenerator) gen() *cache.Entry { + entry := genOperator(g.name, g.version, g.replaces, g.pkg, g.channel, g.catName, g.catNamespace, g.requiredAPIs, g.providedAPIs, nil, g.defaultChannel, g.deprecated) + entry.Properties = append(entry.Properties, g.properties...) + return entry +} + +func genOperatorsRandom(ops ...operatorGenerator) []*cache.Entry { + entries := make([]*cache.Entry, len(ops)) + // Randomize entry order to fuzz input operators over time. + idxs := rand.Perm(len(ops)) + for destIdx, srcIdx := range idxs { + entries[destIdx] = ops[srcIdx].gen() + } + return entries +} + +func TestSolveOperators_OLMConstraint_CompoundAll(t *testing.T) { + namespace := "olm" + csName := "community" + catalog := cache.SourceKey{Name: csName, Namespace: namespace} + + newOperatorGens := []operatorGenerator{{ + name: "bar.v1.0.0", version: "1.0.0", + pkg: "bar", channel: "stable", + catName: csName, catNamespace: namespace, + properties: []*api.Property{{ + Type: constraints.OLMConstraintType, + Value: `{"message": "all constraint", + "all": {"constraints": [ + {"package": {"name": "foo", "versionRange": ">=1.0.0"}}, + {"gvk": {"group": "g1", "version": "v1", "kind": "k1"}}, + {"gvk": {"group": "g2", "version": "v2", "kind": "k2"}} + ]} + }`, + }}, + }} + dependeeOperatorGens := []operatorGenerator{{ + name: "foo.v1.0.1", version: "1.0.1", + pkg: "foo", channel: "stable", replaces: "foo.v1.0.0", + catName: csName, catNamespace: namespace, + providedAPIs: cache.APISet{ + opregistry.APIKey{Group: "g1", Version: "v1", Kind: "k1"}: {}, + opregistry.APIKey{Group: "g2", Version: "v2", Kind: "k2"}: {}, + opregistry.APIKey{Group: "g3", Version: "v3", Kind: "k3"}: {}, + }, + }} + + inputs := append(dependeeOperatorGens, newOperatorGens...) + + satResolver := SatResolver{ + cache: cache.New(cache.StaticSourceProvider{ + catalog: &cache.Snapshot{ + Entries: genOperatorsRandom(append( + inputs, + operatorGenerator{ + name: "foo.v0.99.0", version: "0.99.0", + pkg: "foo", channel: "stable", + catName: csName, catNamespace: namespace, + providedAPIs: cache.APISet{ + opregistry.APIKey{Group: "g1", Version: "v1", Kind: "k1"}: {}, + opregistry.APIKey{Group: "g2", Version: "v2", Kind: "k2"}: {}, + opregistry.APIKey{Group: "g3", Version: "v3", Kind: "k3"}: {}, + }, + }, + operatorGenerator{ + name: "foo.v1.0.0", version: "1.0.0", + pkg: "foo", channel: "stable", replaces: "foo.v0.99.0", + catName: csName, catNamespace: namespace, + providedAPIs: cache.APISet{ + opregistry.APIKey{Group: "g2", Version: "v2", Kind: "k2"}: {}, + opregistry.APIKey{Group: "g3", Version: "v3", Kind: "k3"}: {}, + }, + }, + )...), + }, + }), + log: logrus.New(), + } + + newSub := newSub(namespace, "bar", "stable", catalog) + subs := []*v1alpha1.Subscription{newSub} + + operators, err := satResolver.SolveOperators([]string{namespace}, nil, subs) + require.NoError(t, err) + assert.Equal(t, 2, len(operators)) + + expected := make(cache.OperatorSet, len(inputs)) + for _, gen := range inputs { + op := gen.gen() + expected[op.Name] = op + } + for k := range expected { + if assert.Contains(t, operators, k) { + assert.EqualValues(t, k, operators[k].Name) + } + } +} + +func TestSolveOperators_OLMConstraint_CompoundAny(t *testing.T) { + namespace := "olm" + csName := "community" + catalog := cache.SourceKey{Name: csName, Namespace: namespace} + + newOperatorGens := []operatorGenerator{{ + name: "bar.v1.0.0", version: "1.0.0", + pkg: "bar", channel: "stable", + catName: csName, catNamespace: namespace, + properties: []*api.Property{{ + Type: constraints.OLMConstraintType, + Value: `{"message": "any constraint", + "any": {"constraints": [ + {"gvk": {"group": "g1", "version": "v1", "kind": "k1"}}, + {"gvk": {"group": "g2", "version": "v2", "kind": "k2"}} + ]} + }`, + }}, + }} + dependeeOperatorGens := []operatorGenerator{{ + name: "foo.v1.0.1", version: "1.0.1", + pkg: "foo", channel: "stable", replaces: "foo.v1.0.0", + catName: csName, catNamespace: namespace, + providedAPIs: cache.APISet{ + opregistry.APIKey{Group: "g1", Version: "v1", Kind: "k1"}: {}, + opregistry.APIKey{Group: "g2", Version: "v2", Kind: "k2"}: {}, + opregistry.APIKey{Group: "g3", Version: "v3", Kind: "k3"}: {}, + }, + }} + + inputs := append(dependeeOperatorGens, newOperatorGens...) + + satResolver := SatResolver{ + cache: cache.New(cache.StaticSourceProvider{ + catalog: &cache.Snapshot{ + Entries: genOperatorsRandom(append( + inputs, + operatorGenerator{ + name: "foo.v0.99.0", version: "0.99.0", + pkg: "foo", channel: "stable", + catName: csName, catNamespace: namespace, + providedAPIs: cache.APISet{ + opregistry.APIKey{Group: "g0", Version: "v0", Kind: "k0"}: {}, + }, + }, + operatorGenerator{ + name: "foo.v1.0.0", version: "1.0.0", + pkg: "foo", channel: "stable", replaces: "foo.v0.99.0", + catName: csName, catNamespace: namespace, + providedAPIs: cache.APISet{ + opregistry.APIKey{Group: "g1", Version: "v1", Kind: "k1"}: {}, + opregistry.APIKey{Group: "g2", Version: "v2", Kind: "k2"}: {}, + }, + }, + )...), + }, + }), + log: logrus.New(), + } + + newSub := newSub(namespace, "bar", "stable", catalog) + subs := []*v1alpha1.Subscription{newSub} + + operators, err := satResolver.SolveOperators([]string{namespace}, nil, subs) + require.NoError(t, err) + assert.Equal(t, 2, len(operators)) + + expected := make(cache.OperatorSet, len(inputs)) + for _, gen := range inputs { + op := gen.gen() + expected[op.Name] = op + } + for k := range expected { + if assert.Contains(t, operators, k) { + assert.EqualValues(t, k, operators[k].Name) + } + } +} + +func TestSolveOperators_OLMConstraint_CompoundNone(t *testing.T) { + namespace := "olm" + csName := "community" + catalog := cache.SourceKey{Name: csName, Namespace: namespace} + + newOperatorGens := []operatorGenerator{{ + name: "bar.v1.0.0", version: "1.0.0", + pkg: "bar", channel: "stable", + catName: csName, catNamespace: namespace, + properties: []*api.Property{ + { + Type: constraints.OLMConstraintType, + Value: `{"message": "compound none constraint", + "all": {"constraints": [ + {"gvk": {"group": "g0", "version": "v0", "kind": "k0"}}, + {"none": {"constraints": [ + {"gvk": {"group": "g1", "version": "v1", "kind": "k1"}}, + {"gvk": {"group": "g2", "version": "v2", "kind": "k2"}} + ]}} + ]} + }`, + }, + }, + }} + dependeeOperatorGens := []operatorGenerator{{ + name: "foo.v0.99.0", version: "0.99.0", + pkg: "foo", channel: "stable", + catName: csName, catNamespace: namespace, + providedAPIs: cache.APISet{ + opregistry.APIKey{Group: "g0", Version: "v0", Kind: "k0"}: {}, + }, + }} + + inputs := append(dependeeOperatorGens, newOperatorGens...) + + satResolver := SatResolver{ + cache: cache.New(cache.StaticSourceProvider{ + catalog: &cache.Snapshot{ + Entries: genOperatorsRandom(append( + inputs, + operatorGenerator{ + name: "foo.v1.0.0", version: "1.0.0", + pkg: "foo", channel: "stable", replaces: "foo.v0.99.0", + catName: csName, catNamespace: namespace, + providedAPIs: cache.APISet{ + opregistry.APIKey{Group: "g0", Version: "v0", Kind: "k0"}: {}, + opregistry.APIKey{Group: "g1", Version: "v1", Kind: "k1"}: {}, + opregistry.APIKey{Group: "g2", Version: "v2", Kind: "k2"}: {}, + }, + }, + operatorGenerator{ + name: "foo.v1.0.1", version: "1.0.1", + pkg: "foo", channel: "stable", replaces: "foo.v1.0.0", + catName: csName, catNamespace: namespace, + providedAPIs: cache.APISet{ + opregistry.APIKey{Group: "g0", Version: "v0", Kind: "k0"}: {}, + opregistry.APIKey{Group: "g1", Version: "v1", Kind: "k1"}: {}, + opregistry.APIKey{Group: "g2", Version: "v2", Kind: "k2"}: {}, + opregistry.APIKey{Group: "g3", Version: "v3", Kind: "k3"}: {}, + }, + }, + )...), + }, + }), + log: logrus.New(), + } + + newSub := newSub(namespace, "bar", "stable", catalog) + subs := []*v1alpha1.Subscription{newSub} + + operators, err := satResolver.SolveOperators([]string{namespace}, nil, subs) + require.NoError(t, err) + assert.Equal(t, 2, len(operators)) + + expected := make(cache.OperatorSet, len(inputs)) + for _, gen := range inputs { + op := gen.gen() + expected[op.Name] = op + } + for k := range expected { + if assert.Contains(t, operators, k) { + assert.EqualValues(t, k, operators[k].Name) + } + } +} + +func TestSolveOperators_OLMConstraint_Unknown(t *testing.T) { + namespace := "olm" + csName := "community" + catalog := cache.SourceKey{Name: csName, Namespace: namespace} + + newOperatorGens := []operatorGenerator{{ + name: "bar.v1.0.0", version: "1.0.0", + pkg: "bar", channel: "stable", + catName: csName, catNamespace: namespace, + properties: []*api.Property{{ + Type: constraints.OLMConstraintType, + Value: `{"message": "unknown constraint", "unknown": {"foo": "bar"}}`, + }}, + }} + + satResolver := SatResolver{ + cache: cache.New(cache.StaticSourceProvider{ + catalog: &cache.Snapshot{ + Entries: genOperatorsRandom(newOperatorGens...), + }, + }), + log: logrus.New(), + } + + newSub := newSub(namespace, "bar", "stable", catalog) + subs := []*v1alpha1.Subscription{newSub} + + _, err := satResolver.SolveOperators([]string{namespace}, nil, subs) + require.Error(t, err) + require.Contains(t, err.Error(), `json: unknown field "unknown"`) +} + func TestSolveOperators_IgnoreUnsatisfiableDependencies(t *testing.T) { const namespace = "olm" - Provides := cache.APISet{opregistry.APIKey{Group: "g", Version: "v", Kind: "k", Plural: "ks"}: struct{}{}} + Provides := cache.APISet{testGVKKey: struct{}{}} community := cache.SourceKey{Name: "community", Namespace: namespace} csvs := []*v1alpha1.ClusterServiceVersion{ existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil), @@ -819,13 +1133,13 @@ func TestSolveOperators_IgnoreUnsatisfiableDependencies(t *testing.T) { // Behavior: The resolver should prefer catalogs in the same namespace as the subscription. // It should also prefer the same catalog over global catalogs in terms of the operator cache. func TestSolveOperators_PreferCatalogInSameNamespace(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" altNamespace := "alt-olm" - catalog := cache.SourceKey{"community", namespace} - altnsCatalog := cache.SourceKey{"alt-community", altNamespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} + altnsCatalog := cache.SourceKey{Name: "alt-community", Namespace: altNamespace} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil) csvs := []*v1alpha1.ClusterServiceVersion{csv} @@ -860,11 +1174,11 @@ func TestSolveOperators_PreferCatalogInSameNamespace(t *testing.T) { // Behavior: The resolver should not look in catalogs not in the same namespace or the global catalog namespace when resolving the subscription. // This test should not result in a successful resolution because the catalog fulfilling the subscription is not in the operator cache. func TestSolveOperators_ResolveOnlyInCachedNamespaces(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} otherCatalog := cache.SourceKey{Name: "secret", Namespace: "secret"} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil) @@ -891,7 +1205,7 @@ func TestSolveOperators_ResolveOnlyInCachedNamespaces(t *testing.T) { // Behavior: the resolver should always prefer the default channel for the subscribed bundle (unless we implement ordering for channels) func TestSolveOperators_PreferDefaultChannelInResolution(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" @@ -929,7 +1243,7 @@ func TestSolveOperators_PreferDefaultChannelInResolution(t *testing.T) { // Behavior: the resolver should always prefer the default channel for bundles satisfying transitive dependencies func TestSolveOperators_PreferDefaultChannelInResolutionForTransitiveDependencies(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" @@ -967,11 +1281,11 @@ func TestSolveOperators_PreferDefaultChannelInResolutionForTransitiveDependencie } func TestSolveOperators_SubscriptionlessOperatorsSatisfyDependencies(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil) csvs := []*v1alpha1.ClusterServiceVersion{csv} @@ -1010,11 +1324,11 @@ func TestSolveOperators_SubscriptionlessOperatorsSatisfyDependencies(t *testing. } func TestSolveOperators_SubscriptionlessOperatorsCanConflict(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} csv := existingOperator(namespace, "packageA.v1", "packageA", "alpha", "", Provides, nil, nil, nil) csvs := []*v1alpha1.ClusterServiceVersion{csv} @@ -1038,10 +1352,10 @@ func TestSolveOperators_SubscriptionlessOperatorsCanConflict(t *testing.T) { } func TestSolveOperators_PackageCannotSelfSatisfy(t *testing.T) { - Provides1 := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} - Requires1 := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} - Provides2 := cache.APISet{opregistry.APIKey{"g2", "v", "k", "ks"}: struct{}{}} - Requires2 := cache.APISet{opregistry.APIKey{"g2", "v", "k", "ks"}: struct{}{}} + Provides1 := cache.APISet{testGVKKey: struct{}{}} + Requires1 := cache.APISet{testGVKKey: struct{}{}} + Provides2 := cache.APISet{opregistry.APIKey{Group: "g2", Version: "v", Kind: "k", Plural: "ks"}: struct{}{}} + Requires2 := cache.APISet{opregistry.APIKey{Group: "g2", Version: "v", Kind: "k", Plural: "ks"}: struct{}{}} ProvidesBoth := Provides1.Union(Provides2) RequiresBoth := Requires1.Union(Requires2) @@ -1090,9 +1404,9 @@ func TestSolveOperators_PackageCannotSelfSatisfy(t *testing.T) { } func TestSolveOperators_TransferApiOwnership(t *testing.T) { - Provides1 := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} - Requires1 := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} - Provides2 := cache.APISet{opregistry.APIKey{"g2", "v", "k", "ks"}: struct{}{}} + Provides1 := cache.APISet{testGVKKey: struct{}{}} + Requires1 := cache.APISet{testGVKKey: struct{}{}} + Provides2 := cache.APISet{opregistry.APIKey{Group: "g2", Version: "v", Kind: "k", Plural: "ks"}: struct{}{}} ProvidesBoth := Provides1.Union(Provides2) namespace := "olm" @@ -1271,11 +1585,11 @@ func TestSolveOperatorsWithDeprecatedInnerChannelEntry(t *testing.T) { } func TestSolveOperators_WithSkipsAndStartingCSV(t *testing.T) { - APISet := cache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet := cache.APISet{testGVKKey: struct{}{}} Provides := APISet namespace := "olm" - catalog := cache.SourceKey{"community", namespace} + catalog := cache.SourceKey{Name: "community", Namespace: namespace} newSub := newSub(namespace, "packageB", "alpha", catalog, withStartingCSV("packageB.v1")) subs := []*v1alpha1.Subscription{newSub} @@ -1353,7 +1667,7 @@ func TestSolveOperators_WithSkips(t *testing.T) { func TestSolveOperatorsWithSkipsPreventingSelection(t *testing.T) { const namespace = "test-namespace" catalog := cache.SourceKey{Name: "test-catalog", Namespace: namespace} - gvks := cache.APISet{opregistry.APIKey{Group: "g", Version: "v", Kind: "k", Plural: "ks"}: struct{}{}} + gvks := cache.APISet{testGVKKey: struct{}{}} // Subscription candidate a-1 requires a GVK provided // exclusively by b-1, but b-1 is skipped by b-3 and can't be diff --git a/pkg/controller/registry/resolver/step_resolver_test.go b/pkg/controller/registry/resolver/step_resolver_test.go index f85056192f0..6c29c127ea3 100644 --- a/pkg/controller/registry/resolver/step_resolver_test.go +++ b/pkg/controller/registry/resolver/step_resolver_test.go @@ -33,16 +33,16 @@ var ( // conventions for tests: packages are letters (a,b,c) and apis are numbers (1,2,3) // APISets used for tests - APISet1 = resolvercache.APISet{opregistry.APIKey{"g", "v", "k", "ks"}: struct{}{}} + APISet1 = resolvercache.APISet{testGVKKey: struct{}{}} Provides1 = APISet1 Requires1 = APISet1 - APISet2 = resolvercache.APISet{opregistry.APIKey{"g2", "v2", "k2", "k2s"}: struct{}{}} + APISet2 = resolvercache.APISet{opregistry.APIKey{Group: "g2", Version: "v2", Kind: "k2", Plural: "k2s"}: struct{}{}} Provides2 = APISet2 Requires2 = APISet2 - APISet3 = resolvercache.APISet{opregistry.APIKey{"g3", "v3", "k3", "k3s"}: struct{}{}} + APISet3 = resolvercache.APISet{opregistry.APIKey{Group: "g3", Version: "v3", Kind: "k3", Plural: "k3s"}: struct{}{}} Provides3 = APISet3 Requires3 = APISet3 - APISet4 = resolvercache.APISet{opregistry.APIKey{"g4", "v4", "k4", "k4s"}: struct{}{}} + APISet4 = resolvercache.APISet{opregistry.APIKey{Group: "g4", Version: "v4", Kind: "k4", Plural: "k4s"}: struct{}{}} Provides4 = APISet4 Requires4 = APISet4 ) @@ -886,7 +886,7 @@ func (stub *stubOperatorCacheProvider) Expire(key resolvercache.SourceKey) { func TestNamespaceResolverRBAC(t *testing.T) { namespace := "catsrc-namespace" - catalog := resolvercache.SourceKey{"catsrc", namespace} + catalog := resolvercache.SourceKey{Name: "catsrc", Namespace: namespace} simplePermissions := []v1alpha1.StrategyDeploymentPermissions{ {