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..6c5453d60ab --- /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", 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..b5dfa338581 --- /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 TestParseConstraints(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..efd1e54ddd8 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,12 @@ func sortChannel(bundles []*cache.Entry) ([]*cache.Entry, error) { return chains[0], nil } -func DependencyPredicates(properties []*api.Property) ([]cache.Predicate, error) { +type predicateConverter struct{} + +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 +753,73 @@ 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] + + if property.Type == constraints.OLMConstraintType { + return pc.predicateForConstraintProperty(property.Value) + } + + // Legacy behavior dictates that unknown properties are ignored. + 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, err + } + + preds, err := pc.convertConstraints(constraint) + if err != nil { + return nil, err + } + return preds[0], nil +} + +func (pc *predicateConverter) convertConstraints(constraints ...constraints.Constraint) (preds []cache.Predicate, err error) { + + for _, constraint := range constraints { + + var sub cache.Predicate + switch { + case constraint.GVK != nil: + sub = cache.ProvidingAPIPredicate(opregistry.APIKey{ + Group: constraint.GVK.Group, + Version: constraint.GVK.Version, + Kind: constraint.GVK.Kind, + }) + case constraint.Package != nil: + sub, err = newPackageRequiredPredicate(constraint.Package.Name, constraint.Package.VersionRange) + case constraint.All != nil: + subs, perr := pc.convertConstraints(constraint.All.Constraints...) + sub, err = cache.And(subs...), perr + case constraint.Any != nil: + subs, perr := pc.convertConstraints(constraint.Any.Constraints...) + sub, err = cache.Or(subs...), perr + case constraint.None != nil: + subs, perr := pc.convertConstraints(constraint.None.Constraints...) + sub, err = cache.Not(subs...), perr + default: + return nil, fmt.Errorf("unknown predicate type") + } + if err != nil { + return nil, err + } + + preds = append(preds, sub) + } + + 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 +849,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) {