-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Constraints satisfication checks #674
Conversation
} | ||
return dag.GetResource(b.Node) == nil | ||
case ReplaceConstraintOperator: | ||
// We should entail edges are copied from the original source to the new replacement node in the dag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// We should entail edges are copied from the original source to the new replacement node in the dag | |
// We should ensure edges are copied from the original source to the new replacement node in the dag |
Is this what you meant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
likely a GH autocomplete comment, will fix
return dag.GetResource(b.Node) == nil | ||
case ReplaceConstraintOperator: | ||
// We should entail edges are copied from the original source to the new replacement node in the dag | ||
// Ignoring for now, but will be an optimization we can make |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is? Is it the line above (ensuring edges get copied)? If so, is that really an optimization?
} | ||
) | ||
|
||
func (b *ApplicationConstraint) Scope() ConstraintScope { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
out of curiosity, why is the receiver var name b
? (It confused me a bit as I was reading below — I forgot that's what b
was, and got a bit confused.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
likely GH just auto completing to b, ill rename them
} | ||
|
||
func (b *ApplicationConstraint) IsSatisfied(dag *core.ResourceGraph) bool { | ||
switch b.Operator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically hand-rolled polymorphism. Any reason not to just use different structs?
For example, ReplacementNode
only gets used in one of these cases; that suggests to me that the other cases shouldn't even have that field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was opting for flat objects which can have validation to ensure that they are correctly filled in, but the other option would be to have many different constraint types based on scope + operator
} | ||
|
||
func (b *ApplicationConstraint) Conflict(other Constraint) bool { | ||
return false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"add Foo" and "remove Foo" don't conflict? or "remove Foo" and "replace Foo with Bar"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is just stubbed for now, its not implemented yet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just remove the method then, until we actually have use for it.
Too many stubs make a PR hard to review, imo, because it means I'm always wondering "is this incomplete in a way that I should comment on, or because it's unused?"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just remove the method then, until we actually have use for it.
Too many stubs make a PR hard to review, imo, because it means I'm always wondering "is this incomplete in a way that I should comment on, or because it's unused?"
☝️ I'm in the same boat. We could just remove it from the interface until we need it.
pkg/engine/constraints/constraint.go
Outdated
validOperators := []string{string(AddConstraintOperator), string(RemoveConstraintOperator), string(ReplaceConstraintOperator)} | ||
if !slices.Contains(validOperators, string(appConstraint.Operator)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor/readability: I find it a bit distracting with all the casts to string. Why not keep them as [] ConstraintOperator
?
validOperators := []string{string(AddConstraintOperator), string(RemoveConstraintOperator), string(ReplaceConstraintOperator)} | |
if !slices.Contains(validOperators, string(appConstraint.Operator)) { | |
validOperators := []ConstraintOperator{AddConstraintOperator, RemoveConstraintOperator, ReplaceConstraintOperator} | |
if !collectionutil.Contains(validOperators, appConstraint.Operator) { |
(You'd need to write collectionutil.Contains
, but it's trivial.)
Similar for the other cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense, going to do that
}{ | ||
{ | ||
name: "test", | ||
path: "./samples/constraints.yaml", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than hard-coding the user of the OS fs, and thus having the tests rely on a sample file, I think it'd be better to have the parse work on just bytes or an fs.FS
interface (or whatever it's called, I forget exactly), with this test providing a fully in-memory option. That way you can have each case provide just a small snippet of the yaml it's interested in, rather than having to maintain a monolithic yaml file for all possible cases (and have it be harder to read this test, since the input is in a different file from the expected results here).
} | ||
|
||
func (b *ConstructConstraint) Conflict(other Constraint) bool { | ||
return false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"construct has type=lambda" and "construct has type=eks"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just stubbed right now
type ( | ||
// NodeConstraint is a struct that represents constraints that can be applied on a specific node in the resource graph. | ||
// NodeConstraints are used to control intrinsic properties of a node in the resource graph | ||
NodeConstraint struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is a construct not a kind of node? (You have a separate ConstructConstraint in this PR, and I'm not sure what the relationship is.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i could rename this to resource constraint, but yeah the intention is this is used for end resources
if b.Target.Provider == core.AbstractConstructProvider { | ||
return errors.New("node constraint cannot be applied to an abstract construct") | ||
} | ||
if b.Property == "" || reflect.ValueOf(b.Value).IsZero() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this imply I can't say something like "set number of replicas to 0"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right now it does imply that, which maybe is incorrect, so maybe i should remove the check of value being a zero value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Yeah, I would. If we later want to ensure some value isn't 0, a system-provided constraint can do that ("instanceCount must be > 0", which would then be in conflict with "instanceCount = 0" if the user provided that).
pkg/core/construct_graph.go
Outdated
func (cg *ConstructGraph) ShortestPath(source ResourceId, dest ResourceId) ([]BaseConstruct, error) { | ||
ids, err := cg.underlying.ShortestPath(source.String(), dest.String()) | ||
if err != nil { | ||
return []BaseConstruct{}, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: return nil
not empty lists for idiomatic Go
pkg/core/resource_graph.go
Outdated
func (rg *ResourceGraph) ShortestPath(source ResourceId, dest ResourceId) ([]Resource, error) { | ||
ids, err := rg.underlying.ShortestPath(source.String(), dest.String()) | ||
if err != nil { | ||
return []Resource{}, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: same as above
pkg/core/resource_graph.go
Outdated
@@ -84,6 +96,16 @@ func GetResource[T Resource](g *ResourceGraph, id ResourceId) (resource T, ok bo | |||
return | |||
} | |||
|
|||
func (rg *ResourceGraph) FindResourcesWithRef(id ResourceId) []Resource { | |||
result := []Resource{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: use var result []Resource
which also leaves result as nil if there are no resources.
pkg/core/resources.go
Outdated
@@ -164,6 +166,15 @@ func (s BaseConstructSet) Has(k BaseConstruct) bool { | |||
return ok | |||
} | |||
|
|||
func (s BaseConstructSet) HasId(k ResourceId) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thee feels awkward, like BaseConstructSet should be a map[ResourceId]BaseConstruct
insead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, this is going to make the pr much much larger if thats ok
) | ||
|
||
func (b *ApplicationConstraint) Scope() ConstraintScope { | ||
return EdgeConstraintScope |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return EdgeConstraintScope | |
return ApplicationConstraintScope |
|
||
for _, s := range src { | ||
for _, d := range dst { | ||
path, _ := dag.ShortestPath(s, d) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't ignore errors - either we need to update the interface method or log it and return false.
Also, why only check the shortest path? Shouldn't it be like "must contain X" = "any(path contains X)", or "must not contain X" = "all(path not contains X)"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well shortest path returns an error if there is no path, which could be different based on the type of constraint err get, so i need to go modify the graph method, but i can do that.
We do want to check all i had the comment
// We will likely want to search all paths to see if ANY contain the node. There's an open issue for this https://githuconstraint.com/dominikbraun/graph/issues/82
so i was hoping to just do that once the code is supported by the library, but we can write it all if we want. Lets chat about that though
|
||
func (b *EdgeConstraint) checkSatisfication(path []core.Resource) bool { | ||
// Currently we only support MustContain & MustNotContainConstraintOperator searching for if the node exists in the shortest path | ||
// We will likely want to search all paths to see if ANY contain the node. There's an open issue for this https://github.com/dominikbraun/graph/issues/82 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see... This is probably important enough to implement ourselves. Either outside the library or fork it and PR (I prefer the latter). If you want to leave this for now, I recommend moving this comment to where ShortestPath is being called as it is more relevant there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh lol i see, ok, can i split this off into another task? Just to descope this initial pr?
) | ||
|
||
type ( | ||
// ApplicationConstraint is a struct that represents constraints that can be applied on the entire resource graph |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to see some examples with descriptions of what they mean for all these constraints.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
im going to do a large documentation pass towards the end of this if thats ok, but can toss in some samples quick for now.
Just avoiding doing too much in case it changes
pkg/engine/engine.go
Outdated
} | ||
} | ||
|
||
func (e *Engine) LoadContext(InitialState *core.ConstructGraph, Constraints map[constraints.ConstraintScope][]constraints.Constraint) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: title case is weird for variable names
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, will fix
pkg/graph/graph.go
Outdated
@@ -79,6 +83,17 @@ func (d *Directed[V]) IncomingEdges(to V) []Edge[V] { | |||
}) | |||
} | |||
|
|||
func (d *Directed[V]) RemoveVertex(v string) error { | |||
err := d.underlying.RemoveVertex(v) | |||
if err != nil && !errors.Is(err, graph.ErrVertexHasEdges) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the behaviour if it does return ErrVertexHasEdges
? Does it remove it and its edges? Does it not remove it? Does it leave it in a weird state where the node is removed but not the edges?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if it has edges it wont remove the node, so thats why for that error we should error. This is a bug though and i dont error for it, so good catch
return len(dag.FindResourcesWithRef(b.Node)) == 0 | ||
} | ||
return dag.GetResource(b.Node) == nil | ||
case ReplaceConstraintOperator: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just figured that'd be a node or construct constraint not an application one.
switch b.Operator { | ||
func (constraint *EdgeConstraint) checkSatisfication(path []core.Resource) bool { | ||
// Currently we only support searching for if the node exists in the shortest path | ||
// We will likely want to search all paths to see if ANY contain the node. There's an open issue for this https://githuconstraint.com/dominikbraun/graph/issues/82 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Find/replace got a little extra here
• Does any part of it require special attention?
• Does it relate to or fix any issue?
follows #670, will be too large until that is merged
This pr shows how we check the end resource graph to see if each constraint is satisfied.
Theres also some validate methods which we can use when taking in the constraints to ensure that they are solvable
Standard checks