Skip to content

Commit

Permalink
feat(constraints): add compound constraints and olm.constraint value …
Browse files Browse the repository at this point in the history
…parser (#203)

* feat(constraints): add compound constraints and olm.constraint value parser

Signed-off-by: Eric Stroczynski <ericstroczynski@gmail.com>

* renaming and add yaml tags

Signed-off-by: Eric Stroczynski <ericstroczynski@gmail.com>
  • Loading branch information
Eric Stroczynski authored Dec 10, 2021
1 parent 6897e9a commit 40cb9fd
Show file tree
Hide file tree
Showing 3 changed files with 360 additions and 11 deletions.
11 changes: 0 additions & 11 deletions pkg/constraints/cel.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,6 @@ import (
// PropertiesKey is the key for bundle properties map (input data for CEL evaluation)
const PropertiesKey = "properties"

// Constraint is a struct representing the new generic constraint type
type Constraint struct {
// Constraint message that surfaces in resolution
// This field is optional
Message string `json:"message" yaml:"message"`

// The cel struct that contraints CEL expression
// This field is required
Cel *Cel `json:"cel" yaml:"cel"`
}

// Cel is a struct representing CEL expression information
type Cel struct {
// The CEL expression
Expand Down
86 changes: 86 additions & 0 deletions pkg/constraints/constraint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package constraints

import (
"bytes"
"encoding/json"
"fmt"
)

// OLMConstraintType is the schema "type" key for all constraints known to OLM
// (except for legacy types).
const OLMConstraintType = "olm.constraint"

// Constraint holds parsed, potentially nested dependency constraints.
type Constraint struct {
// Constraint message that surfaces in resolution
// This field is optional
Message string `json:"message,omitempty" yaml:"message,omitempty"`

// The cel struct that contraints CEL expression
Cel *Cel `json:"cel,omitempty" yaml:"cel,omitempty"`

// Package defines a constraint for a package within a version range.
Package *PackageConstraint `json:"package,omitempty" yaml:"package,omitempty"`

// GVK defines a constraint for a GVK.
GVK *GVKConstraint `json:"gvk,omitempty" yaml:"gvk,omitempty"`

// All, Any, and None are compound constraints. See this enhancement for details:
// https://github.com/operator-framework/enhancements/blob/master/enhancements/compound-bundle-constraints.md
All *CompoundConstraint `json:"all,omitempty" yaml:"all,omitempty"`
Any *CompoundConstraint `json:"any,omitempty" yaml:"any,omitempty"`
// A note on None: this constraint is not particularly useful by itself.
// It should be used within an All constraint alongside some other constraint type
// since saying "none of these GVKs/packages/etc." without an alternative doesn't make sense.
None *CompoundConstraint `json:"none,omitempty" yaml:"none,omitempty"`
}

// CompoundConstraint holds a list of potentially nested constraints
// over which a boolean operation is applied.
type CompoundConstraint struct {
Constraints []Constraint `json:"constraints" yaml:"constraints"`
}

// GVKConstraint defines a GVK constraint.
type GVKConstraint struct {
Group string `json:"group" yaml:"group"`
Kind string `json:"kind" yaml:"kind"`
Version string `json:"version" yaml:"version"`
}

// PackageConstraint defines a package constraint.
type PackageConstraint struct {
// PackageName is the name of the package.
PackageName string `json:"packageName" yaml:"packageName"`
// VersionRange required for the package.
VersionRange string `json:"versionRange" yaml:"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.
// QUESTION: make this configurable?
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. Constraints that exceed the number of bytes
// defined by maxConstraintSize result 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/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 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{PackageName: "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{PackageName: "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{PackageName: "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{PackageName: "fuz", VersionRange: ">=1.0.0"},
},
},
},
},
},
{
name: "Valid/Complex",
input: json.RawMessage(inputComplex),
expConstraint: Constraint{
Message: "blah",
All: &CompoundConstraint{
Constraints: []Constraint{
{Package: &PackageConstraint{PackageName: "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{PackageName: "foo", VersionRange: ">=1.0.0"}},
{Package: &PackageConstraint{PackageName: "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": {
"packageName": "foo",
"versionRange": ">=1.0.0"
}
}`

inputBasicCompoundTmpl = `{
"message": "blah",
"%s": {
"constraints": [
{
"message": "blah blah",
"package": {
"packageName": "fuz",
"versionRange": ">=1.0.0"
}
}
]
}}
`

inputComplex = `{
"message": "blah",
"all": {
"constraints": [
{
"package": {
"packageName": "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": {
"packageName": "foo",
"versionRange": ">=1.0.0"
}
},
{
"package": {
"packageName": "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"
}
}
]
}
}
]
}}
`
)

0 comments on commit 40cb9fd

Please sign in to comment.