-
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
Changes from 1 commit
ba0a446
cbf731b
f11e3d9
68dbc1d
49e11db
7088070
5bb4d36
1e979f3
5dc86ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,18 @@ func NewResourceGraph() *ResourceGraph { | |
} | ||
} | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. nit: same as above |
||
} | ||
resources := make([]Resource, len(ids)) | ||
for i, id := range ids { | ||
resources[i] = rg.underlying.GetVertex(id) | ||
} | ||
return resources, nil | ||
} | ||
|
||
func (rg *ResourceGraph) AddResource(resource Resource) { | ||
if rg.GetResource(resource.Id()) == nil { | ||
rg.underlying.AddVertex(resource) | ||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. nit: use |
||
for _, resource := range rg.ListResources() { | ||
if resource.BaseConstructsRef().HasId(id) { | ||
result = append(result, resource) | ||
} | ||
} | ||
return result | ||
} | ||
|
||
func (rg *ResourceGraph) GetDependency(source ResourceId, target ResourceId) *graph.Edge[Resource] { | ||
return rg.underlying.GetEdge(source.String(), target.String()) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,6 +67,8 @@ type ( | |
HasLocalOutput interface { | ||
OutputTo(dest string) error | ||
} | ||
|
||
Capability string | ||
) | ||
|
||
const ( | ||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. Thee feels awkward, like BaseConstructSet should be a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
for c := range s { | ||
if c.Id() == k { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func (s BaseConstructSet) Delete(k BaseConstruct) { | ||
delete(s, k) | ||
} | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,14 +1,17 @@ | ||||||
package constraints | ||||||
|
||||||
import "github.com/klothoplatform/klotho/pkg/core" | ||||||
import ( | ||||||
"errors" | ||||||
|
||||||
"github.com/klothoplatform/klotho/pkg/core" | ||||||
) | ||||||
|
||||||
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 commentThe 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 commentThe 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 |
||||||
ApplicationConstraint struct { | ||||||
Operator ConstraintOperator `yaml:"operator"` | ||||||
Node core.ResourceId `yaml:"node"` | ||||||
ReplacementNode core.ResourceId `yaml:"replacement_node"` | ||||||
Edge Edge `yaml:"edge"` | ||||||
} | ||||||
) | ||||||
|
||||||
|
@@ -17,13 +20,53 @@ func (b *ApplicationConstraint) Scope() ConstraintScope { | |||||
} | ||||||
|
||||||
func (b *ApplicationConstraint) IsSatisfied(dag *core.ResourceGraph) bool { | ||||||
return false | ||||||
} | ||||||
switch b.Operator { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
case AddConstraintOperator: | ||||||
// If the add was for a construct, we need to check if any resource references the construct | ||||||
if b.Node.Provider == core.AbstractConstructProvider { | ||||||
return len(dag.FindResourcesWithRef(b.Node)) > 0 | ||||||
} | ||||||
return dag.GetResource(b.Node) != nil | ||||||
case RemoveConstraintOperator: | ||||||
// If the remove was for a construct, we need to check if any resource references the construct | ||||||
if b.Node.Provider == core.AbstractConstructProvider { | ||||||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well for replace my thought is that we will want to check that we copied the edges over soon, so thats why it is separate. If we recieved a remove + add we wouldnt copy edges There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
// 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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Is this what you meant? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. likely a GH autocomplete comment, will fix |
||||||
// Ignoring for now, but will be an optimization we can make | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) Apply(dag *core.ResourceGraph) error { | ||||||
return nil | ||||||
// If any of the nodes are abstract constructs, we need to check if any resource references the construct | ||||||
if b.Node.Provider == core.AbstractConstructProvider && b.ReplacementNode.Provider == core.AbstractConstructProvider { | ||||||
return len(dag.FindResourcesWithRef(b.Node)) == 0 && len(dag.FindResourcesWithRef(b.ReplacementNode)) > 0 | ||||||
} else if b.Node.Provider == core.AbstractConstructProvider && b.ReplacementNode.Provider != core.AbstractConstructProvider { | ||||||
return len(dag.FindResourcesWithRef(b.Node)) == 0 && dag.GetResource(b.ReplacementNode) != nil | ||||||
} else if b.Node.Provider != core.AbstractConstructProvider && b.ReplacementNode.Provider == core.AbstractConstructProvider { | ||||||
return dag.GetResource(b.Node) == nil && len(dag.FindResourcesWithRef(b.Node)) > 0 | ||||||
} | ||||||
return dag.GetResource(b.Node) == nil && dag.GetResource(b.ReplacementNode) != nil | ||||||
} | ||||||
return false | ||||||
} | ||||||
|
||||||
func (b *ApplicationConstraint) Conflict(other Constraint) bool { | ||||||
return false | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not entirely sure what There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed for now There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more.
☝️ I'm in the same boat. We could just remove it from the interface until we need it. |
||||||
} | ||||||
|
||||||
func (b *ApplicationConstraint) Validate() error { | ||||||
if b.Operator == ReplaceConstraintOperator && (b.Node == core.ResourceId{} || b.ReplacementNode == core.ResourceId{}) { | ||||||
return errors.New("replace constraint must have a node and replacement node defined") | ||||||
} | ||||||
if b.Operator == ReplaceConstraintOperator && b.Node.Provider != core.AbstractConstructProvider && b.ReplacementNode.Provider == core.AbstractConstructProvider { | ||||||
return errors.New("replace constraint cannot replace a resource with an abstract construct") | ||||||
} | ||||||
if b.Operator == AddConstraintOperator && (b.Node == core.ResourceId{}) { | ||||||
return errors.New("add constraint must have a node defined") | ||||||
} | ||||||
|
||||||
if b.Operator == RemoveConstraintOperator && (b.Node == core.ResourceId{}) { | ||||||
return errors.New("remove constraint must have a node defined") | ||||||
} | ||||||
return nil | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
package constraints | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/klothoplatform/klotho/pkg/core" | ||
"github.com/klothoplatform/klotho/pkg/provider/aws/resources" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func Test_ApplicationConstraint_IsSatisfied(t *testing.T) { | ||
eu := &core.ExecutionUnit{Name: "compute"} | ||
eu2 := &core.ExecutionUnit{Name: "compute2"} | ||
|
||
tests := []struct { | ||
name string | ||
constraint []ApplicationConstraint | ||
resources []core.Resource | ||
want bool | ||
}{ | ||
{ | ||
name: "Add is satisfied", | ||
constraint: []ApplicationConstraint{ | ||
{ | ||
Operator: AddConstraintOperator, | ||
Node: core.ResourceId{Provider: core.AbstractConstructProvider, Type: core.EXECUTION_UNIT_TYPE, Name: "compute"}, | ||
}, | ||
{ | ||
Operator: AddConstraintOperator, | ||
Node: core.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_function_also"}, | ||
}, | ||
}, | ||
resources: []core.Resource{ | ||
&resources.LambdaFunction{ | ||
Name: "my_function_also", | ||
}, | ||
&resources.LambdaFunction{ | ||
Name: "my_function", | ||
ConstructsRef: core.BaseConstructSetOf(eu), | ||
}, | ||
}, | ||
want: true, | ||
}, | ||
{ | ||
name: "Add is not satisfied", | ||
constraint: []ApplicationConstraint{ | ||
{ | ||
Operator: AddConstraintOperator, | ||
Node: core.ResourceId{Provider: core.AbstractConstructProvider, Type: core.EXECUTION_UNIT_TYPE, Name: "compute"}, | ||
}, | ||
{ | ||
Operator: AddConstraintOperator, | ||
Node: core.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_function_also"}, | ||
}, | ||
}, | ||
resources: []core.Resource{ | ||
&resources.LambdaFunction{ | ||
Name: "my_function", | ||
}, | ||
}, | ||
want: false, | ||
}, | ||
{ | ||
name: "remove is satisfied", | ||
constraint: []ApplicationConstraint{ | ||
{ | ||
Operator: RemoveConstraintOperator, | ||
Node: core.ResourceId{Provider: core.AbstractConstructProvider, Type: core.EXECUTION_UNIT_TYPE, Name: "compute"}, | ||
}, | ||
{ | ||
Operator: RemoveConstraintOperator, | ||
Node: core.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_function_also"}, | ||
}, | ||
}, | ||
resources: []core.Resource{ | ||
&resources.LambdaFunction{ | ||
Name: "my_function", | ||
}, | ||
}, | ||
want: true, | ||
}, | ||
{ | ||
name: "remove is not satisfied", | ||
constraint: []ApplicationConstraint{ | ||
{ | ||
Operator: RemoveConstraintOperator, | ||
Node: core.ResourceId{Provider: core.AbstractConstructProvider, Type: core.EXECUTION_UNIT_TYPE, Name: "compute"}, | ||
}, | ||
{ | ||
Operator: RemoveConstraintOperator, | ||
Node: core.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_function_also"}, | ||
}, | ||
}, | ||
resources: []core.Resource{ | ||
&resources.LambdaFunction{ | ||
Name: "my_function_also", | ||
}, | ||
&resources.LambdaFunction{ | ||
Name: "my_function", | ||
ConstructsRef: core.BaseConstructSetOf(eu), | ||
}, | ||
}, | ||
want: false, | ||
}, | ||
{ | ||
name: "replace is satisfied", | ||
constraint: []ApplicationConstraint{ | ||
{ | ||
Operator: ReplaceConstraintOperator, | ||
Node: core.ResourceId{Provider: core.AbstractConstructProvider, Type: core.EXECUTION_UNIT_TYPE, Name: "compute"}, | ||
ReplacementNode: core.ResourceId{Provider: core.AbstractConstructProvider, Type: core.EXECUTION_UNIT_TYPE, Name: "compute2"}, | ||
}, | ||
{ | ||
Operator: ReplaceConstraintOperator, | ||
Node: core.ResourceId{Provider: core.AbstractConstructProvider, Type: core.EXECUTION_UNIT_TYPE, Name: "compute"}, | ||
ReplacementNode: core.ResourceId{Provider: "aws", Type: "lambda_function", Name: "lambda_compute"}, | ||
}, | ||
{ | ||
Operator: ReplaceConstraintOperator, | ||
Node: core.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_function_also"}, | ||
ReplacementNode: core.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_function_also2"}, | ||
}, | ||
}, | ||
resources: []core.Resource{ | ||
&resources.LambdaFunction{ | ||
Name: "lambda_compute", | ||
}, | ||
&resources.LambdaFunction{ | ||
Name: "my_function_also2", | ||
}, | ||
&resources.LambdaFunction{ | ||
Name: "my_function", | ||
ConstructsRef: core.BaseConstructSetOf(eu2), | ||
}, | ||
}, | ||
want: true, | ||
}, | ||
{ | ||
name: "replace is not satisfied", | ||
constraint: []ApplicationConstraint{ | ||
{ | ||
Operator: ReplaceConstraintOperator, | ||
Node: core.ResourceId{Provider: core.AbstractConstructProvider, Type: core.EXECUTION_UNIT_TYPE, Name: "compute"}, | ||
ReplacementNode: core.ResourceId{Provider: core.AbstractConstructProvider, Type: core.EXECUTION_UNIT_TYPE, Name: "compute2"}, | ||
}, | ||
{ | ||
Operator: ReplaceConstraintOperator, | ||
Node: core.ResourceId{Provider: core.AbstractConstructProvider, Type: core.EXECUTION_UNIT_TYPE, Name: "compute"}, | ||
ReplacementNode: core.ResourceId{Provider: "aws", Type: "lambda_function", Name: "lambda_compute"}, | ||
}, | ||
{ | ||
Operator: ReplaceConstraintOperator, | ||
Node: core.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_function_also"}, | ||
ReplacementNode: core.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_function_also2"}, | ||
}, | ||
}, | ||
resources: []core.Resource{ | ||
&resources.LambdaFunction{ | ||
Name: "my_function_also", | ||
}, | ||
&resources.LambdaFunction{ | ||
Name: "my_function", | ||
ConstructsRef: core.BaseConstructSetOf(eu), | ||
}, | ||
}, | ||
want: false, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
assert := assert.New(t) | ||
dag := core.NewResourceGraph() | ||
for _, res := range tt.resources { | ||
dag.AddResource(res) | ||
} | ||
for _, constraint := range tt.constraint { | ||
result := constraint.IsSatisfied(dag) | ||
assert.Equalf(tt.want, result, "constraint %s is not satisfied", 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: return
nil
not empty lists for idiomatic Go