Skip to content

Commit 40cb9fd

Browse files
author
Eric Stroczynski
authored
feat(constraints): add compound constraints and olm.constraint value parser (#203)
* feat(constraints): add compound constraints and olm.constraint value parser Signed-off-by: Eric Stroczynski <[email protected]> * renaming and add yaml tags Signed-off-by: Eric Stroczynski <[email protected]>
1 parent 6897e9a commit 40cb9fd

File tree

3 files changed

+360
-11
lines changed

3 files changed

+360
-11
lines changed

pkg/constraints/cel.go

-11
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,6 @@ import (
1616
// PropertiesKey is the key for bundle properties map (input data for CEL evaluation)
1717
const PropertiesKey = "properties"
1818

19-
// Constraint is a struct representing the new generic constraint type
20-
type Constraint struct {
21-
// Constraint message that surfaces in resolution
22-
// This field is optional
23-
Message string `json:"message" yaml:"message"`
24-
25-
// The cel struct that contraints CEL expression
26-
// This field is required
27-
Cel *Cel `json:"cel" yaml:"cel"`
28-
}
29-
3019
// Cel is a struct representing CEL expression information
3120
type Cel struct {
3221
// The CEL expression

pkg/constraints/constraint.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package constraints
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
)
8+
9+
// OLMConstraintType is the schema "type" key for all constraints known to OLM
10+
// (except for legacy types).
11+
const OLMConstraintType = "olm.constraint"
12+
13+
// Constraint holds parsed, potentially nested dependency constraints.
14+
type Constraint struct {
15+
// Constraint message that surfaces in resolution
16+
// This field is optional
17+
Message string `json:"message,omitempty" yaml:"message,omitempty"`
18+
19+
// The cel struct that contraints CEL expression
20+
Cel *Cel `json:"cel,omitempty" yaml:"cel,omitempty"`
21+
22+
// Package defines a constraint for a package within a version range.
23+
Package *PackageConstraint `json:"package,omitempty" yaml:"package,omitempty"`
24+
25+
// GVK defines a constraint for a GVK.
26+
GVK *GVKConstraint `json:"gvk,omitempty" yaml:"gvk,omitempty"`
27+
28+
// All, Any, and None are compound constraints. See this enhancement for details:
29+
// https://github.com/operator-framework/enhancements/blob/master/enhancements/compound-bundle-constraints.md
30+
All *CompoundConstraint `json:"all,omitempty" yaml:"all,omitempty"`
31+
Any *CompoundConstraint `json:"any,omitempty" yaml:"any,omitempty"`
32+
// A note on None: this constraint is not particularly useful by itself.
33+
// It should be used within an All constraint alongside some other constraint type
34+
// since saying "none of these GVKs/packages/etc." without an alternative doesn't make sense.
35+
None *CompoundConstraint `json:"none,omitempty" yaml:"none,omitempty"`
36+
}
37+
38+
// CompoundConstraint holds a list of potentially nested constraints
39+
// over which a boolean operation is applied.
40+
type CompoundConstraint struct {
41+
Constraints []Constraint `json:"constraints" yaml:"constraints"`
42+
}
43+
44+
// GVKConstraint defines a GVK constraint.
45+
type GVKConstraint struct {
46+
Group string `json:"group" yaml:"group"`
47+
Kind string `json:"kind" yaml:"kind"`
48+
Version string `json:"version" yaml:"version"`
49+
}
50+
51+
// PackageConstraint defines a package constraint.
52+
type PackageConstraint struct {
53+
// PackageName is the name of the package.
54+
PackageName string `json:"packageName" yaml:"packageName"`
55+
// VersionRange required for the package.
56+
VersionRange string `json:"versionRange" yaml:"versionRange"`
57+
}
58+
59+
// maxConstraintSize defines the maximum raw size in bytes of an olm.constraint.
60+
// 64Kb seems reasonable, since this number allows for long description strings
61+
// and either few deep nestings or shallow nestings and long constraints lists,
62+
// but not both.
63+
// QUESTION: make this configurable?
64+
const maxConstraintSize = 2 << 16
65+
66+
// ErrMaxConstraintSizeExceeded is returned when a constraint's size > maxConstraintSize.
67+
var ErrMaxConstraintSizeExceeded = fmt.Errorf("olm.constraint value is greater than max constraint size %d bytes", maxConstraintSize)
68+
69+
// Parse parses an olm.constraint property's value recursively into a Constraint.
70+
// Unknown value schemas result in an error. Constraints that exceed the number of bytes
71+
// defined by maxConstraintSize result results in an error.
72+
func Parse(v json.RawMessage) (c Constraint, err error) {
73+
// There is no way to explicitly limit nesting depth.
74+
// From https://github.com/golang/go/issues/31789#issuecomment-538134396,
75+
// the recommended approach is to error out if raw input size
76+
// is greater than some threshold.
77+
if len(v) > maxConstraintSize {
78+
return c, ErrMaxConstraintSizeExceeded
79+
}
80+
81+
d := json.NewDecoder(bytes.NewBuffer(v))
82+
d.DisallowUnknownFields()
83+
err = d.Decode(&c)
84+
85+
return
86+
}

pkg/constraints/constraint_test.go

+274
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
package constraints
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"math/rand"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestParse(t *testing.T) {
13+
type spec struct {
14+
name string
15+
input json.RawMessage
16+
expConstraint Constraint
17+
expError string
18+
}
19+
20+
specs := []spec{
21+
{
22+
name: "Valid/BasicGVK",
23+
input: json.RawMessage(inputBasicGVK),
24+
expConstraint: Constraint{
25+
Message: "blah",
26+
GVK: &GVKConstraint{Group: "example.com", Version: "v1", Kind: "Foo"},
27+
},
28+
},
29+
{
30+
name: "Valid/BasicPackage",
31+
input: json.RawMessage(inputBasicPackage),
32+
expConstraint: Constraint{
33+
Message: "blah",
34+
Package: &PackageConstraint{PackageName: "foo", VersionRange: ">=1.0.0"},
35+
},
36+
},
37+
{
38+
name: "Valid/BasicAll",
39+
input: json.RawMessage(fmt.Sprintf(inputBasicCompoundTmpl, "all")),
40+
expConstraint: Constraint{
41+
Message: "blah",
42+
All: &CompoundConstraint{
43+
Constraints: []Constraint{
44+
{
45+
Message: "blah blah",
46+
Package: &PackageConstraint{PackageName: "fuz", VersionRange: ">=1.0.0"},
47+
},
48+
},
49+
},
50+
},
51+
},
52+
{
53+
name: "Valid/BasicAny",
54+
input: json.RawMessage(fmt.Sprintf(inputBasicCompoundTmpl, "any")),
55+
expConstraint: Constraint{
56+
Message: "blah",
57+
Any: &CompoundConstraint{
58+
Constraints: []Constraint{
59+
{
60+
Message: "blah blah",
61+
Package: &PackageConstraint{PackageName: "fuz", VersionRange: ">=1.0.0"},
62+
},
63+
},
64+
},
65+
},
66+
},
67+
{
68+
name: "Valid/BasicNone",
69+
input: json.RawMessage(fmt.Sprintf(inputBasicCompoundTmpl, "none")),
70+
expConstraint: Constraint{
71+
Message: "blah",
72+
None: &CompoundConstraint{
73+
Constraints: []Constraint{
74+
{
75+
Message: "blah blah",
76+
Package: &PackageConstraint{PackageName: "fuz", VersionRange: ">=1.0.0"},
77+
},
78+
},
79+
},
80+
},
81+
},
82+
{
83+
name: "Valid/Complex",
84+
input: json.RawMessage(inputComplex),
85+
expConstraint: Constraint{
86+
Message: "blah",
87+
All: &CompoundConstraint{
88+
Constraints: []Constraint{
89+
{Package: &PackageConstraint{PackageName: "fuz", VersionRange: ">=1.0.0"}},
90+
{GVK: &GVKConstraint{Group: "fals.example.com", Kind: "Fal", Version: "v1"}},
91+
{
92+
Message: "foo and buf must be stable versions",
93+
All: &CompoundConstraint{
94+
Constraints: []Constraint{
95+
{Package: &PackageConstraint{PackageName: "foo", VersionRange: ">=1.0.0"}},
96+
{Package: &PackageConstraint{PackageName: "buf", VersionRange: ">=1.0.0"}},
97+
{GVK: &GVKConstraint{Group: "foos.example.com", Kind: "Foo", Version: "v1"}},
98+
},
99+
},
100+
},
101+
{
102+
Message: "blah blah",
103+
Any: &CompoundConstraint{
104+
Constraints: []Constraint{
105+
{GVK: &GVKConstraint{Group: "foos.example.com", Kind: "Foo", Version: "v1beta1"}},
106+
{GVK: &GVKConstraint{Group: "foos.example.com", Kind: "Foo", Version: "v1beta2"}},
107+
{GVK: &GVKConstraint{Group: "foos.example.com", Kind: "Foo", Version: "v1"}},
108+
},
109+
},
110+
},
111+
{
112+
None: &CompoundConstraint{
113+
Constraints: []Constraint{
114+
{GVK: &GVKConstraint{Group: "bazs.example.com", Kind: "Baz", Version: "v1alpha1"}},
115+
},
116+
},
117+
},
118+
},
119+
},
120+
},
121+
},
122+
{
123+
name: "Invalid/TooLarge",
124+
input: func(t *testing.T) json.RawMessage {
125+
p := make([]byte, maxConstraintSize+1)
126+
_, err := rand.Read(p)
127+
require.NoError(t, err)
128+
return json.RawMessage(p)
129+
}(t),
130+
expError: ErrMaxConstraintSizeExceeded.Error(),
131+
},
132+
{
133+
name: "Invalid/UnknownField",
134+
input: json.RawMessage(
135+
`{"message": "something", "arbitrary": {"key": "value"}}`,
136+
),
137+
expError: `json: unknown field "arbitrary"`,
138+
},
139+
}
140+
141+
for _, s := range specs {
142+
t.Run(s.name, func(t *testing.T) {
143+
constraint, err := Parse(s.input)
144+
if s.expError == "" {
145+
require.NoError(t, err)
146+
require.Equal(t, s.expConstraint, constraint)
147+
} else {
148+
require.EqualError(t, err, s.expError)
149+
}
150+
})
151+
}
152+
}
153+
154+
const (
155+
inputBasicGVK = `{
156+
"message": "blah",
157+
"gvk": {
158+
"group": "example.com",
159+
"version": "v1",
160+
"kind": "Foo"
161+
}
162+
}`
163+
164+
inputBasicPackage = `{
165+
"message": "blah",
166+
"package": {
167+
"packageName": "foo",
168+
"versionRange": ">=1.0.0"
169+
}
170+
}`
171+
172+
inputBasicCompoundTmpl = `{
173+
"message": "blah",
174+
"%s": {
175+
"constraints": [
176+
{
177+
"message": "blah blah",
178+
"package": {
179+
"packageName": "fuz",
180+
"versionRange": ">=1.0.0"
181+
}
182+
}
183+
]
184+
}}
185+
`
186+
187+
inputComplex = `{
188+
"message": "blah",
189+
"all": {
190+
"constraints": [
191+
{
192+
"package": {
193+
"packageName": "fuz",
194+
"versionRange": ">=1.0.0"
195+
}
196+
},
197+
{
198+
"gvk": {
199+
"group": "fals.example.com",
200+
"version": "v1",
201+
"kind": "Fal"
202+
}
203+
},
204+
{
205+
"message": "foo and buf must be stable versions",
206+
"all": {
207+
"constraints": [
208+
{
209+
"package": {
210+
"packageName": "foo",
211+
"versionRange": ">=1.0.0"
212+
}
213+
},
214+
{
215+
"package": {
216+
"packageName": "buf",
217+
"versionRange": ">=1.0.0"
218+
}
219+
},
220+
{
221+
"gvk": {
222+
"group": "foos.example.com",
223+
"version": "v1",
224+
"kind": "Foo"
225+
}
226+
}
227+
]
228+
}
229+
},
230+
{
231+
"message": "blah blah",
232+
"any": {
233+
"constraints": [
234+
{
235+
"gvk": {
236+
"group": "foos.example.com",
237+
"version": "v1beta1",
238+
"kind": "Foo"
239+
}
240+
},
241+
{
242+
"gvk": {
243+
"group": "foos.example.com",
244+
"version": "v1beta2",
245+
"kind": "Foo"
246+
}
247+
},
248+
{
249+
"gvk": {
250+
"group": "foos.example.com",
251+
"version": "v1",
252+
"kind": "Foo"
253+
}
254+
}
255+
]
256+
}
257+
},
258+
{
259+
"none": {
260+
"constraints": [
261+
{
262+
"gvk": {
263+
"group": "bazs.example.com",
264+
"version": "v1alpha1",
265+
"kind": "Baz"
266+
}
267+
}
268+
]
269+
}
270+
}
271+
]
272+
}}
273+
`
274+
)

0 commit comments

Comments
 (0)