-
Notifications
You must be signed in to change notification settings - Fork 545
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(resolver): resolve compound dep constraints from bundle properties
Signed-off-by: Eric Stroczynski <[email protected]>
- Loading branch information
Showing
4 changed files
with
441 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
pkg/controller/registry/resolver/constraints/constraint.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
274
pkg/controller/registry/resolver/constraints/constraint_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
] | ||
} | ||
} | ||
] | ||
}} | ||
` | ||
) |
Oops, something went wrong.