Skip to content
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

Merged
merged 9 commits into from
Jun 16, 2023
Merged

Conversation

jhsinger-klotho
Copy link
Contributor

• 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

  • Unit tests: Any special considerations?
  • Docs: Do we need to update any docs, internal or public?
  • Backwards compatibility: Will this break existing apps? If so, what would be the extra work required to keep them working?

@jhsinger-klotho jhsinger-klotho changed the title Constraints satisfication Constraints satisfication checks Jun 13, 2023
}
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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?

Copy link
Contributor Author

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
Copy link

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 {
Copy link

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.)

Copy link
Contributor Author

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 {
Copy link

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.

Copy link
Contributor Author

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
Copy link

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"?

Copy link
Contributor Author

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

Copy link

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?"

Copy link
Contributor

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.

Comment on lines 112 to 113
validOperators := []string{string(AddConstraintOperator), string(RemoveConstraintOperator), string(ReplaceConstraintOperator)}
if !slices.Contains(validOperators, string(appConstraint.Operator)) {
Copy link

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 ?

Suggested change
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.

Copy link
Contributor Author

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",
Copy link

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
Copy link

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"?

Copy link
Contributor Author

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 {
Copy link

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.)

Copy link
Contributor Author

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() {
Copy link

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"?

Copy link
Contributor Author

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

Copy link

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).

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
Copy link
Contributor

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

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same as above

@@ -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{}
Copy link
Contributor

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.

@@ -164,6 +166,15 @@ func (s BaseConstructSet) Has(k BaseConstruct) bool {
return ok
}

func (s BaseConstructSet) HasId(k ResourceId) bool {
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return EdgeConstraintScope
return ApplicationConstraintScope


for _, s := range src {
for _, d := range dst {
path, _ := dag.ShortestPath(s, d)
Copy link
Contributor

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)"?

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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

}
}

func (e *Engine) LoadContext(InitialState *core.ConstructGraph, Constraints map[constraints.ConstraintScope][]constraints.Constraint) {
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, will fix

@@ -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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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

@jhsinger-klotho jhsinger-klotho mentioned this pull request Jun 13, 2023
return len(dag.FindResourcesWithRef(b.Node)) == 0
}
return dag.GetResource(b.Node) == nil
case ReplaceConstraintOperator:
Copy link
Contributor

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
Copy link
Contributor

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

@jhsinger-klotho jhsinger-klotho merged commit d9485c9 into main Jun 16, 2023
@jhsinger-klotho jhsinger-klotho deleted the constraints_satisfication branch June 16, 2023 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants