Skip to content

Commit

Permalink
feat(resolver): resolve compound dep constraints from bundle properties
Browse files Browse the repository at this point in the history
Signed-off-by: Eric Stroczynski <[email protected]>
  • Loading branch information
estroz committed Dec 3, 2021
1 parent d92ef2c commit 693d013
Show file tree
Hide file tree
Showing 4 changed files with 441 additions and 9 deletions.
27 changes: 26 additions & 1 deletion pkg/controller/registry/resolver/cache/predicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"

"github.com/blang/semver/v4"

opregistry "github.com/operator-framework/operator-registry/pkg/registry"
)

Expand Down Expand Up @@ -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
}
Expand Down
69 changes: 69 additions & 0 deletions pkg/controller/registry/resolver/constraints/constraint.go
Original file line number Diff line number Diff line change
@@ -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
}
274 changes: 274 additions & 0 deletions pkg/controller/registry/resolver/constraints/constraint_test.go
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
}
]
}}
`
)
Loading

0 comments on commit 693d013

Please sign in to comment.