Skip to content

Commit 693d013

Browse files
committed
feat(resolver): resolve compound dep constraints from bundle properties
Signed-off-by: Eric Stroczynski <[email protected]>
1 parent d92ef2c commit 693d013

File tree

4 files changed

+441
-9
lines changed

4 files changed

+441
-9
lines changed

Diff for: pkg/controller/registry/resolver/cache/predicates.go

+26-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77

88
"github.com/blang/semver/v4"
9-
109
opregistry "github.com/operator-framework/operator-registry/pkg/registry"
1110
)
1211

@@ -282,6 +281,32 @@ func (p orPredicate) String() string {
282281
return b.String()
283282
}
284283

284+
func Not(p ...Predicate) Predicate {
285+
return notPredicate{
286+
predicates: p,
287+
}
288+
}
289+
290+
type notPredicate struct {
291+
predicates []Predicate
292+
}
293+
294+
func (p notPredicate) Test(o *Entry) bool {
295+
// !pred && !pred is equivalent to !(pred || pred).
296+
return !orPredicate{p.predicates}.Test(o)
297+
}
298+
299+
func (p notPredicate) String() string {
300+
var b bytes.Buffer
301+
for i, predicate := range p.predicates {
302+
b.WriteString(predicate.String())
303+
if i != len(p.predicates)-1 {
304+
b.WriteString(" and not ")
305+
}
306+
}
307+
return b.String()
308+
}
309+
285310
type booleanPredicate struct {
286311
result bool
287312
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package constraints
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
)
8+
9+
const OLMConstraintType = "olm.constraint"
10+
11+
// Constraint holds parsed, potentially nested dependency constraints.
12+
type Constraint struct {
13+
Message string `json:"message"`
14+
15+
Package *PackageConstraint `json:"package,omitempty"`
16+
GVK *GVKConstraint `json:"gvk,omitempty"`
17+
18+
All *CompoundConstraint `json:"all,omitempty"`
19+
Any *CompoundConstraint `json:"any,omitempty"`
20+
None *CompoundConstraint `json:"none,omitempty"`
21+
}
22+
23+
// CompoundConstraint holds a list of potentially nested constraints
24+
// over which a boolean operation is applied.
25+
type CompoundConstraint struct {
26+
Constraints []Constraint `json:"constraints"`
27+
}
28+
29+
// GVKConstraint defines a GVK constraint.
30+
type GVKConstraint struct {
31+
Group string `json:"group"`
32+
Kind string `json:"kind"`
33+
Version string `json:"version"`
34+
}
35+
36+
// PackageConstraint defines a package constraint.
37+
type PackageConstraint struct {
38+
// Name of the package.
39+
Name string `json:"name"`
40+
// VersionRange required for the package.
41+
VersionRange string `json:"versionRange"`
42+
}
43+
44+
// maxConstraintSize defines the maximum raw size in bytes of an olm.constraint.
45+
// 64Kb seems reasonable, since this number allows for long description strings
46+
// and either few deep nestings or shallow nestings and long constraints lists,
47+
// but not both.
48+
const maxConstraintSize = 2 << 16
49+
50+
// ErrMaxConstraintSizeExceeded is returned when a constraint's size > maxConstraintSize.
51+
var ErrMaxConstraintSizeExceeded = fmt.Errorf("olm.constraint value is greater than max constraint size %d", maxConstraintSize)
52+
53+
// Parse parses an olm.constraint property's value recursively into a Constraint.
54+
// Unknown value schemas result in an error. A too-large value results in an error.
55+
func Parse(v json.RawMessage) (c Constraint, err error) {
56+
// There is no way to explicitly limit nesting depth.
57+
// From https://github.com/golang/go/issues/31789#issuecomment-538134396,
58+
// the recommended approach is to error out if raw input size
59+
// is greater than some threshold.
60+
if len(v) > maxConstraintSize {
61+
return c, ErrMaxConstraintSizeExceeded
62+
}
63+
64+
d := json.NewDecoder(bytes.NewBuffer(v))
65+
d.DisallowUnknownFields()
66+
err = d.Decode(&c)
67+
68+
return
69+
}
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 TestParseConstraints(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{Name: "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{Name: "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{Name: "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{Name: "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{Name: "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{Name: "foo", VersionRange: ">=1.0.0"}},
96+
{Package: &PackageConstraint{Name: "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+
"name": "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+
"name": "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+
"name": "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+
"name": "foo",
211+
"versionRange": ">=1.0.0"
212+
}
213+
},
214+
{
215+
"package": {
216+
"name": "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)