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
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkg/collectionutil/slices.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ func FlattenUnique[E comparable](slices ...[]E) []E {

return result
}

func Contains[E comparable](slice []E, elem E) bool {
for _, e := range slice {
if e == elem {
return true
}
}
return false
}
33 changes: 33 additions & 0 deletions pkg/collectionutil/slices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,36 @@ func Test_AppendUnique(t *testing.T) {
})
}
}

func Test_Contains(t *testing.T) {
cases := []struct {
name string
inputs []int
b int
want bool
}{
{
name: "contains",
inputs: []int{
4, 5, 6,
},
b: 4,
want: true,
},
{
name: " does not contain",
inputs: []int{
4, 5, 6,
},
b: 3,
want: false,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
actual := Contains(tt.inputs, tt.b)
assert.Equal(tt.want, actual)
})
}
}
2 changes: 1 addition & 1 deletion pkg/core/construct_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (cg *ConstructGraph) GetUpstreamConstructs(source BaseConstruct) []BaseCons
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
return nil, err
}
resources := make([]BaseConstruct, len(ids))
for i, id := range ids {
Expand Down
6 changes: 3 additions & 3 deletions pkg/core/resource_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ 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
return nil, err
}
resources := make([]Resource, len(ids))
for i, id := range ids {
Expand Down Expand Up @@ -97,9 +97,9 @@ func GetResource[T Resource](g *ResourceGraph, id ResourceId) (resource T, ok bo
}

func (rg *ResourceGraph) FindResourcesWithRef(id ResourceId) []Resource {
result := []Resource{}
var result []Resource
for _, resource := range rg.ListResources() {
if resource.BaseConstructsRef().HasId(id) {
if resource.BaseConstructsRef().Has(id) {
result = append(result, resource)
}
}
Expand Down
21 changes: 6 additions & 15 deletions pkg/core/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type (
AnnotationCapability() string
}

BaseConstructSet map[BaseConstruct]struct{}
BaseConstructSet map[ResourceId]BaseConstruct

// Resource describes a resource at the provider, infrastructure level
Resource interface {
Expand Down Expand Up @@ -158,30 +158,21 @@ func (s *BaseConstructSet) Add(k BaseConstruct) {
if *s == nil {
*s = make(BaseConstructSet)
}
(*s)[k] = struct{}{}
(*s)[k.Id()] = k
}

func (s BaseConstructSet) Has(k BaseConstruct) bool {
func (s BaseConstructSet) Has(k ResourceId) bool {
_, ok := s[k]
return ok
}

func (s BaseConstructSet) HasId(k ResourceId) bool {
for c := range s {
if c.Id() == k {
return true
}
}
return false
}

func (s BaseConstructSet) Delete(k BaseConstruct) {
delete(s, k)
delete(s, k.Id())
}

func (s *BaseConstructSet) AddAll(ks BaseConstructSet) {
for k := range ks {
s.Add(k)
for _, c := range ks {
s.Add(c)
}
}

Expand Down
63 changes: 35 additions & 28 deletions pkg/engine/constraints/application_constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,64 +8,71 @@ import (

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

//
// Example
//
// To specify a constraint detailing application level intents in yaml
//
//- scope: application
// operator: add
// node: klotho:execution_unit:my_compute
//
// The end result of this should be that the execution unit construct is added to the construct graph for processing
ApplicationConstraint struct {
Operator ConstraintOperator `yaml:"operator"`
Node core.ResourceId `yaml:"node"`
ReplacementNode core.ResourceId `yaml:"replacement_node"`
}
)

func (b *ApplicationConstraint) Scope() ConstraintScope {
return EdgeConstraintScope
func (constraint *ApplicationConstraint) Scope() ConstraintScope {
return ApplicationConstraintScope
}

func (b *ApplicationConstraint) IsSatisfied(dag *core.ResourceGraph) bool {
switch b.Operator {
func (constraint *ApplicationConstraint) IsSatisfied(dag *core.ResourceGraph) bool {
switch constraint.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
if constraint.Node.Provider == core.AbstractConstructProvider {
return len(dag.FindResourcesWithRef(constraint.Node)) > 0
}
return dag.GetResource(b.Node) != nil
return dag.GetResource(constraint.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
if constraint.Node.Provider == core.AbstractConstructProvider {
return len(dag.FindResourcesWithRef(constraint.Node)) == 0
}
return dag.GetResource(b.Node) == nil
return dag.GetResource(constraint.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.

Shouldn't IsSatisfied for replace just be Remove + Add? Since you don't get a before/after to check edge updating you can only check the nodes.

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

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.

// 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

// We should ensure edges are copied from the original source to the new replacement node in the dag
// Ignoring for now, but will be an extra check we can make later to ensure that the Replace constraint is fully satisfied

// 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
if constraint.Node.Provider == core.AbstractConstructProvider && constraint.ReplacementNode.Provider == core.AbstractConstructProvider {
return len(dag.FindResourcesWithRef(constraint.Node)) == 0 && len(dag.FindResourcesWithRef(constraint.ReplacementNode)) > 0
} else if constraint.Node.Provider == core.AbstractConstructProvider && constraint.ReplacementNode.Provider != core.AbstractConstructProvider {
return len(dag.FindResourcesWithRef(constraint.Node)) == 0 && dag.GetResource(constraint.ReplacementNode) != nil
} else if constraint.Node.Provider != core.AbstractConstructProvider && constraint.ReplacementNode.Provider == core.AbstractConstructProvider {
return dag.GetResource(constraint.Node) == nil && len(dag.FindResourcesWithRef(constraint.Node)) > 0
}
return dag.GetResource(b.Node) == nil && dag.GetResource(b.ReplacementNode) != nil
return dag.GetResource(constraint.Node) == nil && dag.GetResource(constraint.ReplacementNode) != nil
}
return false
}

func (b *ApplicationConstraint) Conflict(other Constraint) bool {
return false
}

func (b *ApplicationConstraint) Validate() error {
if b.Operator == ReplaceConstraintOperator && (b.Node == core.ResourceId{} || b.ReplacementNode == core.ResourceId{}) {
func (constraint *ApplicationConstraint) Validate() error {
if constraint.Operator == ReplaceConstraintOperator && (constraint.Node == core.ResourceId{} || constraint.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 {
if constraint.Operator == ReplaceConstraintOperator && constraint.Node.Provider != core.AbstractConstructProvider && constraint.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{}) {
if constraint.Operator == AddConstraintOperator && (constraint.Node == core.ResourceId{}) {
return errors.New("add constraint must have a node defined")
}

if b.Operator == RemoveConstraintOperator && (b.Node == core.ResourceId{}) {
if constraint.Operator == RemoveConstraintOperator && (constraint.Node == core.ResourceId{}) {
return errors.New("remove constraint must have a node defined")
}
return nil
Expand Down
59 changes: 28 additions & 31 deletions pkg/engine/constraints/constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package constraints
import (
"errors"
"fmt"
"os"
"reflect"

"github.com/klothoplatform/klotho/pkg/collectionutil"
"github.com/klothoplatform/klotho/pkg/core"
"gopkg.in/yaml.v3"
"k8s.io/utils/strings/slices"
Expand All @@ -20,9 +20,6 @@ type (
// IsSatisfied returns whether or not the constraint is satisfied based on the resource graph
// For a resource graph to be valid all constraints must be satisfied
IsSatisfied(dag *core.ResourceGraph) bool
// Conflict returns whether or not the constraint conflicts with another constraint
// If constraints conflict, then the constraints passed in are unsolveable
Conflict(other Constraint) bool
// Validate returns whether or not the constraint is valid
Validate() error
}
Expand All @@ -49,7 +46,7 @@ const (
ApplicationConstraintScope ConstraintScope = "application"
ConstructConstraintScope ConstraintScope = "construct"
EdgeConstraintScope ConstraintScope = "edge"
NodeConstraintScope ConstraintScope = "node"
ResourceConstraintScope ConstraintScope = "resource"

MustExistConstraintOperator ConstraintOperator = "must_exist"
MustNotExistConstraintOperator ConstraintOperator = "must_not_exist"
Expand All @@ -76,21 +73,16 @@ func DecodeYAMLNode[T interface {
return constraint, err
}

// ParseConstraintsFromFile is a helper function that parses a yaml file into a map of constraints
// ParseConstraintsFromFile parses a yaml file into a map of constraints
//
// Future spec may include ordering of the application of constraints, but for now we assume that the order of the constraints is based on the yaml file and they cannot be grouped outside of scope
func ParseConstraintsFromFile(path string) (map[ConstraintScope][]Constraint, error) {
func ParseConstraintsFromFile(bytes []byte) (map[ConstraintScope][]Constraint, error) {
constraints := map[ConstraintScope][]Constraint{}
f, err := os.Open(path)
if err != nil {
return constraints, err
}
defer f.Close() // nolint:errcheck

node := &yaml.Node{}
err = yaml.NewDecoder(f).Decode(node)
err := yaml.Unmarshal(bytes, node)
if err != nil {
return constraints, err
return nil, err
}

var joinedErr error
Expand All @@ -104,25 +96,25 @@ func ParseConstraintsFromFile(path string) (map[ConstraintScope][]Constraint, er
}
switch base.Scope {
case ApplicationConstraintScope:
appConstraint, err := DecodeYAMLNode[*ApplicationConstraint](a)
constraint, err := DecodeYAMLNode[*ApplicationConstraint](a)
if err != nil {
joinedErr = errors.Join(joinedErr, err)
continue
}
validOperators := []string{string(AddConstraintOperator), string(RemoveConstraintOperator), string(ReplaceConstraintOperator)}
if !slices.Contains(validOperators, string(appConstraint.Operator)) {
joinedErr = errors.Join(joinedErr, fmt.Errorf("invalid operator %s for application constraint", appConstraint.Operator))
validOperators := []ConstraintOperator{AddConstraintOperator, RemoveConstraintOperator, ReplaceConstraintOperator}
if !collectionutil.Contains(validOperators, constraint.Operator) {
joinedErr = errors.Join(joinedErr, fmt.Errorf("invalid operator %s for application constraint", constraint.Operator))
continue
}
constraints[ApplicationConstraintScope] = append(constraints[ApplicationConstraintScope], appConstraint)
constraints[ApplicationConstraintScope] = append(constraints[ApplicationConstraintScope], constraint)
case ConstructConstraintScope:
constraint, err := DecodeYAMLNode[*ConstructConstraint](a)
if err != nil {
joinedErr = errors.Join(joinedErr, err)
continue
}
validOperators := []string{string(EqualsConstraintOperator)}
if !slices.Contains(validOperators, string(constraint.Operator)) {
validOperators := []ConstraintOperator{EqualsConstraintOperator}
if !collectionutil.Contains(validOperators, constraint.Operator) {
joinedErr = errors.Join(joinedErr, fmt.Errorf("invalid operator %s for application constraint", constraint.Operator))
continue
}
Expand All @@ -133,24 +125,24 @@ func ParseConstraintsFromFile(path string) (map[ConstraintScope][]Constraint, er
joinedErr = errors.Join(joinedErr, err)
continue
}
validOperators := []string{string(MustContainConstraintOperator), string(MustNotContainConstraintOperator), string(MustExistConstraintOperator), string(MustNotExistConstraintOperator)}
if !slices.Contains(validOperators, string(constraint.Operator)) {
validOperators := []ConstraintOperator{MustContainConstraintOperator, MustNotContainConstraintOperator, MustExistConstraintOperator, MustNotExistConstraintOperator}
if !collectionutil.Contains(validOperators, constraint.Operator) {
joinedErr = errors.Join(joinedErr, fmt.Errorf("invalid operator %s for application constraint", constraint.Operator))
continue
}
constraints[EdgeConstraintScope] = append(constraints[EdgeConstraintScope], constraint)
case NodeConstraintScope:
constraint, err := DecodeYAMLNode[*NodeConstraint](a)
case ResourceConstraintScope:
constraint, err := DecodeYAMLNode[*ResourceConstraint](a)
if err != nil {
joinedErr = errors.Join(joinedErr, err)
continue
}
validOperators := []string{string(EqualsConstraintOperator)}
if !slices.Contains(validOperators, string(constraint.Operator)) {
validOperators := []ConstraintOperator{EqualsConstraintOperator}
if !collectionutil.Contains(validOperators, constraint.Operator) {
joinedErr = errors.Join(joinedErr, fmt.Errorf("invalid operator %s for application constraint", constraint.Operator))
continue
}
constraints[NodeConstraintScope] = append(constraints[NodeConstraintScope], constraint)
constraints[ResourceConstraintScope] = append(constraints[ResourceConstraintScope], constraint)
}
}
}
Expand All @@ -160,9 +152,14 @@ func ParseConstraintsFromFile(path string) (map[ConstraintScope][]Constraint, er
// extraFields is a helper function that checks if there are any extra fields in a yaml node that are not in the struct
// Because you cant use the KnownFields in a nodes decode funtion we handle it ourselves
func extraFields(n *yaml.Node, object reflect.Value) error {
knownFields := []string{}
knownFields := []string{"scope"}
for i := 0; i < object.Elem().NumField(); i++ {
knownFields = append(knownFields, object.Elem().Type().Field(i).Tag.Get("yaml"))
fieldName := object.Elem().Type().Field(i).Name
yamlTag := object.Elem().Type().Field(i).Tag.Get("yaml")
if yamlTag != "" {
fieldName = yamlTag
}
knownFields = append(knownFields, fieldName)
}

if n.Kind != yaml.MappingNode {
Expand All @@ -174,7 +171,7 @@ func extraFields(n *yaml.Node, object reflect.Value) error {
}

for k := range m {
if !slices.Contains(knownFields, k) && k != "scope" {
if !slices.Contains(knownFields, k) {
return fmt.Errorf("unexpected field %s", k)
}
}
Expand Down
Loading