From 9d5e75abcd97818822dbe2404b0a400cc2f4d4c2 Mon Sep 17 00:00:00 2001 From: jhsinger-klotho <111291520+jhsinger-klotho@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:58:07 -0500 Subject: [PATCH] Properties (#798) --- pkg/construct2/properties.go | 17 + pkg/construct2/resource_id.go | 9 + pkg/engine2/cli.go | 46 +- .../operational_eval/dependency_capture.go | 48 +- pkg/engine2/operational_eval/graph.go | 5 +- .../operational_eval/vertex_path_expand.go | 34 +- .../operational_eval/vertex_property.go | 53 +- .../operational_rule/operational_action.go | 13 +- .../operational_configuration.go | 15 +- .../operational_rule/operational_rule.go | 90 ++- .../operational_rule/operational_step.go | 96 ++- .../operational_rule/operational_step_test.go | 111 ++- .../path_selection/candidate_validity.go | 6 +- pkg/engine2/path_selection/edge_validity.go | 108 +-- pkg/engine2/path_selection/path_expansion.go | 65 +- pkg/engine2/solution_context/decisions.go | 10 +- .../resource_configuration.go | 15 +- pkg/engine2/testdata/2_routes.expect.yaml | 2 +- .../testdata/ecs_rds.dataflow-viz.yaml | 2 +- pkg/engine2/testdata/ecs_rds.expect.yaml | 58 +- pkg/engine2/testdata/ecs_rds.iac-viz.yaml | 16 +- .../testdata/k8s_api.dataflow-viz.yaml | 2 +- pkg/engine2/testdata/k8s_api.expect.yaml | 76 +- pkg/engine2/testdata/k8s_api.iac-viz.yaml | 20 +- pkg/infra/cli2.go | 4 +- pkg/knowledge_base2/emitter.go | 20 +- pkg/knowledge_base2/graph.go | 92 +-- pkg/knowledge_base2/kb.go | 82 +-- pkg/knowledge_base2/model.go | 103 +-- pkg/knowledge_base2/operational_rule.go | 22 +- .../properties/any_property.go | 116 +++ .../properties/any_property_test.go | 93 +++ .../properties/bool_property.go | 95 +++ .../properties/bool_property_test.go | 311 ++++++++ .../properties/float_property.go | 110 +++ .../properties/float_property_test.go | 338 +++++++++ .../properties/int_property.go | 103 +++ .../properties/int_property_test.go | 359 +++++++++ .../properties/list_property.go | 213 ++++++ .../properties/list_property_test.go | 419 +++++++++++ .../properties/map_property.go | 227 ++++++ .../properties/map_property_test.go | 463 ++++++++++++ pkg/knowledge_base2/properties/property.go | 65 ++ .../properties/resource_property.go | 142 ++++ .../properties/resource_property_test.go | 312 ++++++++ .../properties/set_property.go | 193 +++++ .../properties/set_property_test.go | 591 +++++++++++++++ .../properties/string_property.go | 121 +++ .../properties/string_property_test.go | 457 ++++++++++++ pkg/knowledge_base2/property_types.go | 647 ---------------- pkg/knowledge_base2/property_types_test.go | 690 ------------------ pkg/knowledge_base2/{ => reader}/embed.go | 62 +- pkg/knowledge_base2/reader/models.go | 126 ++++ pkg/knowledge_base2/reader/properties.go | 288 ++++++++ pkg/knowledge_base2/reader/properties_test.go | 40 + .../reader/resource_template.go | 55 ++ pkg/knowledge_base2/resource_template.go | 282 ++++--- pkg/knowledge_base2/resource_template_test.go | 157 ---- pkg/knowledge_base2/sanitization.go | 59 ++ .../aws/resources/api_deployment.yaml | 8 +- .../aws/resources/api_integration.yaml | 54 +- pkg/templates/aws/resources/api_method.yaml | 20 +- pkg/templates/aws/resources/api_resource.yaml | 32 +- pkg/templates/aws/resources/api_stage.yaml | 16 +- .../aws/resources/app_runer_service.yaml | 20 +- .../aws/resources/availability_zone.yaml | 8 +- pkg/templates/aws/resources/ec2_instance.yaml | 40 +- pkg/templates/aws/resources/ecr_image.yaml | 8 +- pkg/templates/aws/resources/ecs_service.yaml | 44 +- .../aws/resources/ecs_task_definition.yaml | 38 +- .../aws/resources/efs_access_point.yaml | 8 +- .../aws/resources/efs_file_system.yaml | 8 +- .../aws/resources/efs_mount_target.yaml | 32 +- pkg/templates/aws/resources/eks_cluster.yaml | 57 +- .../aws/resources/eks_fargate_profile.yaml | 36 +- .../aws/resources/eks_node_group.yaml | 34 +- .../aws/resources/elasticache_cluster.yaml | 28 +- .../resources/elasticache_subnet_group.yaml | 16 +- .../aws/resources/iam_instance_profile.yaml | 10 +- .../aws/resources/iam_oidc_provider.yaml | 8 +- .../aws/resources/internet_gateway.yaml | 8 +- .../lambda_event_source_mapping.yaml | 18 +- .../aws/resources/lambda_function.yaml | 66 +- .../aws/resources/lambda_permission.yaml | 8 +- .../aws/resources/listener_certificate.yaml | 8 +- .../aws/resources/load_balancer.yaml | 37 +- pkg/templates/aws/resources/nat_gateway.yaml | 24 +- .../aws/resources/private_dns_namespace.yaml | 8 +- pkg/templates/aws/resources/rds_instance.yaml | 18 +- pkg/templates/aws/resources/rds_proxy.yaml | 36 +- .../aws/resources/rds_subnet_group.yaml | 16 +- pkg/templates/aws/resources/rest_api.yaml | 8 +- pkg/templates/aws/resources/route_table.yaml | 8 +- .../aws/resources/secret_version.yaml | 10 +- .../aws/resources/security_group.yaml | 8 +- pkg/templates/aws/resources/subnet.yaml | 28 +- pkg/templates/aws/resources/vpc_endpoint.yaml | 16 +- .../kubernetes/models/container.yaml | 14 +- pkg/templates/kubernetes/models/pod_spec.yaml | 10 +- .../kubernetes/resources/cluster_set.yaml | 12 +- .../kubernetes/resources/config_map.yaml | 12 +- .../kubernetes/resources/deployment.yaml | 12 +- .../kubernetes/resources/helm_chart.yaml | 12 +- .../resources/horizontal_pod_autoscaler.yaml | 12 +- .../resources/kustomize_directory.yaml | 12 +- .../kubernetes/resources/manifest.yaml | 12 +- .../kubernetes/resources/namespace.yaml | 12 +- .../resources/persistent_volume.yaml | 20 +- .../resources/persistent_volume_claim.yaml | 28 +- pkg/templates/kubernetes/resources/pod.yaml | 12 +- .../kubernetes/resources/service.yaml | 12 +- .../kubernetes/resources/service_account.yaml | 12 +- .../kubernetes/resources/service_export.yaml | 12 +- .../kubernetes/resources/storage_class.yaml | 14 +- .../resources/target_group_binding.yaml | 11 +- 115 files changed, 6637 insertions(+), 2828 deletions(-) create mode 100644 pkg/knowledge_base2/properties/any_property.go create mode 100644 pkg/knowledge_base2/properties/any_property_test.go create mode 100644 pkg/knowledge_base2/properties/bool_property.go create mode 100644 pkg/knowledge_base2/properties/bool_property_test.go create mode 100644 pkg/knowledge_base2/properties/float_property.go create mode 100644 pkg/knowledge_base2/properties/float_property_test.go create mode 100644 pkg/knowledge_base2/properties/int_property.go create mode 100644 pkg/knowledge_base2/properties/int_property_test.go create mode 100644 pkg/knowledge_base2/properties/list_property.go create mode 100644 pkg/knowledge_base2/properties/list_property_test.go create mode 100644 pkg/knowledge_base2/properties/map_property.go create mode 100644 pkg/knowledge_base2/properties/map_property_test.go create mode 100644 pkg/knowledge_base2/properties/property.go create mode 100644 pkg/knowledge_base2/properties/resource_property.go create mode 100644 pkg/knowledge_base2/properties/resource_property_test.go create mode 100644 pkg/knowledge_base2/properties/set_property.go create mode 100644 pkg/knowledge_base2/properties/set_property_test.go create mode 100644 pkg/knowledge_base2/properties/string_property.go create mode 100644 pkg/knowledge_base2/properties/string_property_test.go delete mode 100644 pkg/knowledge_base2/property_types.go delete mode 100644 pkg/knowledge_base2/property_types_test.go rename pkg/knowledge_base2/{ => reader}/embed.go (71%) create mode 100644 pkg/knowledge_base2/reader/models.go create mode 100644 pkg/knowledge_base2/reader/properties.go create mode 100644 pkg/knowledge_base2/reader/properties_test.go create mode 100644 pkg/knowledge_base2/reader/resource_template.go delete mode 100644 pkg/knowledge_base2/resource_template_test.go create mode 100644 pkg/knowledge_base2/sanitization.go diff --git a/pkg/construct2/properties.go b/pkg/construct2/properties.go index d1fe03ad0..7a8e309dc 100644 --- a/pkg/construct2/properties.go +++ b/pkg/construct2/properties.go @@ -432,6 +432,23 @@ func (i *mapValuePathItem) Remove(value any) (err error) { arr = arr.Elem() } if arr.Kind() != reflect.Slice && arr.Kind() != reflect.Array { + if hs, ok := arr.Interface().(set.HashedSet[string, any]); ok { + if hs.Contains(value) { + removed := hs.Remove(value) + if !removed { + return &PropertyPathError{ + Path: itemToPath(i), + Cause: fmt.Errorf("value %v not removed from set", value), + } + } + } else { + return &PropertyPathError{ + Path: itemToPath(i), + Cause: fmt.Errorf("value %v not found in set", value), + } + } + return nil + } return &PropertyPathError{ Path: itemToPath(i), Cause: fmt.Errorf("for non-nil value'd (%v), must be array (got %s) to remove by value", value, arr.Type()), diff --git a/pkg/construct2/resource_id.go b/pkg/construct2/resource_id.go index cda432c14..4f51f1ce2 100644 --- a/pkg/construct2/resource_id.go +++ b/pkg/construct2/resource_id.go @@ -67,6 +67,15 @@ func (l *ResourceList) UnmarshalText(b []byte) error { return fmt.Errorf("could not unmarshal resource list: %s", string(b)) } +func (l ResourceList) MatchesAny(id ResourceId) bool { + for _, rid := range l { + if rid.Matches(id) { + return true + } + } + return false +} + var zeroId = ResourceId{} func (id ResourceId) IsZero() bool { diff --git a/pkg/engine2/cli.go b/pkg/engine2/cli.go index fa037ed18..b53b9f8ab 100644 --- a/pkg/engine2/cli.go +++ b/pkg/engine2/cli.go @@ -5,15 +5,18 @@ import ( "fmt" "os" "path/filepath" + "reflect" "runtime/pprof" "strings" + "github.com/iancoleman/strcase" "github.com/klothoplatform/klotho/pkg/analytics" "github.com/klothoplatform/klotho/pkg/closenicely" construct "github.com/klothoplatform/klotho/pkg/construct2" "github.com/klothoplatform/klotho/pkg/engine2/constraints" "github.com/klothoplatform/klotho/pkg/io" knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/klothoplatform/klotho/pkg/knowledge_base2/reader" "github.com/klothoplatform/klotho/pkg/logging" "github.com/klothoplatform/klotho/pkg/templates" "github.com/pkg/errors" @@ -125,7 +128,7 @@ func (em *EngineMain) AddEngineCli(root *cobra.Command) { } func (em *EngineMain) AddEngine() error { - kb, err := knowledgebase.NewKBFromFs(templates.ResourceTemplates, templates.EdgeTemplates, templates.Models) + kb, err := reader.NewKBFromFs(templates.ResourceTemplates, templates.EdgeTemplates, templates.Models) if err != nil { return err } @@ -140,17 +143,27 @@ type resourceInfo struct { Views map[string]string `json:"views"` } -func addSubProperties(properties map[string]any, subProperties map[string]*knowledgebase.Property) { +var validationFields = []string{"MinLength", "MaxLength", "MinValue", "MaxValue", "AllowedValues"} + +func addSubProperties(properties map[string]any, subProperties map[string]knowledgebase.Property) { for _, subProperty := range subProperties { - properties[subProperty.Name] = map[string]any{ - "type": subProperty.Type, - "deployTime": subProperty.DeployTime, - "configurationDisabled": subProperty.ConfigurationDisabled, - "required": subProperty.Required, + details := subProperty.Details() + properties[details.Name] = map[string]any{ + "type": subProperty.Type(), + "deployTime": details.DeployTime, + "configurationDisabled": details.ConfigurationDisabled, + "required": details.Required, + } + for _, validationField := range validationFields { + valField := reflect.ValueOf(subProperty).Elem().FieldByName(validationField) + if valField.IsValid() && !valField.IsZero() { + val := valField.Interface() + properties[details.Name].(map[string]any)[strcase.ToLowerCamel(validationField)] = val + } } - if subProperty.Properties != nil { - properties[subProperty.Name].(map[string]any)["properties"] = map[string]any{} - addSubProperties(properties[subProperty.Name].(map[string]any)["properties"].(map[string]any), subProperty.Properties) + if subProperty.SubProperties() != nil { + properties[details.Name].(map[string]any)["properties"] = map[string]any{} + addSubProperties(properties[details.Name].(map[string]any)["properties"].(map[string]any), subProperty.SubProperties()) } } @@ -166,18 +179,7 @@ func (em *EngineMain) ListResourceTypes(cmd *cobra.Command, args []string) error for _, resourceType := range resourceTypes { properties := map[string]any{} - for _, property := range resourceType.Properties { - properties[property.Name] = map[string]any{ - "type": property.Type, - "deployTime": property.DeployTime, - "configurationDisabled": property.ConfigurationDisabled, - "required": property.Required, - } - if property.Properties != nil { - properties[property.Name].(map[string]any)["properties"] = map[string]any{} - addSubProperties(properties[property.Name].(map[string]any)["properties"].(map[string]any), property.Properties) - } - } + addSubProperties(properties, resourceType.Properties) typeAndClassifications[resourceType.QualifiedTypeName] = resourceInfo{ Classifications: resourceType.Classification.Is, Properties: properties, diff --git a/pkg/engine2/operational_eval/dependency_capture.go b/pkg/engine2/operational_eval/dependency_capture.go index ccbf241b6..564ec71b2 100644 --- a/pkg/engine2/operational_eval/dependency_capture.go +++ b/pkg/engine2/operational_eval/dependency_capture.go @@ -165,11 +165,40 @@ func (ctx *fauxConfigContext) ExecuteOpRule( exec(opRule.If) } for _, step := range opRule.Steps { - for _, stepRes := range step.Resources { - exec(stepRes.Selector) - for _, propValue := range stepRes.Properties { - exec(propValue) - } + errs = errors.Join(errs, ctx.executeOpStep(data, step)) + } + return errs +} + +func (ctx *fauxConfigContext) ExecutePropertyRule( + data knowledgebase.DynamicValueData, + propRule knowledgebase.PropertyRule, +) error { + var errs error + exec := func(v any) { + errs = errors.Join(errs, ctx.Execute(v, data)) + } + exec(propRule.If) + if propRule.Value != nil { + ctx.ExecuteValue(propRule.Value, data) + } + errs = errors.Join(errs, ctx.executeOpStep(data, propRule.Step)) + + return errs +} + +func (ctx *fauxConfigContext) executeOpStep( + data knowledgebase.DynamicValueData, + step knowledgebase.OperationalStep, +) error { + var errs error + exec := func(v any) { + errs = errors.Join(errs, ctx.Execute(v, data)) + } + for _, stepRes := range step.Resources { + exec(stepRes.Selector) + for _, propValue := range stepRes.Properties { + exec(propValue) } } return errs @@ -251,14 +280,7 @@ func emptyValue(tmpl *knowledgebase.ResourceTemplate, property string) (any, err if prop == nil { return nil, fmt.Errorf("could not find property %s on template %s", property, tmpl.Id()) } - ptype, err := prop.PropertyType() - if err != nil { - return nil, fmt.Errorf( - "could not get property type for property %s on template %s: %w", - property, tmpl.Id(), err, - ) - } - return ptype.ZeroValue(), nil + return prop.ZeroValue(), nil } func (ctx *fauxConfigContext) HasUpstream(selector any, resource construct.ResourceId) (bool, error) { diff --git a/pkg/engine2/operational_eval/graph.go b/pkg/engine2/operational_eval/graph.go index 76ae22969..66dcdad2c 100644 --- a/pkg/engine2/operational_eval/graph.go +++ b/pkg/engine2/operational_eval/graph.go @@ -144,10 +144,9 @@ func (eval *Evaluator) resourceVertices( ) (graphChanges, error) { changes := newChanges() var errs error - - addProp := func(prop *knowledgebase.Property) error { + addProp := func(prop knowledgebase.Property) error { vertex := &propertyVertex{ - Ref: construct.PropertyRef{Resource: res.ID, Property: prop.Path}, + Ref: construct.PropertyRef{Resource: res.ID, Property: prop.Details().Path}, Template: prop, EdgeRules: make(map[construct.SimpleEdge][]knowledgebase.OperationalRule), } diff --git a/pkg/engine2/operational_eval/vertex_path_expand.go b/pkg/engine2/operational_eval/vertex_path_expand.go index 0dff7e355..b1591cb81 100644 --- a/pkg/engine2/operational_eval/vertex_path_expand.go +++ b/pkg/engine2/operational_eval/vertex_path_expand.go @@ -246,45 +246,29 @@ func (v *pathExpandVertex) addDepsFromProps( } var errs error for k, prop := range tmpl.Properties { - pt, err := prop.PropertyType() - if err != nil { - errs = errors.Join(errs, err) - continue - } - if prop.OperationalRule == nil { + details := prop.Details() + if details.OperationalRule == nil { // If the property can't create resources, skip it. continue } - ready, err := operational_rule.EvaluateIfCondition(*prop.OperationalRule, + ready, err := operational_rule.EvaluateIfCondition(details.OperationalRule.If, eval.Solution, knowledgebase.DynamicValueData{Resource: res}) if err != nil || !ready { continue } - resType, ok := pt.(*knowledgebase.ResourcePropertyType) - if !ok { - listType, ok := pt.(*knowledgebase.ListPropertyType) - if !ok || listType.Value == "" { + ref := construct.PropertyRef{Resource: res, Property: k} + for _, dep := range dependencies { + if dep == v.Edge.Source || dep == v.Edge.Target { continue } - pType := knowledgebase.Property{Type: listType.Value} - pt, err = pType.PropertyType() + resource, err := eval.Solution.RawView().Vertex(res) if err != nil { errs = errors.Join(errs, err) continue } - resType, ok = pt.(*knowledgebase.ResourcePropertyType) - if !ok { - continue - } - } - - ref := construct.PropertyRef{Resource: res, Property: k} - for _, dep := range dependencies { - if dep == v.Edge.Source || dep == v.Edge.Target { - continue - } - if resType.Value.Matches(dep) { + // if this dependency could pass validation for the resources property, consider it as a dependent vertex + if err := prop.Validate(resource, dep); err == nil { changes.addEdge(v.Key(), Key{Ref: ref}) } } diff --git a/pkg/engine2/operational_eval/vertex_property.go b/pkg/engine2/operational_eval/vertex_property.go index 52bfdc59d..aa98c2e37 100644 --- a/pkg/engine2/operational_eval/vertex_property.go +++ b/pkg/engine2/operational_eval/vertex_property.go @@ -16,7 +16,7 @@ type ( propertyVertex struct { Ref construct.PropertyRef - Template *knowledgebase.Property + Template knowledgebase.Property EdgeRules map[construct.SimpleEdge][]knowledgebase.OperationalRule } ) @@ -35,21 +35,21 @@ func (prop *propertyVertex) Dependencies(eval *Evaluator) (graphChanges, error) // Template can be nil when checking for dependencies from a propertyVertex when adding an edge template if prop.Template != nil { - propCtx.ExecuteValue(prop.Template.DefaultValue, resData) - - if opRule := prop.Template.OperationalRule; opRule != nil { - if err := propCtx.ExecuteOpRule(resData, *opRule); err != nil { + _, _ = prop.Template.GetDefaultValue(propCtx, resData) + details := prop.Template.Details() + if opRule := details.OperationalRule; opRule != nil { + if err := propCtx.ExecutePropertyRule(resData, *opRule); err != nil { return changes, fmt.Errorf("could not execute resource operational rule for %s: %w", prop.Ref, err) } } - if !prop.Template.Namespace { + if !details.Namespace { tmpl, err := kb.GetResourceTemplate(prop.Ref.Resource) if err != nil { return changes, fmt.Errorf("could not get resource template for %s: %w", prop.Ref.Resource, err) } for propKey, propTmpl := range tmpl.Properties { - if propTmpl.Namespace { + if propTmpl.Details().Namespace { nsRef := construct.PropertyRef{Resource: prop.Ref.Resource, Property: propKey} propCtx.addRef(nsRef) } @@ -130,8 +130,8 @@ func (v *propertyVertex) Evaluate(eval *Evaluator) error { if err := eval.UpdateId(v.Ref.Resource, res.ID); err != nil { return err } - - if strings.HasPrefix(v.Template.Type, "list") || strings.HasPrefix(v.Template.Type, "set") { + propertyType := v.Template.Type() + if strings.HasPrefix(propertyType, "list") || strings.HasPrefix(propertyType, "set") { // If we have modified a list or set we want to re add the resource to be evaluated // so the nested fields are ensured to be set if required return eval.AddResources(res) @@ -160,11 +160,19 @@ func (v *propertyVertex) evaluateConstraints(sol solution_context.SolutionContex return fmt.Errorf("could not get current value for %s: %w", v.Ref, err) } - if currentValue == nil && setConstraint.Operator == "" && v.Template != nil && v.Template.DefaultValue != nil { + ctx := solution_context.DynamicCtx(sol) + var defaultVal any + if currentValue == nil { + defaultVal, err = v.Template.GetDefaultValue(ctx, dynData) + if err != nil { + return fmt.Errorf("could not get default value for %s: %w", v.Ref, err) + } + } + if currentValue == nil && setConstraint.Operator == "" && v.Template != nil && defaultVal != nil { err = solution_context.ConfigureResource( sol, res, - knowledgebase.Configuration{Field: v.Ref.Property, Value: v.Template.DefaultValue}, + knowledgebase.Configuration{Field: v.Ref.Property, Value: defaultVal}, dynData, "set", ) @@ -182,12 +190,6 @@ func (v *propertyVertex) evaluateConstraints(sol solution_context.SolutionContex if property == nil { return fmt.Errorf("could not get property %s from resource %s", v.Ref.Property, res.ID) } - switch setConstraint.Operator { - case constraints.AddConstraintOperator, constraints.RemoveConstraintOperator: - if property.IsPropertyTypeScalar() { - return fmt.Errorf("cannot add/remove to scalar property %s", v.Ref) - } - } err = solution_context.ConfigureResource( sol, res, @@ -228,7 +230,7 @@ func (v *propertyVertex) evaluateConstraints(sol solution_context.SolutionContex } func (v *propertyVertex) evaluateResourceOperational(sol solution_context.SolutionContext, res *construct.Resource) error { - if v.Template == nil || v.Template.OperationalRule == nil { + if v.Template == nil || v.Template.Details().OperationalRule == nil { return nil } @@ -238,7 +240,7 @@ func (v *propertyVertex) evaluateResourceOperational(sol solution_context.Soluti Data: knowledgebase.DynamicValueData{Resource: res.ID}, } - err := opCtx.HandleOperationalRule(*v.Template.OperationalRule) + err := opCtx.HandlePropertyRule(*v.Template.Details().OperationalRule) if err != nil { return fmt.Errorf("could not apply operational rule for %s: %w", v.Ref, err) } @@ -282,21 +284,26 @@ func (v *propertyVertex) Ready(eval *Evaluator) (ReadyPriority, error) { // wait until we have a template return NotReadyMax, nil } - if v.Template.OperationalRule != nil { + if v.Template.Details().OperationalRule != nil { // operational rules should run as soon as possible to create any resources they need return ReadyNow, nil } - if strings.Contains(v.Template.Type, "list") || strings.Contains(v.Template.Type, "set") { + ptype := v.Template.Type() + if strings.HasPrefix(ptype, "list") || strings.HasPrefix(ptype, "set") { // never sure when a list/set is ready - it'll just be appended to by edges through // `v.EdgeRules` return NotReadyHigh, nil } - if strings.Contains(v.Template.Type, "map") && len(v.Template.Properties) == 0 { + if strings.HasPrefix(ptype, "map") && len(v.Template.SubProperties()) == 0 { // maps without sub-properties (ie, not objects) are also appended to by edges return NotReadyHigh, nil } // properties that have values set via edge rules dont' have default values - if v.Template.DefaultValue != nil { + defaultVal, err := v.Template.GetDefaultValue(solution_context.DynamicCtx(eval.Solution), knowledgebase.DynamicValueData{Resource: v.Ref.Resource}) + if err != nil { + return NotReadyMid, nil + } + if defaultVal != nil { return ReadyNow, nil } // for non-list/set types, once an edge is here to set the value, it can be run diff --git a/pkg/engine2/operational_rule/operational_action.go b/pkg/engine2/operational_rule/operational_action.go index 6bc7bf6cd..295798b56 100644 --- a/pkg/engine2/operational_rule/operational_action.go +++ b/pkg/engine2/operational_rule/operational_action.go @@ -194,7 +194,7 @@ func (action *operationalResourceAction) useAvailableResources(resource *constru if err != nil { return err } - if tmpl.GetProperty(k).Namespace { + if tmpl.GetProperty(k).Details().Namespace { oldId := res.ID res.ID.Namespace = resource.ID.Namespace err := action.ruleCtx.Solution.OperationalView().UpdateResourceID(oldId, res.ID) @@ -230,22 +230,23 @@ func (action *operationalResourceAction) useAvailableResources(resource *constru if edgeTmpl == nil { continue } - if edgeTmpl.Unique == (knowledgebase.Unique{}) { + + if !edgeTmpl.Unique.Target || !edgeTmpl.Unique.Source { // many-to-many is okay availableResources.Add(res) continue } switch action.Step.Direction { case knowledgebase.DirectionDownstream: - if !edgeTmpl.Unique.Target { - // one-to-many is okay + if !edgeTmpl.Unique.Source { + // many-to-one is okay availableResources.Add(res) continue } case knowledgebase.DirectionUpstream: - if !edgeTmpl.Unique.Source { - // many-to-one are okay + if !edgeTmpl.Unique.Target { + // one-to-many are okay availableResources.Add(res) continue } diff --git a/pkg/engine2/operational_rule/operational_configuration.go b/pkg/engine2/operational_rule/operational_configuration.go index bc67f069d..a4a880854 100644 --- a/pkg/engine2/operational_rule/operational_configuration.go +++ b/pkg/engine2/operational_rule/operational_configuration.go @@ -2,7 +2,6 @@ package operational_rule import ( "fmt" - "strings" "github.com/klothoplatform/klotho/pkg/engine2/solution_context" knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" @@ -18,18 +17,6 @@ func (ctx OperationalRuleContext) HandleConfigurationRule(config knowledgebase.C if err != nil { return fmt.Errorf("resource %s not found: %w", res, err) } - val, err := resource.GetProperty(config.Config.Field) - action := "set" - if err == nil && val != nil { - resTempalte, err := ctx.Solution.KnowledgeBase().GetResourceTemplate(resource.ID) - if err != nil { - return err - } - prop := resTempalte.GetProperty(config.Config.Field) - if prop != nil && (strings.HasPrefix(prop.Type, "list") || strings.HasPrefix(prop.Type, "set") || strings.HasPrefix(prop.Type, "map")) { - action = "add" - } - } resolvedField := config.Config.Field err = dyn.ExecuteDecode(config.Config.Field, ctx.Data, &resolvedField) @@ -38,7 +25,7 @@ func (ctx OperationalRuleContext) HandleConfigurationRule(config knowledgebase.C } config.Config.Field = resolvedField - err = solution_context.ConfigureResource(ctx.Solution, resource, config.Config, ctx.Data, action) + err = solution_context.ConfigureResource(ctx.Solution, resource, config.Config, ctx.Data, "add") if err != nil { return err } diff --git a/pkg/engine2/operational_rule/operational_rule.go b/pkg/engine2/operational_rule/operational_rule.go index 53e9179ca..e98b1a0aa 100644 --- a/pkg/engine2/operational_rule/operational_rule.go +++ b/pkg/engine2/operational_rule/operational_rule.go @@ -16,13 +16,13 @@ import ( type ( OperationalRuleContext struct { Solution solution_context.SolutionContext - Property *knowledgebase.Property + Property knowledgebase.Property Data knowledgebase.DynamicValueData } ) func (ctx OperationalRuleContext) HandleOperationalRule(rule knowledgebase.OperationalRule) error { - shouldRun, err := EvaluateIfCondition(rule, ctx.Solution, ctx.Data) + shouldRun, err := EvaluateIfCondition(rule.If, ctx.Solution, ctx.Data) if err != nil { return err } @@ -30,13 +30,6 @@ func (ctx OperationalRuleContext) HandleOperationalRule(rule knowledgebase.Opera return nil } - if ctx.Property != nil && len(rule.Steps) > 0 { - err := ctx.CleanProperty(rule) - if err != nil { - return err - } - } - var errs error for i, operationalStep := range rule.Steps { err := ctx.HandleOperationalStep(operationalStep) @@ -56,20 +49,70 @@ func (ctx OperationalRuleContext) HandleOperationalRule(rule knowledgebase.Opera return errs } +func (ctx OperationalRuleContext) HandlePropertyRule(rule knowledgebase.PropertyRule) error { + if ctx.Property == nil { + return fmt.Errorf("property rule has no property") + } + if ctx.Data.Resource.IsZero() { + return fmt.Errorf("property rule has no resource") + } + + shouldRun, err := EvaluateIfCondition(rule.If, ctx.Solution, ctx.Data) + if err != nil { + return err + } + if !shouldRun { + return nil + } + + if ctx.Property != nil && len(rule.Step.Resources) > 0 { + err := ctx.CleanProperty(rule.Step) + if err != nil { + return err + } + } + + var errs error + if len(rule.Step.Resources) > 0 { + err = ctx.HandleOperationalStep(rule.Step) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("could not apply step: %w", err)) + } + } + + if rule.Value != nil { + dynctx := solution_context.DynamicCtx(ctx.Solution) + val, err := ctx.Property.Parse(rule.Value, dynctx, ctx.Data) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("could not parse value %s: %w", rule.Value, err)) + } + resource, err := ctx.Solution.RawView().Vertex(ctx.Data.Resource) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("could not get resource %s: %w", ctx.Data.Resource, err)) + } else { + err = ctx.Property.SetProperty(resource, val) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("could not set property %s: %w", ctx.Property, err)) + } + } + } + return errs +} + // CleanProperty clears the property associated with the rule if it no longer matches the rule. // For array properties, each element must match at least one step selector and non-matching // elements will be removed. -func (ctx OperationalRuleContext) CleanProperty(rule knowledgebase.OperationalRule) error { +func (ctx OperationalRuleContext) CleanProperty(step knowledgebase.OperationalStep) error { log := zap.L().With( zap.String("op", "op_rule"), - zap.String("property", ctx.Property.Path), + zap.String("property", ctx.Property.Details().Path), zap.String("resource", ctx.Data.Resource.String()), ).Sugar() resource, err := ctx.Solution.RawView().Vertex(ctx.Data.Resource) if err != nil { return err } - path, err := resource.PropertyPath(ctx.Property.Path) + path, err := resource.PropertyPath(ctx.Property.Details().Path) if err != nil { return err } @@ -83,17 +126,16 @@ func (ctx OperationalRuleContext) CleanProperty(rule knowledgebase.OperationalRu if err != nil { return false, err } - for _, step := range rule.Steps { - for i, sel := range step.Resources { - match, err := sel.IsMatch(solution_context.DynamicCtx(ctx.Solution), ctx.Data, propRes) - if err != nil { - return false, fmt.Errorf("error checking if %s matches selector %d: %w", prop, i, err) - } - if match { - return true, nil - } + for i, sel := range step.Resources { + match, err := sel.IsMatch(solution_context.DynamicCtx(ctx.Solution), ctx.Data, propRes) + if err != nil { + return false, fmt.Errorf("error checking if %s matches selector %d: %w", prop, i, err) + } + if match { + return true, nil } } + return false, nil } @@ -208,16 +250,16 @@ func (ctx OperationalRuleContext) CleanProperty(rule knowledgebase.OperationalRu } func EvaluateIfCondition( - rule knowledgebase.OperationalRule, + tmplString string, sol solution_context.SolutionContext, data knowledgebase.DynamicValueData, ) (bool, error) { - if rule.If == "" { + if tmplString == "" { return true, nil } result := false dyn := solution_context.DynamicCtx(sol) - err := dyn.ExecuteDecode(rule.If, data, &result) + err := dyn.ExecuteDecode(tmplString, data, &result) if err != nil { return false, err } diff --git a/pkg/engine2/operational_rule/operational_step.go b/pkg/engine2/operational_rule/operational_step.go index 57083ee4f..72948a260 100644 --- a/pkg/engine2/operational_rule/operational_step.go +++ b/pkg/engine2/operational_rule/operational_step.go @@ -39,7 +39,7 @@ func (ctx OperationalRuleContext) HandleOperationalStep(step knowledgebase.Opera var ids []construct.ResourceId if ctx.Property != nil { var err error - ids, err = ctx.addDependenciesFromProperty(step, resource, ctx.Property.Path) + ids, err = ctx.addDependenciesFromProperty(step, resource, ctx.Property.Details().Path) if err != nil { return err } @@ -254,65 +254,63 @@ func (ctx OperationalRuleContext) SetField(resource, fieldResource *construct.Re if step.UsePropertyRef != "" { propertyValue = construct.PropertyRef{Resource: fieldResource.ID, Property: step.UsePropertyRef} } + path := ctx.Property.Details().Path - if ctx.Property.IsPropertyTypeScalar() { - res, err := resource.GetProperty(ctx.Property.Path) + removeResource := func(currResId construct.ResourceId) error { + err := ctx.removeDependencyForDirection(step.Direction, resource.ID, currResId) if err != nil { - zap.S().Debugf("property %s not found on resource %s", ctx.Property.Path, resource.ID) - } - // If the current field is a resource id we will compare it against the one passed in to see if we need to remove the current resource - if currResId, ok := res.(construct.ResourceId); ok && !currResId.IsZero() { - if res != fieldResource.ID { - err = ctx.removeDependencyForDirection(step.Direction, resource.ID, currResId) - if err != nil { - return err - } - zap.S().Infof("Removing old field value for '%s' (%s) for %s", ctx.Property.Path, res, fieldResource.ID) - // Remove the old field value if it's unused - err = reconciler.RemoveResource(ctx.Solution, currResId, false) - if err != nil { - return err - } - } + return err } - - // Right now we only enforce the top level properties if they have rules, so we can assume the path is equal to the name of the property - err = resource.SetProperty(ctx.Property.Path, propertyValue) + zap.S().Infof("Removing old field value for '%s' (%s) for %s", path, currResId, fieldResource.ID) + // Remove the old field value if it's unused + err = reconciler.RemoveResource(ctx.Solution, currResId, false) if err != nil { - return fmt.Errorf("error setting field %s#%s with %s: %w", resource.ID, ctx.Property.Path, fieldResource.ID, err) + return err } - zap.S().Debugf("set field %s#%s to %s", resource.ID, ctx.Property.Path, fieldResource.ID) - // See if we need to namespace the resource due to setting the property - if ctx.Property.Namespace { - resource.ID.Namespace = fieldResource.ID.Name + return nil + } + + propVal, err := resource.GetProperty(path) + if err != nil { + zap.S().Debugf("property %s not found on resource %s", path, resource.ID) + } + switch val := propVal.(type) { + case construct.ResourceId: + if val != fieldResource.ID { + err = removeResource(val) } - } else { - // First lets check if the array already contains the id. if it does we dont want to append it - res, err := resource.GetProperty(ctx.Property.Path) - if err != nil { - zap.S().Debugf("property %s not found on resource %s", ctx.Property.Path, resource.ID) + case construct.PropertyRef: + if val.Resource != fieldResource.ID { + err = removeResource(val.Resource) } - resVal := reflect.ValueOf(res) - if resVal.IsValid() && (resVal.Kind() == reflect.Slice || resVal.Kind() == reflect.Array) { - // If the current field is a resource id we will compare it against the one passed in to see if we need to remove the current resource - for i := 0; i < resVal.Len(); i++ { - currResId, ok := resVal.Index(i).Interface().(construct.ResourceId) - if !ok { - continue - } - if !currResId.IsZero() && currResId == fieldResource.ID { - return nil - } + } + if err != nil { + return err + } + resVal := reflect.ValueOf(propVal) + if resVal.IsValid() && (resVal.Kind() == reflect.Slice || resVal.Kind() == reflect.Array) { + // If the current field is a resource id we will compare it against the one passed in to see if we need to remove the current resource + for i := 0; i < resVal.Len(); i++ { + currResId, ok := resVal.Index(i).Interface().(construct.ResourceId) + if !ok { + continue + } + if !currResId.IsZero() && currResId == fieldResource.ID { + return nil } } - // Right now we only enforce the top level properties if they have rules, so we can assume the path is equal to the name of the property - err = resource.AppendProperty(ctx.Property.Path, propertyValue) - if err != nil { - return fmt.Errorf("error appending field %s#%s with %s: %w", resource.ID, ctx.Property.Path, fieldResource.ID, err) - } - zap.S().Infof("appended field %s#%s with %s", resource.ID, ctx.Property.Path, fieldResource.ID) + } + // Right now we only enforce the top level properties if they have rules, so we can assume the path is equal to the name of the property + err = ctx.Property.AppendProperty(resource, propertyValue) + if err != nil { + return fmt.Errorf("error appending field %s#%s with %s: %w", resource.ID, path, fieldResource.ID, err) + } + zap.S().Infof("appended field %s#%s with %s", resource.ID, path, fieldResource.ID) + if ctx.Property.Details().Namespace { + resource.ID.Namespace = fieldResource.ID.Name } + // updated the rule context ids if they have changed if ctx.Data.Resource.Matches(oldId) { ctx.Data.Resource = resource.ID } diff --git a/pkg/engine2/operational_rule/operational_step_test.go b/pkg/engine2/operational_rule/operational_step_test.go index a505b7cef..8c41694c3 100644 --- a/pkg/engine2/operational_rule/operational_step_test.go +++ b/pkg/engine2/operational_rule/operational_step_test.go @@ -8,6 +8,7 @@ import ( "github.com/klothoplatform/klotho/pkg/construct2/graphtest" "github.com/klothoplatform/klotho/pkg/engine2/enginetesting" knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/klothoplatform/klotho/pkg/knowledge_base2/properties" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -398,7 +399,7 @@ func Test_setField(t *testing.T) { name string resource *construct.Resource resourceToSet *construct.Resource - property *knowledgebase.Property + property knowledgebase.Property initialState []any step knowledgebase.OperationalStep wantResource *construct.Resource @@ -413,9 +414,15 @@ func Test_setField(t *testing.T) { }, }, resourceToSet: res4, - property: &knowledgebase.Property{Name: "Res4", Namespace: true, Path: "Res4", Type: "resource"}, - step: knowledgebase.OperationalStep{Direction: knowledgebase.DirectionDownstream}, - initialState: []any{"mock:resource4:test4", "mock:resource1:test1", "mock:resource1:test1 -> mock:resource4:thisWillBeReplaced"}, + property: &properties.ResourceProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Res4", + Path: "Res4", + Namespace: true, + }, + }, + step: knowledgebase.OperationalStep{Direction: knowledgebase.DirectionDownstream}, + initialState: []any{"mock:resource4:test4", "mock:resource1:test1", "mock:resource1:test1 -> mock:resource4:thisWillBeReplaced"}, wantResource: &construct.Resource{ ID: construct.ResourceId{Provider: "mock", Type: "resource1", Namespace: res4.ID.Name, Name: "test1"}, Properties: map[string]interface{}{ @@ -436,9 +443,15 @@ func Test_setField(t *testing.T) { }, }, resourceToSet: res4, - property: &knowledgebase.Property{Name: "Res4", Namespace: true, Path: "Res4", Type: "resource"}, - step: knowledgebase.OperationalStep{Direction: knowledgebase.DirectionUpstream}, - initialState: []any{"mock:resource4:test4", "mock:resource1:test1", "mock:resource4:thisWillBeReplaced -> mock:resource1:test1"}, + property: &properties.ResourceProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Res4", + Path: "Res4", + Namespace: true, + }, + }, + step: knowledgebase.OperationalStep{Direction: knowledgebase.DirectionUpstream}, + initialState: []any{"mock:resource4:test4", "mock:resource1:test1", "mock:resource4:thisWillBeReplaced -> mock:resource1:test1"}, wantResource: &construct.Resource{ ID: construct.ResourceId{Provider: "mock", Type: "resource1", Namespace: res4.ID.Name, Name: "test1"}, Properties: map[string]interface{}{ @@ -457,9 +470,15 @@ func Test_setField(t *testing.T) { Properties: make(map[string]interface{}), }, resourceToSet: MockResource4("test4"), - property: &knowledgebase.Property{Name: "Res4", Namespace: true, Path: "Res4", Type: "resource"}, - step: knowledgebase.OperationalStep{Direction: knowledgebase.DirectionUpstream}, - initialState: []any{"mock:resource1:test1", "mock:resource4:test4", "mock:resource4:test4 -> mock:resource1:test1"}, + property: &properties.ResourceProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Res4", + Path: "Res4", + Namespace: true, + }, + }, + step: knowledgebase.OperationalStep{Direction: knowledgebase.DirectionUpstream}, + initialState: []any{"mock:resource1:test1", "mock:resource4:test4", "mock:resource4:test4 -> mock:resource1:test1"}, wantResource: &construct.Resource{ ID: construct.ResourceId{Provider: "mock", Type: "resource1", Namespace: res4.ID.Name, Name: "test1"}, Properties: map[string]interface{}{ @@ -481,8 +500,13 @@ func Test_setField(t *testing.T) { }, initialState: []any{"mock:resource4:test4", "mock:resource1:test1", "mock:resource4:test4 -> mock:resource1:test1"}, resourceToSet: res4, - property: &knowledgebase.Property{Name: "Res4", Path: "Res4", Type: "resource"}, - step: knowledgebase.OperationalStep{Direction: knowledgebase.DirectionUpstream}, + property: &properties.ResourceProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Res4", + Path: "Res4", + }, + }, + step: knowledgebase.OperationalStep{Direction: knowledgebase.DirectionUpstream}, wantResource: &construct.Resource{ ID: construct.ResourceId{Provider: "mock", Type: "resource1", Name: "test1"}, Properties: map[string]interface{}{ @@ -503,9 +527,15 @@ func Test_setField(t *testing.T) { }, }, resourceToSet: MockResource2("test2"), - property: &knowledgebase.Property{Name: "Res2s", Type: "list(resource)", Path: "Res2s"}, - step: knowledgebase.OperationalStep{Direction: knowledgebase.DirectionUpstream}, - initialState: []any{"mock:resource2:test", "mock:resource1:test1", "mock:resource2:test -> mock:resource1:test1"}, + property: &properties.ListProperty{ + ItemProperty: &properties.ResourceProperty{}, + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Res2s", + Path: "Res2s", + }, + }, + step: knowledgebase.OperationalStep{Direction: knowledgebase.DirectionUpstream}, + initialState: []any{"mock:resource2:test", "mock:resource1:test1", "mock:resource2:test -> mock:resource1:test1"}, wantResource: &construct.Resource{ ID: construct.ResourceId{Provider: "mock", Type: "resource1", Name: "test1"}, Properties: map[string]interface{}{ @@ -587,19 +617,24 @@ func MockResource4(name string) *construct.Resource { var resource1 = &knowledgebase.ResourceTemplate{ QualifiedTypeName: "mock:resource1", Properties: knowledgebase.Properties{ - "Name": { - Name: "Name", - Type: "string", - Namespace: false, + "Name": &properties.StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Name", + }, }, - "Res4": { - Name: "Res4", - Type: "resource", - Namespace: true, + "Res4": &properties.ResourceProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Res4", + Path: "Res4", + Namespace: true, + }, }, - "Res2s": { - Name: "Res2s", - Type: "list(resource)", + "Res2s": &properties.ListProperty{ + ItemProperty: &properties.ResourceProperty{}, + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Res2s", + Path: "Res2s", + }, }, }, Classification: knowledgebase.Classification{ @@ -611,10 +646,10 @@ var resource1 = &knowledgebase.ResourceTemplate{ var resource2 = &knowledgebase.ResourceTemplate{ QualifiedTypeName: "mock:resource2", Properties: knowledgebase.Properties{ - "Name": { - Name: "Name", - Type: "string", - Namespace: false, + "Name": &properties.StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Name", + }, }, }, Classification: knowledgebase.Classification{ @@ -626,10 +661,10 @@ var resource2 = &knowledgebase.ResourceTemplate{ var resource3 = &knowledgebase.ResourceTemplate{ QualifiedTypeName: "mock:resource3", Properties: knowledgebase.Properties{ - "Name": { - Name: "Name", - Type: "string", - Namespace: false, + "Name": &properties.StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Name", + }, }, }, Classification: knowledgebase.Classification{ @@ -641,10 +676,10 @@ var resource3 = &knowledgebase.ResourceTemplate{ var resource4 = &knowledgebase.ResourceTemplate{ QualifiedTypeName: "mock:resource4", Properties: knowledgebase.Properties{ - "Name": { - Name: "Name", - Type: "string", - Namespace: false, + "Name": &properties.StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Name: "Name", + }, }, }, Classification: knowledgebase.Classification{ diff --git a/pkg/engine2/path_selection/candidate_validity.go b/pkg/engine2/path_selection/candidate_validity.go index 2c294c9b5..f8b05b42f 100644 --- a/pkg/engine2/path_selection/candidate_validity.go +++ b/pkg/engine2/path_selection/candidate_validity.go @@ -270,11 +270,7 @@ func (d downstreamChecker) makeValid(resource, operationResource *construct.Reso continue } } - if p.IsPropertyTypeScalar() { - return true, errors.Join(errs, currRes.SetProperty(property, downstream)) - } else { - return true, errors.Join(errs, currRes.AppendProperty(property, downstream)) - } + return true, errors.Join(errs, p.AppendProperty(currRes, downstream)) } return false, errs } diff --git a/pkg/engine2/path_selection/edge_validity.go b/pkg/engine2/path_selection/edge_validity.go index 65b2bc3be..ac31b9ea0 100644 --- a/pkg/engine2/path_selection/edge_validity.go +++ b/pkg/engine2/path_selection/edge_validity.go @@ -72,48 +72,50 @@ func checkProperties(ctx solution_context.SolutionContext, resource, toCheck *co explicitlyNotValid := false explicitlyValid := false - err = template.LoopProperties(resource, func(prop *knowledgebase.Property) error { - if prop.OperationalRule == nil { + err = template.LoopProperties(resource, func(prop knowledgebase.Property) error { + details := prop.Details() + rule := details.OperationalRule + if rule == nil || len(rule.Step.Resources) == 0 { return nil } - for _, step := range prop.OperationalRule.Steps { - if !step.Unique || step.Direction != direction { + step := rule.Step + if !step.Unique || step.Direction != direction { + return nil + } + //check if the upstream resource is the same type as the matched resource type + for _, selector := range step.Resources { + match, err := selector.CanUse(solution_context.DynamicCtx(ctx), knowledgebase.DynamicValueData{Resource: resource.ID}, + toCheck) + if err != nil { + return fmt.Errorf("error checking if resource %s matches selector %s: %w", toCheck, selector.Selector, err) + } + // if its a match for the selectors, lets ensure that it has a dependency and exists in the properties of the rul + if !match { continue } - //check if the upstream resource is the same type as the matched resource type - for _, selector := range step.Resources { - match, err := selector.CanUse(solution_context.DynamicCtx(ctx), knowledgebase.DynamicValueData{Resource: resource.ID}, - toCheck) - if err != nil { - return fmt.Errorf("error checking if resource %s matches selector %s: %w", toCheck, selector, err) - } - // if its a match for the selectors, lets ensure that it has a dependency and exists in the properties of the rul - if !match { - continue + property, err := resource.GetProperty(details.Path) + if err != nil { + return fmt.Errorf("error getting property %s for resource %s: %w", details.Path, toCheck, err) + } + if property != nil { + if checkIfPropertyContainsResource(property, toCheck.ID) { + explicitlyValid = true + return knowledgebase.ErrStopWalk } - property, err := resource.GetProperty(prop.Path) + } else { + loneDep, err := checkIfLoneDependency(ctx, resource.ID, toCheck.ID, direction, selector) if err != nil { - return fmt.Errorf("error getting property %s for resource %s: %w", prop.Path, toCheck, err) + return err } - if property != nil { - if checkIfPropertyContainsResource(property, toCheck.ID) { - explicitlyValid = true - return knowledgebase.ErrStopWalk - } - } else { - loneDep, err := checkIfLoneDependency(ctx, resource.ID, toCheck.ID, direction, selector) - if err != nil { - return err - } - if loneDep { - explicitlyValid = true - return knowledgebase.ErrStopWalk - } + if loneDep { + explicitlyValid = true + return knowledgebase.ErrStopWalk } - explicitlyNotValid = true - return knowledgebase.ErrStopWalk } + explicitlyNotValid = true + return knowledgebase.ErrStopWalk } + return nil }) if err != nil { @@ -242,32 +244,34 @@ func checkIfCreatedAsUniqueValidity(ctx solution_context.SolutionContext, resour if err != nil { return false, err } - err = template.LoopProperties(currRes, func(prop *knowledgebase.Property) error { - if prop.OperationalRule == nil { + err = template.LoopProperties(currRes, func(prop knowledgebase.Property) error { + details := prop.Details() + rule := details.OperationalRule + if rule == nil || len(rule.Step.Resources) == 0 { + return nil + } + step := rule.Step + // we want the step to be the opposite of the direction passed in so we know its creating the resource in the direction of the resource + // since we are looking at the resources dependencies + if !step.Unique || step.Direction == direction { return nil } - for _, step := range prop.OperationalRule.Steps { - // we want the step to be the opposite of the direction passed in so we know its creating the resource in the direction of the resource - // since we are looking at the resources dependencies - if !step.Unique || step.Direction == direction { + //check if the upstream resource is the same type as the matched resource type + for _, selector := range step.Resources { + match, err := selector.CanUse(solution_context.DynamicCtx(ctx), knowledgebase.DynamicValueData{Resource: currRes.ID}, + resource) + if err != nil { + return fmt.Errorf("error checking if resource %s matches selector %s: %w", other, selector.Selector, err) + } + // if its a match for the selectors, lets ensure that it has a dependency and exists in the properties of the rul + if !match { continue } - //check if the upstream resource is the same type as the matched resource type - for _, selector := range step.Resources { - match, err := selector.CanUse(solution_context.DynamicCtx(ctx), knowledgebase.DynamicValueData{Resource: currRes.ID}, - resource) - if err != nil { - return fmt.Errorf("error checking if resource %s matches selector %s: %w", other, selector, err) - } - // if its a match for the selectors, lets ensure that it has a dependency and exists in the properties of the rul - if !match { - continue - } - foundMatch = true - return knowledgebase.ErrStopWalk - } + foundMatch = true + return knowledgebase.ErrStopWalk } + return nil }) if err != nil { diff --git a/pkg/engine2/path_selection/path_expansion.go b/pkg/engine2/path_selection/path_expansion.go index 2a7a35404..257ae9364 100644 --- a/pkg/engine2/path_selection/path_expansion.go +++ b/pkg/engine2/path_selection/path_expansion.go @@ -195,55 +195,54 @@ func handleProperties( if err != nil && !errors.Is(err, graph.ErrVertexNotFound) { return err } - handleProp := func(prop *knowledgebase.Property) error { + handleProp := func(prop knowledgebase.Property) error { oldId := res.ID opRuleCtx := operational_rule.OperationalRuleContext{ Solution: ctx, Property: prop, Data: knowledgebase.DynamicValueData{Resource: res.ID}, } - if prop.OperationalRule == nil { + details := prop.Details() + if details.OperationalRule == nil || len(details.OperationalRule.Step.Resources) == 0 { return nil } - for _, step := range prop.OperationalRule.Steps { - for _, selector := range step.Resources { - if step.Direction == knowledgebase.DirectionDownstream && i < len(resultResources)-1 { - downstreamRes := resultResources[i+1] - canUse, err := selector.CanUse( - solution_context.DynamicCtx(ctx), - knowledgebase.DynamicValueData{Resource: res.ID}, - downstreamRes, - ) - if canUse && err == nil { - err = opRuleCtx.SetField(res, downstreamRes, step) - if err != nil { - errs = errors.Join(errs, err) - } - if prop.Namespace && exists != nil { - errs = errors.Join(errs, construct.PropagateUpdatedId(ctx.OperationalView(), oldId)) - } + step := details.OperationalRule.Step + for _, selector := range step.Resources { + if step.Direction == knowledgebase.DirectionDownstream && i < len(resultResources)-1 { + downstreamRes := resultResources[i+1] + canUse, err := selector.CanUse( + solution_context.DynamicCtx(ctx), + knowledgebase.DynamicValueData{Resource: res.ID}, + downstreamRes, + ) + if canUse && err == nil { + err = opRuleCtx.SetField(res, downstreamRes, step) + if err != nil { + errs = errors.Join(errs, err) + } + if details.Namespace && exists != nil { + errs = errors.Join(errs, construct.PropagateUpdatedId(ctx.OperationalView(), oldId)) } - } else if i > 0 { - upstreamRes := resultResources[i-1] - canUse, err := selector.CanUse( - solution_context.DynamicCtx(ctx), - knowledgebase.DynamicValueData{Resource: res.ID}, - upstreamRes, - ) - if canUse && err == nil { - err = opRuleCtx.SetField(res, upstreamRes, step) + } + } else if i > 0 { + upstreamRes := resultResources[i-1] + if canUse, err := selector.CanUse(solution_context.DynamicCtx(ctx), + knowledgebase.DynamicValueData{Resource: res.ID}, upstreamRes); canUse && err == nil { + err = opRuleCtx.SetField(res, upstreamRes, step) + if err != nil { + errs = errors.Join(errs, err) + } + if details.Namespace && exists != nil { + err = construct.PropagateUpdatedId(ctx.OperationalView(), id) if err != nil { errs = errors.Join(errs, err) } - if prop.Namespace && exists != nil { - errs = errors.Join(errs, construct.PropagateUpdatedId(ctx.OperationalView(), id)) - } } - } + } } - if prop.Namespace && oldId != res.ID { + if details.Namespace && oldId != res.ID { errs = errors.Join(errs, graph_addons.ReplaceVertex(g, oldId, res, construct.ResourceHasher)) _, props, err := tempGraph.VertexWithProperties(oldId) if err == nil && props.Attributes != nil { diff --git a/pkg/engine2/solution_context/decisions.go b/pkg/engine2/solution_context/decisions.go index 51d04b472..1fa5c241b 100644 --- a/pkg/engine2/solution_context/decisions.go +++ b/pkg/engine2/solution_context/decisions.go @@ -1,6 +1,9 @@ package solution_context -import construct "github.com/klothoplatform/klotho/pkg/construct2" +import ( + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) type ( KV struct { @@ -46,6 +49,11 @@ type ( Property string Value any } + + ResourceConfigurationError struct { + Resource construct.ResourceId + Property knowledgebase.Property + } ) func (d AddResourceDecision) internal() {} diff --git a/pkg/engine2/solution_context/resource_configuration.go b/pkg/engine2/solution_context/resource_configuration.go index 4402cb4cf..6f323f796 100644 --- a/pkg/engine2/solution_context/resource_configuration.go +++ b/pkg/engine2/solution_context/resource_configuration.go @@ -25,7 +25,14 @@ func ConfigureResource( return fmt.Errorf("data resource (%s) does not match configuring resource (%s)", data.Resource, resource.ID) } field := configuration.Field - + rt, err := ctx.KnowledgeBase().GetResourceTemplate(resource.ID) + if err != nil { + return err + } + property := rt.GetProperty(field) + if property == nil { + return fmt.Errorf("failed to get property %s on resource %s: %w", field, resource.ID, err) + } val, err := knowledgebase.TransformToPropertyValue( resource.ID, field, @@ -39,17 +46,17 @@ func ConfigureResource( switch action { case "set": - err = resource.SetProperty(field, val) + err = property.SetProperty(resource, val) if err != nil { return fmt.Errorf("failed to set property %s on resource %s: %w", field, resource.ID, err) } case "add": - err = resource.AppendProperty(field, val) + err = property.AppendProperty(resource, val) if err != nil { return fmt.Errorf("failed to add property %s on resource %s: %w", field, resource.ID, err) } case "remove": - err = resource.RemoveProperty(field, val) + err = property.RemoveProperty(resource, val) if err != nil { return fmt.Errorf("failed to remove property %s on resource %s: %w", field, resource.ID, err) } diff --git a/pkg/engine2/testdata/2_routes.expect.yaml b/pkg/engine2/testdata/2_routes.expect.yaml index 89a2ef1c7..5489d9bc5 100755 --- a/pkg/engine2/testdata/2_routes.expect.yaml +++ b/pkg/engine2/testdata/2_routes.expect.yaml @@ -49,8 +49,8 @@ resources: aws:api_integration:rest_api_1:integ0: IntegrationHttpMethod: POST Method: aws:api_method:rest_api_1:api_method-0 - RequestParameters: {} Resource: aws:api_resource:rest_api_1:api_resource-0 + RequestParameters: {} RestApi: aws:rest_api:rest_api_1 Route: /lambda0/api Target: aws:lambda_function:lambda_function_0 diff --git a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml index 496e888be..f2bc779f3 100755 --- a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml +++ b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml @@ -12,6 +12,6 @@ resources: parent: vpc/vpc-0 ecs_service/ecs_service_0 -> rds_instance/rds-instance-2: - path: aws:ecs_task_definition:ecs_service_0,aws:iam_role:ecs_service_0-execution-role + path: aws:ecs_task_definition:ecs_service_0,aws:iam_role:ecs_service_0-rds-instance-2 diff --git a/pkg/engine2/testdata/ecs_rds.expect.yaml b/pkg/engine2/testdata/ecs_rds.expect.yaml index 2e269ea2a..fd92146d1 100755 --- a/pkg/engine2/testdata/ecs_rds.expect.yaml +++ b/pkg/engine2/testdata/ecs_rds.expect.yaml @@ -34,7 +34,7 @@ resources: rds-instance-2_RDS_ENDPOINT: aws:rds_instance:rds-instance-2#Endpoint rds-instance-2_RDS_PASSWORD: aws:rds_instance:rds-instance-2#Password rds-instance-2_RDS_USERNAME: aws:rds_instance:rds-instance-2#Username - ExecutionRole: aws:iam_role:ecs_service_0-execution-role + ExecutionRole: aws:iam_role:ecs_service_0-rds-instance-2 Image: aws:ecr_image:ecs_service_0-image LogGroup: aws:log_group:ecs_service_0-log-group Memory: "512" @@ -46,12 +46,12 @@ resources: Region: aws:region:region-0 RequiresCompatibilities: - FARGATE - TaskRole: aws:iam_role:ecs_service_0-execution-role + TaskRole: aws:iam_role:ecs_service_0-rds-instance-2 aws:ecr_image:ecs_service_0-image: Context: . Dockerfile: ecs_service_0-image.Dockerfile Repo: aws:ecr_repo:ecr_repo-0 - aws:iam_role:ecs_service_0-execution-role: + aws:iam_role:ecs_service_0-rds-instance-2: AssumeRolePolicyDoc: Statement: - Action: @@ -78,27 +78,14 @@ resources: RetentionInDays: 5 aws:ecr_repo:ecr_repo-0: ForceDelete: true - aws:availability_zone:region-0:availability_zone-0: - Index: 0 - Region: aws:region:region-0 - aws:availability_zone:region-0:availability_zone-1: - Index: 1 - Region: aws:region:region-0 - aws:availability_zone:region-0:availability_zone-2: - Index: 2 - Region: aws:region:region-0 - aws:availability_zone:region-0:availability_zone-3: - Index: 3 - Region: aws:region:region-0 - aws:region:region-0: aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-0-0-0-0: aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1: aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0: ElasticIp: aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-0-0-0-0 Subnet: aws:subnet:vpc-0:subnet-2 aws:subnet:vpc-0:subnet-2: - AvailabilityZone: aws:availability_zone:region-0:availability_zone-2 - CidrBlock: "" + AvailabilityZone: aws:availability_zone:region-0:availability_zone-0 + CidrBlock: 10.0.0.0/18 MapPublicIpOnLaunch: false RouteTable: aws:route_table:route_table-subnet-2-2 Type: public @@ -111,14 +98,17 @@ resources: - CidrBlock: 0.0.0.0/0 Gateway: aws:internet_gateway:vpc-0:internet_gateway-0 Vpc: aws:vpc:vpc-0 + aws:availability_zone:region-0:availability_zone-0: + Index: 0 + Region: aws:region:region-0 aws:internet_gateway:vpc-0:internet_gateway-0: Vpc: aws:vpc:vpc-0 aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1: ElasticIp: aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1 Subnet: aws:subnet:vpc-0:subnet-3 aws:subnet:vpc-0:subnet-3: - AvailabilityZone: aws:availability_zone:region-0:availability_zone-3 - CidrBlock: "" + AvailabilityZone: aws:availability_zone:region-0:availability_zone-1 + CidrBlock: 10.0.64.0/18 MapPublicIpOnLaunch: false RouteTable: aws:route_table:route_table-subnet-3-3 Type: public @@ -131,6 +121,10 @@ resources: - CidrBlock: 0.0.0.0/0 Gateway: aws:internet_gateway:vpc-0:internet_gateway-0 Vpc: aws:vpc:vpc-0 + aws:availability_zone:region-0:availability_zone-1: + Index: 1 + Region: aws:region:region-0 + aws:region:region-0: aws:rds_instance:rds-instance-2: AllocatedStorage: 20 DatabaseName: main @@ -175,11 +169,6 @@ resources: Protocol: "-1" ToPort: 0 IngressRules: - - Description: Allow ingress traffic from within the same security group - FromPort: 0 - Protocol: "-1" - Self: true - ToPort: 0 - CidrBlocks: - 10.0.128.0/18 Description: Allow ingress traffic from ip addresses within the subnet subnet-0 @@ -192,6 +181,11 @@ resources: FromPort: 0 Protocol: "-1" ToPort: 0 + - Description: Allow ingress traffic from within the same security group + FromPort: 0 + Protocol: "-1" + Self: true + ToPort: 0 Vpc: aws:vpc:vpc-0 aws:route_table:route_table-subnet-0-0: Routes: @@ -215,34 +209,32 @@ edges: aws:ecs_service:ecs_service_0 -> aws:subnet:vpc-0:subnet-0: aws:ecs_service:ecs_service_0 -> aws:subnet:vpc-0:subnet-1: aws:ecs_task_definition:ecs_service_0 -> aws:ecr_image:ecs_service_0-image: - aws:ecs_task_definition:ecs_service_0 -> aws:iam_role:ecs_service_0-execution-role: + aws:ecs_task_definition:ecs_service_0 -> aws:iam_role:ecs_service_0-rds-instance-2: aws:ecs_task_definition:ecs_service_0 -> aws:log_group:ecs_service_0-log-group: aws:ecs_task_definition:ecs_service_0 -> aws:region:region-0: aws:ecr_image:ecs_service_0-image -> aws:ecr_repo:ecr_repo-0: - aws:iam_role:ecs_service_0-execution-role -> aws:rds_instance:rds-instance-2: - aws:availability_zone:region-0:availability_zone-0 -> aws:region:region-0: - aws:availability_zone:region-0:availability_zone-1 -> aws:region:region-0: - aws:availability_zone:region-0:availability_zone-2 -> aws:region:region-0: - aws:availability_zone:region-0:availability_zone-3 -> aws:region:region-0: + aws:iam_role:ecs_service_0-rds-instance-2 -> aws:rds_instance:rds-instance-2: ? aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0 -> aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-0-0-0-0 : aws:nat_gateway:subnet-2:nat_gateway-route_table-subnet-0-0-0 -> aws:subnet:vpc-0:subnet-2: - aws:subnet:vpc-0:subnet-2 -> aws:availability_zone:region-0:availability_zone-2: + aws:subnet:vpc-0:subnet-2 -> aws:availability_zone:region-0:availability_zone-0: aws:subnet:vpc-0:subnet-2 -> aws:route_table_association:subnet-2-route_table-subnet-2-2: aws:subnet:vpc-0:subnet-2 -> aws:vpc:vpc-0: aws:route_table_association:subnet-2-route_table-subnet-2-2 -> aws:route_table:route_table-subnet-2-2: aws:route_table:route_table-subnet-2-2 -> aws:internet_gateway:vpc-0:internet_gateway-0: aws:route_table:route_table-subnet-2-2 -> aws:vpc:vpc-0: + aws:availability_zone:region-0:availability_zone-0 -> aws:region:region-0: aws:internet_gateway:vpc-0:internet_gateway-0 -> aws:vpc:vpc-0: ? aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1 -> aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1 : aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1 -> aws:subnet:vpc-0:subnet-3: - aws:subnet:vpc-0:subnet-3 -> aws:availability_zone:region-0:availability_zone-3: + aws:subnet:vpc-0:subnet-3 -> aws:availability_zone:region-0:availability_zone-1: aws:subnet:vpc-0:subnet-3 -> aws:route_table_association:subnet-3-route_table-subnet-3-3: aws:subnet:vpc-0:subnet-3 -> aws:vpc:vpc-0: aws:route_table_association:subnet-3-route_table-subnet-3-3 -> aws:route_table:route_table-subnet-3-3: aws:route_table:route_table-subnet-3-3 -> aws:internet_gateway:vpc-0:internet_gateway-0: aws:route_table:route_table-subnet-3-3 -> aws:vpc:vpc-0: + aws:availability_zone:region-0:availability_zone-1 -> aws:region:region-0: aws:rds_instance:rds-instance-2 -> aws:rds_subnet_group:rds_subnet_group-0: aws:rds_subnet_group:rds_subnet_group-0 -> aws:subnet:vpc-0:subnet-0: aws:rds_subnet_group:rds_subnet_group-0 -> aws:subnet:vpc-0:subnet-1: diff --git a/pkg/engine2/testdata/ecs_rds.iac-viz.yaml b/pkg/engine2/testdata/ecs_rds.iac-viz.yaml index cbe83a876..8faa223f6 100755 --- a/pkg/engine2/testdata/ecs_rds.iac-viz.yaml +++ b/pkg/engine2/testdata/ecs_rds.iac-viz.yaml @@ -23,12 +23,6 @@ resources: aws:subnet:vpc-0/subnet-1 -> aws:security_group:vpc-0/security_group-rds-instance-2-1: aws:subnet:vpc-0/subnet-1 -> vpc/vpc-0: - aws:availability_zone:region-0/availability_zone-3: - aws:availability_zone:region-0/availability_zone-3 -> region/region-0: - - aws:availability_zone:region-0/availability_zone-2: - aws:availability_zone:region-0/availability_zone-2 -> region/region-0: - rds_subnet_group/rds_subnet_group-0: rds_subnet_group/rds_subnet_group-0 -> aws:subnet:vpc-0/subnet-0: rds_subnet_group/rds_subnet_group-0 -> aws:subnet:vpc-0/subnet-1: @@ -36,13 +30,13 @@ resources: elastic_ip/elastic_ip-nat_gateway-route_table-subnet-1-1-1-1: aws:subnet:vpc-0/subnet-3: - aws:subnet:vpc-0/subnet-3 -> aws:availability_zone:region-0/availability_zone-3: + aws:subnet:vpc-0/subnet-3 -> aws:availability_zone:region-0/availability_zone-1: aws:subnet:vpc-0/subnet-3 -> vpc/vpc-0: elastic_ip/elastic_ip-nat_gateway-route_table-subnet-0-0-0-0: aws:subnet:vpc-0/subnet-2: - aws:subnet:vpc-0/subnet-2 -> aws:availability_zone:region-0/availability_zone-2: + aws:subnet:vpc-0/subnet-2 -> aws:availability_zone:region-0/availability_zone-0: aws:subnet:vpc-0/subnet-2 -> vpc/vpc-0: ecr_repo/ecr_repo-0: @@ -65,8 +59,8 @@ resources: ecr_image/ecs_service_0-image: ecr_image/ecs_service_0-image -> ecr_repo/ecr_repo-0: - iam_role/ecs_service_0-execution-role: - iam_role/ecs_service_0-execution-role -> rds_instance/rds-instance-2: + iam_role/ecs_service_0-rds-instance-2: + iam_role/ecs_service_0-rds-instance-2 -> rds_instance/rds-instance-2: log_group/ecs_service_0-log-group: @@ -90,7 +84,7 @@ resources: ecs_task_definition/ecs_service_0: ecs_task_definition/ecs_service_0 -> ecr_image/ecs_service_0-image: - ecs_task_definition/ecs_service_0 -> iam_role/ecs_service_0-execution-role: + ecs_task_definition/ecs_service_0 -> iam_role/ecs_service_0-rds-instance-2: ecs_task_definition/ecs_service_0 -> log_group/ecs_service_0-log-group: ecs_task_definition/ecs_service_0 -> region/region-0: diff --git a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml index 4ac49e5ff..d6e6c3931 100755 --- a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml +++ b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml @@ -8,7 +8,7 @@ resources: parent: vpc/vpc-0 load_balancer/rest-api-4-integbcc77100 -> kubernetes:pod:eks_cluster-0/pod2: - path: aws:load_balancer_listener:rest_api_4_integration_0-pod2,aws:target_group:rest-api-4-integbcc77100,kubernetes:target_group_binding:eks_cluster-0:restapi4integration0-pod2,kubernetes:service:restapi4integration0-pod2 + path: aws:load_balancer_listener:rest_api_4_integration_0-pod2,aws:target_group:rest-api-4-integbcc77100,kubernetes:target_group_binding:restapi4integration0-pod2,kubernetes:service:restapi4integration0-pod2 kubernetes:helm_chart:eks_cluster-0/metricsserver: diff --git a/pkg/engine2/testdata/k8s_api.expect.yaml b/pkg/engine2/testdata/k8s_api.expect.yaml index e949f5943..b3f245a10 100755 --- a/pkg/engine2/testdata/k8s_api.expect.yaml +++ b/pkg/engine2/testdata/k8s_api.expect.yaml @@ -120,8 +120,8 @@ resources: aws:load_balancer:rest-api-4-integbcc77100: Scheme: internal Subnets: - - aws:subnet:vpc-0:subnet-1 - aws:subnet:vpc-0:subnet-0 + - aws:subnet:vpc-0:subnet-1 Type: network aws:load_balancer_listener:rest_api_4_integration_0-pod2: DefaultActions: @@ -143,8 +143,7 @@ resources: Protocol: TCP TargetType: ip Vpc: aws:vpc:vpc-0 - kubernetes:target_group_binding:eks_cluster-0:restapi4integration0-pod2: - Cluster: aws:eks_cluster:eks_cluster-0 + kubernetes:target_group_binding:restapi4integration0-pod2: Object: apiVersion: elbv2.k8s.aws/v1beta1 kind: TargetGroupBinding @@ -286,12 +285,12 @@ resources: - ec2.amazonaws.com Version: "2012-10-17" ManagedPolicies: + - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy - arn:aws:iam::aws:policy/AWSCloudMapFullAccess - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore - - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy aws:iam_role:pod2: AssumeRolePolicyDoc: Statement: @@ -453,19 +452,6 @@ resources: - sts.amazonaws.com Cluster: aws:eks_cluster:eks_cluster-0 Region: aws:region:region-0 - aws:availability_zone:region-0:availability_zone-0: - Index: 0 - Region: aws:region:region-0 - aws:availability_zone:region-0:availability_zone-1: - Index: 1 - Region: aws:region:region-0 - aws:availability_zone:region-0:availability_zone-2: - Index: 2 - Region: aws:region:region-0 - aws:availability_zone:region-0:availability_zone-3: - Index: 3 - Region: aws:region:region-0 - aws:region:region-0: aws:eks_cluster:eks_cluster-0: ClusterRole: aws:iam_role:ClusterRole-eks_cluster-0 KubeConfig: kubernetes:kube_config:kube_config-eks_cluster-0-0 @@ -516,17 +502,6 @@ resources: Protocol: "-1" ToPort: 0 IngressRules: - - CidrBlocks: - - 0.0.0.0/0 - Description: Allows ingress traffic from the EKS control plane - FromPort: 9443 - Protocol: TCP - ToPort: 9443 - - Description: Allow ingress traffic from within the same security group - FromPort: 0 - Protocol: "-1" - Self: true - ToPort: 0 - CidrBlocks: - 10.0.128.0/18 Description: Allow ingress traffic from ip addresses within the subnet subnet-0 @@ -539,6 +514,17 @@ resources: FromPort: 0 Protocol: "-1" ToPort: 0 + - CidrBlocks: + - 0.0.0.0/0 + Description: Allows ingress traffic from the EKS control plane + FromPort: 9443 + Protocol: TCP + ToPort: 9443 + - Description: Allow ingress traffic from within the same security group + FromPort: 0 + Protocol: "-1" + Self: true + ToPort: 0 Vpc: aws:vpc:vpc-0 aws:route_table:route_table-subnet-0-0: Routes: @@ -558,23 +544,29 @@ resources: Subnet: aws:subnet:vpc-0:subnet-3 aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-0-0-0-0: aws:subnet:vpc-0:subnet-2: - AvailabilityZone: aws:availability_zone:region-0:availability_zone-2 - CidrBlock: "" + AvailabilityZone: aws:availability_zone:region-0:availability_zone-0 + CidrBlock: 10.0.0.0/18 MapPublicIpOnLaunch: false RouteTable: aws:route_table:route_table-subnet-2-2 Type: public Vpc: aws:vpc:vpc-0 aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1: aws:subnet:vpc-0:subnet-3: - AvailabilityZone: aws:availability_zone:region-0:availability_zone-3 - CidrBlock: "" + AvailabilityZone: aws:availability_zone:region-0:availability_zone-1 + CidrBlock: 10.0.64.0/18 MapPublicIpOnLaunch: false RouteTable: aws:route_table:route_table-subnet-3-3 Type: public Vpc: aws:vpc:vpc-0 + aws:availability_zone:region-0:availability_zone-0: + Index: 0 + Region: aws:region:region-0 aws:route_table_association:subnet-2-route_table-subnet-2-2: RouteTable: aws:route_table:route_table-subnet-2-2 Subnet: aws:subnet:vpc-0:subnet-2 + aws:availability_zone:region-0:availability_zone-1: + Index: 1 + Region: aws:region:region-0 aws:route_table_association:subnet-3-route_table-subnet-3-3: RouteTable: aws:route_table:route_table-subnet-3-3 Subnet: aws:subnet:vpc-0:subnet-3 @@ -583,6 +575,7 @@ resources: - CidrBlock: 0.0.0.0/0 Gateway: aws:internet_gateway:vpc-0:internet_gateway-0 Vpc: aws:vpc:vpc-0 + aws:region:region-0: aws:route_table:route_table-subnet-3-3: Routes: - CidrBlock: 0.0.0.0/0 @@ -622,11 +615,10 @@ edges: aws:load_balancer:rest-api-4-integbcc77100 -> aws:subnet:vpc-0:subnet-0: aws:load_balancer:rest-api-4-integbcc77100 -> aws:subnet:vpc-0:subnet-1: aws:load_balancer_listener:rest_api_4_integration_0-pod2 -> aws:target_group:rest-api-4-integbcc77100: - aws:target_group:rest-api-4-integbcc77100 -> kubernetes:target_group_binding:eks_cluster-0:restapi4integration0-pod2: - kubernetes:target_group_binding:eks_cluster-0:restapi4integration0-pod2 -> aws:eks_cluster:eks_cluster-0: - ? kubernetes:target_group_binding:eks_cluster-0:restapi4integration0-pod2 -> kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller - : - kubernetes:target_group_binding:eks_cluster-0:restapi4integration0-pod2 -> kubernetes:service:restapi4integration0-pod2: + aws:target_group:rest-api-4-integbcc77100 -> kubernetes:target_group_binding:restapi4integration0-pod2: + kubernetes:target_group_binding:restapi4integration0-pod2 -> aws:eks_cluster:eks_cluster-0: + kubernetes:target_group_binding:restapi4integration0-pod2 -> kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller: + kubernetes:target_group_binding:restapi4integration0-pod2 -> kubernetes:service:restapi4integration0-pod2: kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller -> aws:eks_cluster:eks_cluster-0: kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller -> aws:region:region-0: ? kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller -> kubernetes:service_account:eks_cluster-0:aws-load-balancer-controller @@ -652,10 +644,6 @@ edges: aws:iam_role:pod2 -> aws:iam_oidc_provider:eks_cluster-0: aws:iam_oidc_provider:eks_cluster-0 -> aws:eks_cluster:eks_cluster-0: aws:iam_oidc_provider:eks_cluster-0 -> aws:region:region-0: - aws:availability_zone:region-0:availability_zone-0 -> aws:region:region-0: - aws:availability_zone:region-0:availability_zone-1 -> aws:region:region-0: - aws:availability_zone:region-0:availability_zone-2 -> aws:region:region-0: - aws:availability_zone:region-0:availability_zone-3 -> aws:region:region-0: aws:eks_cluster:eks_cluster-0 -> aws:iam_role:ClusterRole-eks_cluster-0: aws:eks_cluster:eks_cluster-0 -> aws:subnet:vpc-0:subnet-0: aws:eks_cluster:eks_cluster-0 -> aws:subnet:vpc-0:subnet-1: @@ -682,13 +670,15 @@ edges: ? aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1 -> aws:elastic_ip:elastic_ip-nat_gateway-route_table-subnet-1-1-1-1 : aws:nat_gateway:subnet-3:nat_gateway-route_table-subnet-1-1-1 -> aws:subnet:vpc-0:subnet-3: - aws:subnet:vpc-0:subnet-2 -> aws:availability_zone:region-0:availability_zone-2: + aws:subnet:vpc-0:subnet-2 -> aws:availability_zone:region-0:availability_zone-0: aws:subnet:vpc-0:subnet-2 -> aws:route_table_association:subnet-2-route_table-subnet-2-2: aws:subnet:vpc-0:subnet-2 -> aws:vpc:vpc-0: - aws:subnet:vpc-0:subnet-3 -> aws:availability_zone:region-0:availability_zone-3: + aws:subnet:vpc-0:subnet-3 -> aws:availability_zone:region-0:availability_zone-1: aws:subnet:vpc-0:subnet-3 -> aws:route_table_association:subnet-3-route_table-subnet-3-3: aws:subnet:vpc-0:subnet-3 -> aws:vpc:vpc-0: + aws:availability_zone:region-0:availability_zone-0 -> aws:region:region-0: aws:route_table_association:subnet-2-route_table-subnet-2-2 -> aws:route_table:route_table-subnet-2-2: + aws:availability_zone:region-0:availability_zone-1 -> aws:region:region-0: aws:route_table_association:subnet-3-route_table-subnet-3-3 -> aws:route_table:route_table-subnet-3-3: aws:route_table:route_table-subnet-2-2 -> aws:internet_gateway:vpc-0:internet_gateway-0: aws:route_table:route_table-subnet-2-2 -> aws:vpc:vpc-0: diff --git a/pkg/engine2/testdata/k8s_api.iac-viz.yaml b/pkg/engine2/testdata/k8s_api.iac-viz.yaml index 1acc114f8..7800756cb 100755 --- a/pkg/engine2/testdata/k8s_api.iac-viz.yaml +++ b/pkg/engine2/testdata/k8s_api.iac-viz.yaml @@ -45,12 +45,6 @@ resources: iam_role/pod2: iam_role/pod2 -> iam_oidc_provider/eks_cluster-0: - aws:availability_zone:region-0/availability_zone-3: - aws:availability_zone:region-0/availability_zone-3 -> region/region-0: - - aws:availability_zone:region-0/availability_zone-2: - aws:availability_zone:region-0/availability_zone-2 -> region/region-0: - aws:api_resource:rest_api_4/api_resource-0: aws:api_resource:rest_api_4/api_resource-0 -> rest_api/rest_api_4: @@ -77,13 +71,13 @@ resources: elastic_ip/elastic_ip-nat_gateway-route_table-subnet-1-1-1-1: aws:subnet:vpc-0/subnet-3: - aws:subnet:vpc-0/subnet-3 -> aws:availability_zone:region-0/availability_zone-3: + aws:subnet:vpc-0/subnet-3 -> aws:availability_zone:region-0/availability_zone-1: aws:subnet:vpc-0/subnet-3 -> vpc/vpc-0: elastic_ip/elastic_ip-nat_gateway-route_table-subnet-0-0-0-0: aws:subnet:vpc-0/subnet-2: - aws:subnet:vpc-0/subnet-2 -> aws:availability_zone:region-0/availability_zone-2: + aws:subnet:vpc-0/subnet-2 -> aws:availability_zone:region-0/availability_zone-0: aws:subnet:vpc-0/subnet-2 -> vpc/vpc-0: aws:api_method:rest_api_4/rest_api_4_integration_0_method: @@ -161,11 +155,11 @@ resources: aws:api_deployment:rest_api_4/api_deployment-0 -> aws:api_method:rest_api_4/rest_api_4_integration_0_method: aws:api_deployment:rest_api_4/api_deployment-0 -> rest_api/rest_api_4: - kubernetes:target_group_binding:eks_cluster-0/restapi4integration0-pod2: - kubernetes:target_group_binding:eks_cluster-0/restapi4integration0-pod2 -> eks_cluster/eks_cluster-0: - kubernetes:target_group_binding:eks_cluster-0/restapi4integration0-pod2 -> target_group/rest-api-4-integbcc77100: - kubernetes:target_group_binding:eks_cluster-0/restapi4integration0-pod2 -> kubernetes:helm_chart:eks_cluster-0/aws-load-balancer-controller: - kubernetes:target_group_binding:eks_cluster-0/restapi4integration0-pod2 -> kubernetes:service/restapi4integration0-pod2: + kubernetes:target_group_binding/restapi4integration0-pod2: + kubernetes:target_group_binding/restapi4integration0-pod2 -> eks_cluster/eks_cluster-0: + kubernetes:target_group_binding/restapi4integration0-pod2 -> target_group/rest-api-4-integbcc77100: + kubernetes:target_group_binding/restapi4integration0-pod2 -> kubernetes:helm_chart:eks_cluster-0/aws-load-balancer-controller: + kubernetes:target_group_binding/restapi4integration0-pod2 -> kubernetes:service/restapi4integration0-pod2: kubernetes:manifest/fluent-bit: kubernetes:manifest/fluent-bit -> eks_cluster/eks_cluster-0: diff --git a/pkg/infra/cli2.go b/pkg/infra/cli2.go index 25ee70801..b8835e805 100644 --- a/pkg/infra/cli2.go +++ b/pkg/infra/cli2.go @@ -13,7 +13,7 @@ import ( "github.com/klothoplatform/klotho/pkg/infra/iac3" "github.com/klothoplatform/klotho/pkg/infra/kubernetes" "github.com/klothoplatform/klotho/pkg/io" - knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/klothoplatform/klotho/pkg/knowledge_base2/reader" "github.com/klothoplatform/klotho/pkg/logging" "github.com/klothoplatform/klotho/pkg/templates" "github.com/spf13/cobra" @@ -106,7 +106,7 @@ func GenerateIac(cmd *cobra.Command, args []string) error { return err } - kb, err := knowledgebase.NewKBFromFs(templates.ResourceTemplates, templates.EdgeTemplates, templates.Models) + kb, err := reader.NewKBFromFs(templates.ResourceTemplates, templates.EdgeTemplates, templates.Models) if err != nil { return err } diff --git a/pkg/knowledge_base2/emitter.go b/pkg/knowledge_base2/emitter.go index 6bc058385..5f6378ab5 100644 --- a/pkg/knowledge_base2/emitter.go +++ b/pkg/knowledge_base2/emitter.go @@ -150,12 +150,7 @@ func HasConsumedFromResource(consumer, emitter *construct.Resource, ctx DynamicV errs = errors.Join(errs, fmt.Errorf("property %s not found", consume.PropertyPath)) continue } - ptype, err := prop.PropertyType() - if err != nil { - errs = errors.Join(errs, err) - continue - } - if ptype.Contains(pval, val) { + if prop.Contains(pval, val) { return true, nil } } @@ -231,16 +226,5 @@ func (c *ConsumptionObject) Consume(val any, ctx DynamicValueContext, resource * return err } prop := rt.GetProperty(c.PropertyPath) - if prop.IsPropertyTypeScalar() { - err := resource.SetProperty(c.PropertyPath, val) - if err != nil { - return err - } - } else { - err := resource.AppendProperty(c.PropertyPath, val) - if err != nil { - return err - } - } - return nil + return prop.AppendProperty(resource, val) } diff --git a/pkg/knowledge_base2/graph.go b/pkg/knowledge_base2/graph.go index 65c0c838d..6f3cf0925 100644 --- a/pkg/knowledge_base2/graph.go +++ b/pkg/knowledge_base2/graph.go @@ -256,64 +256,64 @@ func IsOperationalResourceSideEffect(dag construct.Graph, kb TemplateKB, rid, si dynCtx := DynamicValueContext{Graph: dag, KnowledgeBase: kb} for _, property := range template.Properties { ruleSatisfied := false - if property.OperationalRule == nil { + rule := property.Details().OperationalRule + if rule == nil || len(rule.Step.Resources) == 0 { continue } - rule := property.OperationalRule - for i, step := range rule.Steps { - // We only check if the resource selector is a match in terms of properties and classifications (not the actual id) - // We do this because if we have explicit ids in the selector and someone changes the id of a side effect resource - // we would no longer think it is a side effect since the id would no longer match. - // To combat this we just check against type - for j, resourceSelector := range step.Resources { - if match, err := resourceSelector.IsMatch(dynCtx, DynamicValueData{Resource: rid}, sideEffectResource); match { - ruleSatisfied = true - break - } else if err != nil { - return false, fmt.Errorf( - "error checking if %s is side effect of %s in step %d, resource %d: %w", - sideEffect, rid, i, j, err, - ) - } + step := rule.Step + // We only check if the resource selector is a match in terms of properties and classifications (not the actual id) + // We do this because if we have explicit ids in the selector and someone changes the id of a side effect resource + // we would no longer think it is a side effect since the id would no longer match. + // To combat this we just check against type + for j, resourceSelector := range step.Resources { + if match, err := resourceSelector.IsMatch(dynCtx, DynamicValueData{Resource: rid}, sideEffectResource); match { + ruleSatisfied = true + break + } else if err != nil { + return false, fmt.Errorf( + "error checking if %s is side effect of %s in property %s, resource %d: %w", + sideEffect, rid, property.Details().Name, j, err, + ) } + } - // If the side effect resource fits the rule we then perform 2 more checks - // 1. is there a path in the direction of the rule - // 2. Is the property set with the resource that we are checking for - if ruleSatisfied { - if step.Direction == DirectionUpstream { - resources, err := graph.ShortestPath(dag, sideEffect, rid) - if len(resources) == 0 || err != nil { - continue - } - } else { - resources, err := graph.ShortestPath(dag, rid, sideEffect) - if len(resources) == 0 || err != nil { - continue - } + // If the side effect resource fits the rule we then perform 2 more checks + // 1. is there a path in the direction of the rule + // 2. Is the property set with the resource that we are checking for + if ruleSatisfied { + if step.Direction == DirectionUpstream { + resources, err := graph.ShortestPath(dag, sideEffect, rid) + if len(resources) == 0 || err != nil { + continue } - - propertyVal, err := resource.GetProperty(property.Path) - if err != nil || propertyVal == nil { + } else { + resources, err := graph.ShortestPath(dag, rid, sideEffect) + if len(resources) == 0 || err != nil { continue } - val := reflect.ValueOf(propertyVal) - if val.Kind() == reflect.Array || val.Kind() == reflect.Slice { - for i := 0; i < val.Len(); i++ { - if arrId, ok := val.Index(i).Interface().(construct.ResourceId); ok && arrId == sideEffect { - return true, nil - } - } - } else { - if val.IsZero() { - continue - } - if valId, ok := val.Interface().(construct.ResourceId); ok && valId == sideEffect { + } + + propertyVal, err := resource.GetProperty(property.Details().Path) + if err != nil || propertyVal == nil { + continue + } + val := reflect.ValueOf(propertyVal) + if val.Kind() == reflect.Array || val.Kind() == reflect.Slice { + for i := 0; i < val.Len(); i++ { + if arrId, ok := val.Index(i).Interface().(construct.ResourceId); ok && arrId == sideEffect { return true, nil } } + } else { + if val.IsZero() { + continue + } + if valId, ok := val.Interface().(construct.ResourceId); ok && valId == sideEffect { + return true, nil + } } } + } return false, nil } diff --git a/pkg/knowledge_base2/kb.go b/pkg/knowledge_base2/kb.go index 0285ac657..64ae6cf4d 100644 --- a/pkg/knowledge_base2/kb.go +++ b/pkg/knowledge_base2/kb.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "sort" + "text/template" "github.com/dominikbraun/graph" construct "github.com/klothoplatform/klotho/pkg/construct2" @@ -32,7 +33,7 @@ type ( // KnowledgeBase is a struct that represents the object which contains the knowledge of how to make resources operational KnowledgeBase struct { underlying graph.Graph[string, *ResourceTemplate] - models map[string]*Model + Models map[string]*Model } EdgePathSatisfaction struct { @@ -42,6 +43,11 @@ type ( Source PathSatisfactionRoute Target PathSatisfactionRoute } + + ValueOrTemplate struct { + Value any + Template *template.Template + } ) const ( @@ -59,7 +65,7 @@ func NewKB() *KnowledgeBase { } func (kb *KnowledgeBase) GetModel(model string) *Model { - return kb.models[model] + return kb.Models[model] } // ListResources returns a list of all resources in the knowledge base @@ -216,37 +222,34 @@ func (kb *KnowledgeBase) GetAllowedNamespacedResourceIds(ctx DynamicValueContext if property == nil { return result, nil } - rule := property.OperationalRule + rule := property.Details().OperationalRule if rule == nil { return result, nil } - for _, step := range rule.Steps { - if step.Resources != nil { - for _, resource := range step.Resources { - if resource.Selector != "" { - id, err := ExecuteDecodeAsResourceId(ctx, resource.Selector, DynamicValueData{Resource: resourceId}) - if err != nil { - return nil, err - } - template, err := kb.GetResourceTemplate(id) - if err != nil { - return nil, err - } - if template.ResourceContainsClassifications(resource.Classifications) { - result = append(result, id) - } + if rule.Step.Resources != nil { + for _, resource := range rule.Step.Resources { + if resource.Selector != "" { + id, err := ExecuteDecodeAsResourceId(ctx, resource.Selector, DynamicValueData{Resource: resourceId}) + if err != nil { + return nil, err + } + template, err := kb.GetResourceTemplate(id) + if err != nil { + return nil, err } - if resource.Classifications != nil && resource.Selector == "" { - for _, resTempalte := range kb.ListResources() { - if resTempalte.ResourceContainsClassifications(resource.Classifications) { - result = append(result, resTempalte.Id()) - } + if template.ResourceContainsClassifications(resource.Classifications) { + result = append(result, id) + } + } + if resource.Classifications != nil && resource.Selector == "" { + for _, resTempalte := range kb.ListResources() { + if resTempalte.ResourceContainsClassifications(resource.Classifications) { + result = append(result, resTempalte.Id()) } - } + } } - } return result, nil } @@ -274,7 +277,7 @@ func (kb *KnowledgeBase) GetResourcesNamespaceResource(resource *construct.Resou } namespaceProperty := template.GetNamespacedProperty() if namespaceProperty != nil { - ns, err := resource.GetProperty(namespaceProperty.Name) + ns, err := resource.GetProperty(namespaceProperty.Details().Name) if err != nil { return construct.ResourceId{}, err } @@ -295,8 +298,8 @@ func (kb *KnowledgeBase) GetResourcePropertyType(resource construct.ResourceId, return "" } for _, property := range template.Properties { - if property.Name == propertyName { - return property.Type + if property.Details().Name == propertyName { + return property.Type() } } return "" @@ -322,21 +325,14 @@ func TransformToPropertyValue( propertyName, resource, ) } - propertyType, err := property.PropertyType() - if err != nil { - return nil, fmt.Errorf( - "could not find property type %s on resource %s for property %s", - property.Type, resource, property.Name, - ) - } if value == nil { - return propertyType.ZeroValue(), nil + return property.ZeroValue(), nil } - val, err := propertyType.Parse(value, ctx, data) + val, err := property.Parse(value, ctx, data) if err != nil { return nil, fmt.Errorf( "could not parse value %v for property %s on resource %s: %w", - value, property.Name, resource, err, + value, property.Details().Name, resource, err, ) } return val, nil @@ -363,8 +359,8 @@ resourceLoop: } data := DynamicValueData{Resource: resource.ID} - for _, prop := range tmpl.Properties { - path, err := resource.PropertyPath(prop.Name) + for name := range tmpl.Properties { + path, err := resource.PropertyPath(name) if err != nil { errs = errors.Join(errs, err) continue @@ -373,14 +369,14 @@ resourceLoop: if preXform == nil { continue } - val, err := TransformToPropertyValue(resource.ID, prop.Name, preXform, ctx, data) + val, err := TransformToPropertyValue(resource.ID, name, preXform, ctx, data) if err != nil { - errs = errors.Join(errs, fmt.Errorf("error transforming %s#%s: %w", resource.ID, prop.Name, err)) + errs = errors.Join(errs, fmt.Errorf("error transforming %s#%s: %w", resource.ID, name, err)) continue resourceLoop } err = path.Set(val) if err != nil { - errs = errors.Join(errs, fmt.Errorf("errors setting %s#%s: %w", resource.ID, prop.Name, err)) + errs = errors.Join(errs, fmt.Errorf("errors setting %s#%s: %w", resource.ID, name, err)) continue resourceLoop } } diff --git a/pkg/knowledge_base2/model.go b/pkg/knowledge_base2/model.go index cc5dbd26a..cc550496f 100644 --- a/pkg/knowledge_base2/model.go +++ b/pkg/knowledge_base2/model.go @@ -8,101 +8,26 @@ type ( Model struct { Name string `json:"name" yaml:"name"` Properties Properties `json:"properties" yaml:"properties"` - Property *Property `json:"property" yaml:"property"` + Property Property `json:"property" yaml:"property"` } ) -func updateModels(property *Property, properties Properties, models map[string]*Model) error { - for name, p := range properties { - modelType := p.ModelType() - if modelType != nil { - if len(p.Properties) != 0 { - return fmt.Errorf("property %s has properties but is labeled as a model", name) - } - model := models[*modelType] - if model == nil || model.Properties == nil { - return fmt.Errorf("model %s not found", *modelType) - } - // We know that this means we want the properties to be spread onto the resource - if p.Name == *modelType { - if model.Property != nil { - return fmt.Errorf("model %s as property can not be spread into properties", *modelType) - } - delete(properties, name) - for name, prop := range model.Properties { - // since properties are pointers and models can be reused, we need to clone the property from the model itself - newProp := prop.Clone() - newProp.Path = fmt.Sprintf("%s.%s", name, prop.Path) - - // we also need to check if the current property has a default and propagate it lower - if p.DefaultValue != nil { - defaultMap, ok := p.DefaultValue.(map[string]any) - if !ok { - return fmt.Errorf("default value for %s is not a map", p.Path) - } - newProp.DefaultValue = defaultMap[name] - } - properties[name] = newProp - } - if property != nil { - if err := updateModelPaths(property); err != nil { - return err - } - } - } else { - m := models[*modelType] - if m.Properties != nil { - p.Properties = models[*modelType].Properties.Clone() - modelString := fmt.Sprintf("model(%s)", *modelType) - if p.Type == modelString { - p.Type = "map" - } else if p.Type == fmt.Sprintf("list(%s)", modelString) { - p.Type = "list" - } - if err := updateModelPaths(p); err != nil { - return err - } - } else if m.Property != nil { - p = m.Property.Clone() - } - } - } - err := updateModels(p, p.Properties, models) - if err != nil { - return err - } - } - return nil -} - -func updateModelPaths(p *Property) error { - for _, prop := range p.Properties { - prop.Path = fmt.Sprintf("%s.%s", p.Path, prop.Name) - err := updateModelPaths(prop) - if err != nil { - return err - } - } - return nil -} - // GetObjectValue returns the value of the object as the model type func (m *Model) GetObjectValue(val any, ctx DynamicValueContext, data DynamicValueData) (any, error) { - GetVal := func(p *Property, val map[string]any) (any, error) { - pType, err := p.PropertyType() - if err != nil { - return nil, err - } - propVal, found := val[p.Name] + GetVal := func(p Property, val map[string]any) (any, error) { + propVal, found := val[p.Details().Name] if !found { - if p.DefaultValue != nil { - return p.DefaultValue, nil + defaultVal, err := p.GetDefaultValue(ctx, data) + if err != nil { + return nil, err + } + if defaultVal != nil { + return defaultVal, nil } - return nil, fmt.Errorf("property %s not found", p.Name) + return nil, fmt.Errorf("property %s not found", p.Details().Name) } - - return pType.Parse(propVal, ctx, data) + return p.Parse(propVal, ctx, data) } if m.Properties != nil && m.Property != nil { return nil, fmt.Errorf("model has both properties and a property") @@ -129,11 +54,7 @@ func (m *Model) GetObjectValue(val any, ctx DynamicValueContext, data DynamicVal } return obj, errs } else { - pType, err := m.Property.PropertyType() - if err != nil { - return nil, err - } - value, err := pType.Parse(val, ctx, data) + value, err := m.Property.Parse(val, ctx, data) if err != nil { return nil, err } diff --git a/pkg/knowledge_base2/operational_rule.go b/pkg/knowledge_base2/operational_rule.go index 60a7b1943..133e9d74a 100644 --- a/pkg/knowledge_base2/operational_rule.go +++ b/pkg/knowledge_base2/operational_rule.go @@ -16,6 +16,23 @@ type ( ConfigurationRules []ConfigurationRule `json:"configuration_rules" yaml:"configuration_rules"` } + EdgeRule struct { + If string `json:"if" yaml:"if"` + Steps []EdgeOperationalStep `json:"steps" yaml:"steps"` + ConfigurationRules []ConfigurationRule `json:"configuration_rules" yaml:"configuration_rules"` + } + + PropertyRule struct { + If string `json:"if" yaml:"if"` + Step OperationalStep `json:"step" yaml:"step"` + Value any `json:"value" yaml:"value"` + } + + EdgeOperationalStep struct { + Resource string `json:"resource" yaml:"resource"` + OperationalStep + } + // OperationalRule defines a rule that must pass checks and actions which must be carried out to make a resource operational OperationalStep struct { Resource string `json:"resource" yaml:"resource"` @@ -25,7 +42,7 @@ type ( Resources []ResourceSelector `json:"resources" yaml:"resources"` // NumNeeded defines the number of resources that must satisfy the rule NumNeeded int `json:"num_needed" yaml:"num_needed"` - + // FailIfMissing fails if the step is not satisfied when being evaluated. If this flag is set, the step cannot create dependencies FailIfMissing bool `json:"fail_if_missing" yaml:"fail_if_missing"` // Unique defines if the resource that is created should be unique Unique bool `json:"unique" yaml:"unique"` @@ -51,6 +68,9 @@ type ( ResourceSelector struct { Selector string `json:"selector" yaml:"selector"` Properties map[string]any `json:"properties" yaml:"properties"` + // NumPreferred defines the amount of resources that should be preferred to satisfy the selector. + // This number is only used if num needed on the step is not met + NumPreferred int `json:"num_preferred" yaml:"num_preferred"` // Classifications defines the classifications that the rule should be enforced on. Classifications must be specified if resource types is not specified Classifications []string `json:"classifications" yaml:"classifications"` } diff --git a/pkg/knowledge_base2/properties/any_property.go b/pkg/knowledge_base2/properties/any_property.go new file mode 100644 index 000000000..23fcda309 --- /dev/null +++ b/pkg/knowledge_base2/properties/any_property.go @@ -0,0 +1,116 @@ +package properties + +import ( + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) + +type ( + AnyProperty struct { + SharedPropertyFields + knowledgebase.PropertyDetails + } +) + +func (a *AnyProperty) SetProperty(resource *construct.Resource, value any) error { + return resource.SetProperty(a.Path, value) +} + +func (a *AnyProperty) AppendProperty(resource *construct.Resource, value any) error { + propVal, err := resource.GetProperty(a.Path) + if err != nil { + return err + } + if propVal == nil { + return a.SetProperty(resource, value) + } + return resource.AppendProperty(a.Path, value) +} + +func (a *AnyProperty) RemoveProperty(resource *construct.Resource, value any) error { + return resource.RemoveProperty(a.Path, value) +} + +func (a *AnyProperty) Details() *knowledgebase.PropertyDetails { + return &a.PropertyDetails +} + +func (a *AnyProperty) Clone() knowledgebase.Property { + clone := *a + return &clone +} + +func (a *AnyProperty) GetDefaultValue(ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if a.DefaultValue == nil { + return nil, nil + } + return a.Parse(a.DefaultValue, ctx, data) +} + +func (a *AnyProperty) Parse(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if val, ok := value.(string); ok { + // first check if its a resource id + rType := ResourceProperty{} + id, err := rType.Parse(val, ctx, data) + if err == nil { + return id, nil + } + + // check if its a property ref + ref, err := ParsePropertyRef(val, ctx, data) + if err == nil { + return ref, nil + } + + // check if its any other template string + var result any + err = ctx.ExecuteDecode(val, data, &result) + if err == nil { + return result, nil + } + } + + if mapVal, ok := value.(map[string]any); ok { + m := MapProperty{KeyProperty: &StringProperty{}, ValueProperty: &AnyProperty{}} + return m.Parse(mapVal, ctx, data) + } + + if listVal, ok := value.([]any); ok { + l := ListProperty{ItemProperty: &AnyProperty{}} + return l.Parse(listVal, ctx, data) + } + + return value, nil +} + +func (a *AnyProperty) ZeroValue() any { + return nil +} + +func (a *AnyProperty) Contains(value any, contains any) bool { + if val, ok := value.(string); ok { + s := StringProperty{} + return s.Contains(val, contains) + } + if mapVal, ok := value.(map[string]any); ok { + m := MapProperty{KeyProperty: &StringProperty{}, ValueProperty: &AnyProperty{}} + return m.Contains(mapVal, contains) + } + if listVal, ok := value.([]any); ok { + l := ListProperty{ItemProperty: &AnyProperty{}} + return l.Contains(listVal, contains) + } + return false +} + +func (a *AnyProperty) Type() string { + return "any" +} + +func (a *AnyProperty) Validate(resource *construct.Resource, value any) error { + return nil +} + +func (a *AnyProperty) SubProperties() knowledgebase.Properties { + return nil +} diff --git a/pkg/knowledge_base2/properties/any_property_test.go b/pkg/knowledge_base2/properties/any_property_test.go new file mode 100644 index 000000000..d4237391d --- /dev/null +++ b/pkg/knowledge_base2/properties/any_property_test.go @@ -0,0 +1,93 @@ +package properties + +import ( + "testing" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/stretchr/testify/assert" +) + +func Test_SetAnyProperty(t *testing.T) { + tests := []struct { + name string + property *AnyProperty + resource *construct.Resource + value any + }{ + { + name: "any property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &AnyProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.SetProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value, tt.resource.Properties[tt.property.Path]) + }) + } +} + +func Test_AppendAnyProperty(t *testing.T) { + tests := []struct { + name string + property *AnyProperty + resource *construct.Resource + value any + expect any + }{ + { + name: "any property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &AnyProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + expect: "test", + }, + { + name: "existing property", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": map[string]any{ + "test": "test", + }, + }, + }, + property: &AnyProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: map[string]any{ + "test2": "test2", + }, + expect: map[string]any{ + "test": "test", + "test2": "test2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.AppendProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.expect, tt.resource.Properties[tt.property.Path]) + }) + } +} diff --git a/pkg/knowledge_base2/properties/bool_property.go b/pkg/knowledge_base2/properties/bool_property.go new file mode 100644 index 000000000..2b86cc834 --- /dev/null +++ b/pkg/knowledge_base2/properties/bool_property.go @@ -0,0 +1,95 @@ +package properties + +import ( + "fmt" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) + +type ( + BoolProperty struct { + SharedPropertyFields + knowledgebase.PropertyDetails + } +) + +func (b *BoolProperty) SetProperty(resource *construct.Resource, value any) error { + if val, ok := value.(bool); ok { + return resource.SetProperty(b.Path, val) + } else if val, ok := value.(construct.PropertyRef); ok { + return resource.SetProperty(b.Path, val) + } + return fmt.Errorf("invalid bool value %v", value) +} + +func (b *BoolProperty) AppendProperty(resource *construct.Resource, value any) error { + return b.SetProperty(resource, value) +} + +func (b *BoolProperty) RemoveProperty(resource *construct.Resource, value any) error { + propVal, err := resource.GetProperty(b.Path) + if err != nil { + return err + } + if propVal == nil { + return nil + } + return resource.RemoveProperty(b.Path, value) +} + +func (b *BoolProperty) Clone() knowledgebase.Property { + clone := *b + return &clone +} + +func (b *BoolProperty) Details() *knowledgebase.PropertyDetails { + return &b.PropertyDetails +} + +func (b *BoolProperty) GetDefaultValue(ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if b.DefaultValue == nil { + return nil, nil + } + return b.Parse(b.DefaultValue, ctx, data) +} + +func (b *BoolProperty) Parse(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if val, ok := value.(string); ok { + var result bool + err := ctx.ExecuteDecode(val, data, &result) + return result, err + } + if val, ok := value.(bool); ok { + return val, nil + } + val, err := ParsePropertyRef(value, ctx, data) + + if err == nil { + return val, nil + } + return nil, fmt.Errorf("invalid bool value %v", value) +} + +func (b *BoolProperty) ZeroValue() any { + return false +} + +func (b *BoolProperty) Contains(value any, contains any) bool { + return false +} + +func (b *BoolProperty) Type() string { + return "bool" +} + +func (b *BoolProperty) Validate(resource *construct.Resource, value any) error { + if _, ok := value.(bool); !ok { + return fmt.Errorf("invalid bool value %v", value) + } + return nil +} + +func (b *BoolProperty) SubProperties() knowledgebase.Properties { + return nil +} diff --git a/pkg/knowledge_base2/properties/bool_property_test.go b/pkg/knowledge_base2/properties/bool_property_test.go new file mode 100644 index 000000000..29a2f1434 --- /dev/null +++ b/pkg/knowledge_base2/properties/bool_property_test.go @@ -0,0 +1,311 @@ +package properties + +import ( + "testing" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/stretchr/testify/assert" +) + +func Test_SetBoolProperty(t *testing.T) { + tests := []struct { + name string + property *BoolProperty + resource *construct.Resource + value bool + }{ + { + name: "bool property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: true, + }, + { + name: "existing property", + resource: &construct.Resource{Properties: map[string]any{"test": false}}, + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.SetProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value, tt.resource.Properties[tt.property.Path]) + }) + } +} + +func Test_AppendBoolProperty(t *testing.T) { + tests := []struct { + name string + property *BoolProperty + resource *construct.Resource + value bool + }{ + { + name: "bool property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: true, + }, + { + name: "existing property", + resource: &construct.Resource{Properties: map[string]any{"test": false}}, + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.AppendProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value, tt.resource.Properties[tt.property.Path]) + }) + } +} + +func Test_RemoveBoolProperty(t *testing.T) { + tests := []struct { + name string + property *BoolProperty + resource *construct.Resource + }{ + { + name: "bool property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + }, + { + name: "existing property", + resource: &construct.Resource{Properties: map[string]any{"test": false}}, + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.RemoveProperty(tt.resource, nil) + if !assert.NoError(err) { + return + } + assert.NotContains(tt.resource.Properties, tt.property.Path) + }) + } +} + +func Test_GetDefaultBoolValue(t *testing.T) { + tests := []struct { + name string + property *BoolProperty + value any + }{ + { + name: "bool property", + property: &BoolProperty{}, + }, + { + name: "existing default", + property: &BoolProperty{ + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: true, + }, + }, + value: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase.DynamicValueContext{} + data := knowledgebase.DynamicValueData{} + result, err := tt.property.GetDefaultValue(ctx, data) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value, result) + }) + } +} + +func Test_CloneBoolProperty(t *testing.T) { + tests := []struct { + name string + property *BoolProperty + }{ + { + name: "bool property", + property: &BoolProperty{}, + }, + { + name: "existing default", + property: &BoolProperty{ + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + result := tt.property.Clone() + assert.Equal(tt.property, result) + assert.NotSame(tt.property, result) + }) + } +} + +func Test_BoolPropertyParse(t *testing.T) { + tests := []struct { + name string + property *BoolProperty + value any + ctx knowledgebase.DynamicValueContext + data knowledgebase.DynamicValueData + want any + wantErr bool + }{ + { + name: "bool property", + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: true, + want: true, + wantErr: false, + }, + { + name: "template value", + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "{{ true }}", + want: true, + wantErr: false, + }, + { + name: "invalid value", + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + result, err := tt.property.Parse(tt.value, tt.ctx, tt.data) + if !assert.Equal(tt.wantErr, err != nil) { + return + } + assert.Equal(tt.want, result) + }) + } +} + +func Test_BoolPropertyZeroValue(t *testing.T) { + assert := assert.New(t) + property := &BoolProperty{} + assert.Equal(property.ZeroValue(), false) +} + +func Test_BoolPropertyDetails(t *testing.T) { + assert := assert.New(t) + property := &BoolProperty{} + assert.Same(property.Details(), &property.PropertyDetails) +} + +func Test_BoolPropertyContains(t *testing.T) { + assert := assert.New(t) + property := &BoolProperty{} + assert.False(property.Contains(nil, nil)) +} + +func Test_BoolPropertyType(t *testing.T) { + assert := assert.New(t) + property := &BoolProperty{} + assert.Equal(property.Type(), "bool") +} + +func Test_BoolPropertyValidate(t *testing.T) { + tests := []struct { + name string + property *BoolProperty + value any + wantErr bool + }{ + { + name: "bool property", + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: true, + wantErr: false, + }, + { + name: "invalid value", + property: &BoolProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + resource := &construct.Resource{} + err := tt.property.Validate(resource, tt.value) + assert.Equal(tt.wantErr, err != nil) + }) + } +} + +func Test_BoolPropertySubProperties(t *testing.T) { + assert := assert.New(t) + property := &BoolProperty{} + assert.Nil(property.SubProperties()) +} diff --git a/pkg/knowledge_base2/properties/float_property.go b/pkg/knowledge_base2/properties/float_property.go new file mode 100644 index 000000000..905673bb2 --- /dev/null +++ b/pkg/knowledge_base2/properties/float_property.go @@ -0,0 +1,110 @@ +package properties + +import ( + "fmt" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) + +type ( + FloatProperty struct { + MinValue *float64 + MaxValue *float64 + SharedPropertyFields + knowledgebase.PropertyDetails + } +) + +func (f *FloatProperty) SetProperty(resource *construct.Resource, value any) error { + if val, ok := value.(float64); ok { + return resource.SetProperty(f.Path, val) + } else if val, ok := value.(construct.PropertyRef); ok { + return resource.SetProperty(f.Path, val) + } + return fmt.Errorf("invalid float value %v", value) +} + +func (f *FloatProperty) AppendProperty(resource *construct.Resource, value any) error { + return f.SetProperty(resource, value) +} + +func (f *FloatProperty) RemoveProperty(resource *construct.Resource, value any) error { + propVal, err := resource.GetProperty(f.Path) + if err != nil { + return err + } + if propVal == nil { + return nil + } + return resource.RemoveProperty(f.Path, value) + +} + +func (f *FloatProperty) Details() *knowledgebase.PropertyDetails { + return &f.PropertyDetails +} + +func (f *FloatProperty) Clone() knowledgebase.Property { + clone := *f + return &clone +} + +func (f *FloatProperty) GetDefaultValue(ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if f.DefaultValue == nil { + return nil, nil + } + return f.Parse(f.DefaultValue, ctx, data) +} + +func (f *FloatProperty) Parse(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if val, ok := value.(string); ok { + var result float32 + err := ctx.ExecuteDecode(val, data, &result) + return result, err + } + if val, ok := value.(float32); ok { + return val, nil + } + if val, ok := value.(float64); ok { + return val, nil + } + if val, ok := value.(int); ok { + return float64(val), nil + } + val, err := ParsePropertyRef(value, ctx, data) + if err == nil { + return val, nil + } + return nil, fmt.Errorf("invalid float value %v", value) +} + +func (f *FloatProperty) ZeroValue() any { + return 0.0 +} + +func (f *FloatProperty) Contains(value any, contains any) bool { + return false +} + +func (f *FloatProperty) Type() string { + return "float" +} + +func (f *FloatProperty) Validate(resource *construct.Resource, value any) error { + floatVal, ok := value.(float64) + if !ok { + return fmt.Errorf("invalid int value %v", value) + } + if f.MinValue != nil && floatVal < *f.MinValue { + return fmt.Errorf("int value %f is less than lower bound %f", value, *f.MinValue) + } + if f.MaxValue != nil && floatVal > *f.MaxValue { + return fmt.Errorf("int value %f is greater than upper bound %f", value, *f.MaxValue) + } + return nil +} + +func (f *FloatProperty) SubProperties() knowledgebase.Properties { + return nil +} diff --git a/pkg/knowledge_base2/properties/float_property_test.go b/pkg/knowledge_base2/properties/float_property_test.go new file mode 100644 index 000000000..ba876fda4 --- /dev/null +++ b/pkg/knowledge_base2/properties/float_property_test.go @@ -0,0 +1,338 @@ +package properties + +import ( + "testing" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase2 "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/stretchr/testify/assert" +) + +func Test_SetFloatProperty(t *testing.T) { + tests := []struct { + name string + property *FloatProperty + resource *construct.Resource + value float64 + }{ + { + name: "float property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1.0, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + err := test.property.SetProperty(test.resource, test.value) + if !assert.NoError(err) { + return + } + assert.Equal(test.value, test.resource.Properties[test.property.Path]) + }) + } +} + +func Test_AppendFloatProperty(t *testing.T) { + tests := []struct { + name string + property *FloatProperty + resource *construct.Resource + value float64 + }{ + { + name: "float property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1.0, + }, + { + name: "existing property", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": 1.0, + }, + }, + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 2.0, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + err := test.property.AppendProperty(test.resource, test.value) + if !assert.NoError(err) { + return + } + assert.Equal(test.value, test.resource.Properties[test.property.Path]) + }) + } +} + +func Test_RemoveFloatProperty(t *testing.T) { + tests := []struct { + name string + property *FloatProperty + resource *construct.Resource + }{ + { + name: "float property", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": 1.0, + }, + }, + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + }, + { + name: "nonexistent property", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": 1.0, + }, + }, + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test2", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + err := test.property.RemoveProperty(test.resource, nil) + if !assert.NoError(err) { + return + } + assert.NotContains(test.resource.Properties, test.property.Path) + }) + } +} + +func Test_ParseFloatValue(t *testing.T) { + tests := []struct { + name string + property *FloatProperty + value any + expect any + wantErr bool + }{ + { + name: "float value", + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1.0, + expect: 1.0, + }, + { + name: "template value", + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: "{{ 1.0 }}", + expect: float32(1.0), + }, + { + name: "int value", + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + expect: 1.0, + }, + { + name: "invalid value", + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: "sdf", + wantErr: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase2.DynamicValueContext{} + data := knowledgebase2.DynamicValueData{} + value, err := test.property.Parse(test.value, ctx, data) + if test.wantErr { + assert.Error(err) + return + } + if !assert.NoError(err) { + return + } + assert.Equal(test.expect, value) + }) + } +} + +func Test_FloatProperty_Validate(t *testing.T) { + tests := []struct { + name string + property *FloatProperty + value any + wantErr bool + }{ + { + name: "float value", + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1.0, + }, + { + name: "int value", + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + wantErr: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + resource := &construct.Resource{} + err := test.property.Validate(resource, test.value) + if test.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + }) + } +} + +func Test_FloatProperty_GetDefaultValue(t *testing.T) { + tests := []struct { + name string + property *FloatProperty + expect any + }{ + { + name: "default value", + property: &FloatProperty{ + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: 1.0, + }, + }, + expect: 1.0, + }, + { + name: "no default value", + property: &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + expect: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase2.DynamicValueContext{} + data := knowledgebase2.DynamicValueData{} + value, err := test.property.GetDefaultValue(ctx, data) + if !assert.NoError(err) { + return + } + assert.Equal(test.expect, value) + }) + } +} + +func Test_FloatPRoperty_Clone(t *testing.T) { + assert := assert.New(t) + property := &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + clone := property.Clone() + assert.Equal(property.Path, clone.Details().Path) + assert.NotSame(property, clone) +} + +func Test_FloatProperty_Details(t *testing.T) { + assert := assert.New(t) + property := &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + details := property.Details() + assert.Equal(property.Path, details.Path) +} + +func Test_FloatProperty_ZeroValue(t *testing.T) { + assert := assert.New(t) + property := &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + assert.Equal(float64(0.0), property.ZeroValue()) +} + +func Test_FloatProperty_Contains(t *testing.T) { + assert := assert.New(t) + property := &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + assert.False(property.Contains(1.0, 1.0)) +} + +func Test_FloatProperty_Type(t *testing.T) { + assert := assert.New(t) + property := &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + assert.Equal("float", property.Type()) +} + +func Test_FloatProperty_SubProperties(t *testing.T) { + assert := assert.New(t) + property := &FloatProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + assert.Empty(property.SubProperties()) +} diff --git a/pkg/knowledge_base2/properties/int_property.go b/pkg/knowledge_base2/properties/int_property.go new file mode 100644 index 000000000..10459fc1f --- /dev/null +++ b/pkg/knowledge_base2/properties/int_property.go @@ -0,0 +1,103 @@ +package properties + +import ( + "fmt" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) + +type ( + IntProperty struct { + MinValue *int + MaxValue *int + SharedPropertyFields + knowledgebase.PropertyDetails + } +) + +func (i *IntProperty) SetProperty(resource *construct.Resource, value any) error { + if val, ok := value.(int); ok { + return resource.SetProperty(i.Path, val) + } else if val, ok := value.(construct.PropertyRef); ok { + return resource.SetProperty(i.Path, val) + } + return fmt.Errorf("invalid int value %v", value) +} + +func (i *IntProperty) AppendProperty(resource *construct.Resource, value any) error { + return i.SetProperty(resource, value) +} + +func (i *IntProperty) RemoveProperty(resource *construct.Resource, value any) error { + propVal, err := resource.GetProperty(i.Path) + if err != nil { + return err + } + if propVal == nil { + return nil + } + return resource.RemoveProperty(i.Path, value) +} + +func (i *IntProperty) Details() *knowledgebase.PropertyDetails { + return &i.PropertyDetails +} + +func (i *IntProperty) Clone() knowledgebase.Property { + clone := *i + return &clone +} + +func (i *IntProperty) GetDefaultValue(ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if i.DefaultValue == nil { + return nil, nil + } + return i.Parse(i.DefaultValue, ctx, data) +} + +func (i *IntProperty) Parse(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if val, ok := value.(string); ok { + var result int + err := ctx.ExecuteDecode(val, data, &result) + return result, err + } + if val, ok := value.(int); ok { + return val, nil + } + val, err := ParsePropertyRef(value, ctx, data) + if err == nil { + return val, nil + } + return nil, fmt.Errorf("invalid int value %v", value) +} + +func (i *IntProperty) ZeroValue() any { + return 0 +} + +func (i *IntProperty) Contains(value any, contains any) bool { + return false +} + +func (i *IntProperty) Type() string { + return "int" +} + +func (i *IntProperty) Validate(resource *construct.Resource, value any) error { + intVal, ok := value.(int) + if !ok { + return fmt.Errorf("invalid int value %v", value) + } + if i.MinValue != nil && intVal < *i.MinValue { + return fmt.Errorf("int value %v is less than lower bound %d", value, *i.MinValue) + } + if i.MaxValue != nil && intVal > *i.MaxValue { + return fmt.Errorf("int value %v is greater than upper bound %d", value, *i.MaxValue) + } + return nil +} + +func (i *IntProperty) SubProperties() knowledgebase.Properties { + return nil +} diff --git a/pkg/knowledge_base2/properties/int_property_test.go b/pkg/knowledge_base2/properties/int_property_test.go new file mode 100644 index 000000000..4235e9be7 --- /dev/null +++ b/pkg/knowledge_base2/properties/int_property_test.go @@ -0,0 +1,359 @@ +package properties + +import ( + "testing" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase2 "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/stretchr/testify/assert" +) + +func Test_SetIntPropertyProperty(t *testing.T) { + tests := []struct { + name string + property *IntProperty + resource *construct.Resource + value int + }{ + { + name: "float property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + err := test.property.SetProperty(test.resource, test.value) + if !assert.NoError(err) { + return + } + assert.Equal(test.value, test.resource.Properties[test.property.Path]) + }) + } +} + +func Test_AppendIntProperty(t *testing.T) { + tests := []struct { + name string + property *IntProperty + resource *construct.Resource + value int + }{ + { + name: "float property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1.0, + }, + { + name: "existing property", + resource: &construct.Resource{ + Properties: construct.Properties{ + "test": 1.0, + }, + }, + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 2.0, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + err := test.property.AppendProperty(test.resource, test.value) + if !assert.NoError(err) { + return + } + assert.Equal(test.value, test.resource.Properties[test.property.Path]) + }) + } +} + +func Test_RemoveIntProperty(t *testing.T) { + tests := []struct { + name string + property *IntProperty + resource *construct.Resource + }{ + { + name: "int property", + resource: &construct.Resource{ + Properties: construct.Properties{ + "test": 1.0, + }, + }, + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + }, + { + name: "non existing property", + resource: &construct.Resource{ + Properties: construct.Properties{ + "test": 1.0, + }, + }, + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test2", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + err := test.property.RemoveProperty(test.resource, nil) + if !assert.NoError(err) { + return + } + assert.NotContains(test.resource.Properties, test.property.Path) + }) + } +} + +func Test_IntProperty_Parse(t *testing.T) { + tests := []struct { + name string + property *IntProperty + value any + expected int + wantErr bool + }{ + { + name: "int property", + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + expected: 1, + }, + { + name: "template value", + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test2", + }, + }, + value: "{{ 1 }}", + expected: 1, + }, + { + name: "non int property", + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + wantErr: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + ctx := &knowledgebase2.DynamicValueContext{} + data := knowledgebase2.DynamicValueData{} + value, err := test.property.Parse(test.value, ctx, data) + if test.wantErr { + assert.Error(err) + return + } + if !assert.NoError(err) { + return + } + assert.Equal(test.expected, value) + }) + } +} + +func Test_IntProperty_Validate(t *testing.T) { + upperBound := 10 + lowerBound := 0 + tests := []struct { + name string + property *IntProperty + value any + wantErr bool + }{ + { + name: "int property", + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + }, + { + name: "within bounds", + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + MaxValue: &upperBound, + MinValue: &lowerBound, + }, + value: 5, + }, + { + name: "above upper bound", + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + MaxValue: &upperBound, + MinValue: &lowerBound, + }, + value: 11, + wantErr: true, + }, + { + name: "below lower bound", + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + MaxValue: &upperBound, + MinValue: &lowerBound, + }, + value: -1, + wantErr: true, + }, + { + name: "non int property", + property: &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + wantErr: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + resource := &construct.Resource{} + err := test.property.Validate(resource, test.value) + if test.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + }) + } +} + +func Test_IntProperty_Clone(t *testing.T) { + assert := assert.New(t) + property := &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + actual := property.Clone() + assert.Equal(property, actual) + assert.NotSame(property, actual) +} + +func Test_IntProperty_Details(t *testing.T) { + assert := assert.New(t) + property := &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + actual := property.Details() + assert.Equal(property.PropertyDetails, *actual) + assert.Same(&property.PropertyDetails, actual) +} + +func Test_IntProperty_Type(t *testing.T) { + assert := assert.New(t) + property := &IntProperty{} + assert.Equal(property.Type(), "int") +} + +func Test_IntProperty_ZeroValue(t *testing.T) { + assert := assert.New(t) + property := &IntProperty{} + assert.Equal(property.ZeroValue(), 0) +} + +func Test_IntProperty_GetDefaultValue(t *testing.T) { + tests := []struct { + name string + property *IntProperty + expect any + }{ + { + name: "default value", + property: &IntProperty{ + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: 1, + }, + }, + expect: 1, + }, + { + name: "no default value", + property: &IntProperty{}, + expect: nil, + }, + { + name: "default value with template", + property: &IntProperty{ + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: "{{ 1 }}", + }, + }, + expect: 1, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase2.DynamicValueContext{} + data := knowledgebase2.DynamicValueData{} + actual, err := test.property.GetDefaultValue(ctx, data) + if !assert.NoError(err) { + return + } + assert.Equal(test.expect, actual) + }) + } +} + +func Test_IntProperty_Contains(t *testing.T) { + assert := assert.New(t) + property := &IntProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + assert.False(property.Contains(1, 1)) +} + +func Test_IntProperty_SubProperties(t *testing.T) { + assert := assert.New(t) + property := &IntProperty{} + assert.Nil(property.SubProperties()) +} diff --git a/pkg/knowledge_base2/properties/list_property.go b/pkg/knowledge_base2/properties/list_property.go new file mode 100644 index 000000000..cb5e805f4 --- /dev/null +++ b/pkg/knowledge_base2/properties/list_property.go @@ -0,0 +1,213 @@ +package properties + +import ( + "errors" + "fmt" + "reflect" + "strings" + + "github.com/klothoplatform/klotho/pkg/collectionutil" + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) + +type ( + ListProperty struct { + MinLength *int + MaxLength *int + ItemProperty knowledgebase.Property + Properties knowledgebase.Properties + SharedPropertyFields + knowledgebase.PropertyDetails + } +) + +func (l *ListProperty) SetProperty(resource *construct.Resource, value any) error { + if val, ok := value.([]any); ok { + return resource.SetProperty(l.Path, val) + } + return fmt.Errorf("invalid list value %v", value) +} + +func (l *ListProperty) AppendProperty(resource *construct.Resource, value any) error { + propval, err := resource.GetProperty(l.Path) + if err != nil { + return err + } + if propval == nil { + err := l.SetProperty(resource, []any{}) + if err != nil { + return err + } + } + if l.ItemProperty != nil && !strings.HasPrefix(l.ItemProperty.Type(), "list") { + if reflect.ValueOf(value).Kind() == reflect.Slice || reflect.ValueOf(value).Kind() == reflect.Array { + var errs error + for i := 0; i < reflect.ValueOf(value).Len(); i++ { + err := resource.AppendProperty(l.Path, reflect.ValueOf(value).Index(i).Interface()) + if err != nil { + errs = errors.Join(errs, err) + } + } + return errs + } + } + return resource.AppendProperty(l.Path, value) +} + +func (l *ListProperty) RemoveProperty(resource *construct.Resource, value any) error { + propval, err := resource.GetProperty(l.Path) + if err != nil { + return err + } + if propval == nil { + return nil + } + if l.ItemProperty != nil && !strings.HasPrefix(l.ItemProperty.Type(), "list") { + if reflect.ValueOf(value).Kind() == reflect.Slice || reflect.ValueOf(value).Kind() == reflect.Array { + var errs error + for i := 0; i < reflect.ValueOf(value).Len(); i++ { + err := resource.RemoveProperty(l.Path, reflect.ValueOf(value).Index(i).Interface()) + if err != nil { + errs = errors.Join(errs, err) + } + } + return errs + } + } + return resource.RemoveProperty(l.Path, value) +} + +func (l *ListProperty) Details() *knowledgebase.PropertyDetails { + return &l.PropertyDetails +} + +func (l *ListProperty) Clone() knowledgebase.Property { + var itemProp knowledgebase.Property + if l.ItemProperty != nil { + itemProp = l.ItemProperty.Clone() + } + var props knowledgebase.Properties + if l.Properties != nil { + props = l.Properties.Clone() + } + clone := *l + clone.ItemProperty = itemProp + clone.Properties = props + return &clone +} + +func (list *ListProperty) GetDefaultValue(ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if list.DefaultValue == nil { + return nil, nil + } + return list.Parse(list.DefaultValue, ctx, data) +} + +func (list *ListProperty) Parse(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + + var result []any + val, ok := value.([]any) + if !ok { + // before we fail, check to see if the entire value is a template + if strVal, ok := value.(string); ok { + var result []any + err := ctx.ExecuteDecode(strVal, data, &result) + return result, err + } + return nil, fmt.Errorf("invalid list value %v", value) + } + + for _, v := range val { + if len(list.Properties) != 0 { + m := MapProperty{Properties: list.Properties} + val, err := m.Parse(v, ctx, data) + if err != nil { + return nil, err + } + result = append(result, val) + } else { + val, err := list.ItemProperty.Parse(v, ctx, data) + if err != nil { + return nil, err + } + result = append(result, val) + } + } + return result, nil +} + +func (l *ListProperty) ZeroValue() any { + return nil +} + +func (l *ListProperty) Contains(value any, contains any) bool { + list, ok := value.([]any) + if !ok { + return false + } + containsList, ok := contains.([]any) + if !ok { + return collectionutil.Contains(list, contains) + } + for _, v := range list { + for _, cv := range containsList { + if reflect.DeepEqual(v, cv) { + return true + } + } + } + return false +} + +func (l *ListProperty) Type() string { + if l.ItemProperty != nil { + return fmt.Sprintf("list(%s)", l.ItemProperty.Type()) + } + return "list" +} + +func (l *ListProperty) Validate(resource *construct.Resource, value any) error { + listVal, ok := value.([]any) + if !ok { + return fmt.Errorf("invalid map value %v", value) + } + if l.MinLength != nil { + if len(listVal) < *l.MinLength { + return fmt.Errorf("list value %v is too short. min length is %d", value, *l.MinLength) + } + } + if l.MaxLength != nil { + if len(listVal) > *l.MaxLength { + return fmt.Errorf("list value %v is too long. max length is %d", value, *l.MaxLength) + } + } + var errs error + + for _, v := range listVal { + if len(l.Properties) != 0 { + m := MapProperty{Properties: l.Properties} + err := m.Validate(resource, v) + if err != nil { + errs = errors.New(errs.Error() + "\n" + err.Error()) + } + } else { + err := l.ItemProperty.Validate(resource, v) + if err != nil { + errs = errors.New(errs.Error() + "\n" + err.Error()) + } + } + } + if errs != nil { + return errs + } + return nil +} + +func (l *ListProperty) SubProperties() knowledgebase.Properties { + return l.Properties +} + +func (l *ListProperty) Item() knowledgebase.Property { + return l.ItemProperty +} diff --git a/pkg/knowledge_base2/properties/list_property_test.go b/pkg/knowledge_base2/properties/list_property_test.go new file mode 100644 index 000000000..9aadcc7a1 --- /dev/null +++ b/pkg/knowledge_base2/properties/list_property_test.go @@ -0,0 +1,419 @@ +package properties + +import ( + "testing" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/stretchr/testify/assert" +) + +func Test_ListProperty_Set(t *testing.T) { + tests := []struct { + name string + property *ListProperty + resource *construct.Resource + value []any + }{ + { + name: "list property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: []any{"test"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.SetProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value, tt.resource.Properties[tt.property.Path]) + }) + } +} + +func Test_ListProperty_Append(t *testing.T) { + tests := []struct { + name string + property *ListProperty + resource *construct.Resource + value any + expect any + }{ + { + name: "list property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + expect: []any{"test"}, + }, + { + name: "existing property", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": []any{"first"}, + }, + }, + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + expect: []any{"first", "test"}, + }, + { + name: "appends list of values", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": []any{"first"}, + }, + }, + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + }, + value: []any{"test"}, + expect: []any{"first", "test"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.AppendProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.expect, tt.resource.Properties[tt.property.Path]) + }) + } +} + +func Test_ListProperty_Remove(t *testing.T) { + tests := []struct { + name string + property *ListProperty + resource *construct.Resource + value any + expect any + }{ + { + name: "list property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + }, + { + name: "existing property", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": []any{"first", "test"}, + }, + }, + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + expect: []any{"first"}, + }, + { + name: "removes list of values", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": []any{"first", "test"}, + }, + }, + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + }, + value: []any{"test"}, + expect: []any{"first"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.RemoveProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.expect, tt.resource.Properties[tt.property.Path]) + }) + } +} + +func Test_ListProperty_Clone(t *testing.T) { + assert := assert.New(t) + property := &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + } + clone := property.Clone() + assert.Equal(property, clone) + assert.NotSame(property, clone) + clonedListP := clone.(*ListProperty) + assert.NotSame(property.ItemProperty, clonedListP.ItemProperty) +} + +func Test_ListProperty_Details(t *testing.T) { + assert := assert.New(t) + property := &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + } + details := property.Details() + assert.Same(&property.PropertyDetails, details) +} + +func Test_ListProperty_GetDefaultValue(t *testing.T) { + tests := []struct { + name string + property *ListProperty + expect any + }{ + { + name: "list property", + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: []any{"test"}, + }, + ItemProperty: &StringProperty{}, + }, + expect: []any{"test"}, + }, + { + name: "list property with template value", + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: []any{"{{ \"test\" }}"}, + }, + }, + expect: []any{"test"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase.DynamicValueContext{} + data := knowledgebase.DynamicValueData{} + result, err := tt.property.GetDefaultValue(ctx, data) + if !assert.NoError(err) { + return + } + assert.Equal(tt.expect, result) + }) + } +} + +func Test_ListProperty_Parse(t *testing.T) { + tests := []struct { + name string + property *ListProperty + value any + expect any + }{ + { + name: "list property", + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + }, + value: []any{"test"}, + expect: []any{"test"}, + }, + { + name: "list property with template value", + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + }, + value: []any{"{{ \"test\" }}"}, + expect: []any{"test"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase.DynamicValueContext{} + data := knowledgebase.DynamicValueData{} + result, err := tt.property.Parse(tt.value, ctx, data) + if !assert.NoError(err) { + return + } + assert.Equal(tt.expect, result) + }) + } +} + +func Test_ListProperty_Validate(t *testing.T) { + minLength := 1 + maxLength := 2 + tests := []struct { + name string + property *ListProperty + value any + wantErr bool + }{ + { + name: "list property", + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + MinLength: &minLength, + MaxLength: &maxLength, + ItemProperty: &StringProperty{}, + }, + value: []any{"test"}, + }, + { + name: "list property over max length", + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + MinLength: &minLength, + MaxLength: &maxLength, + ItemProperty: &StringProperty{}, + }, + value: []any{"test", "test", "test"}, + wantErr: true, + }, + { + name: "list property under min length", + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + MinLength: &minLength, + MaxLength: &maxLength, + ItemProperty: &StringProperty{}, + }, + value: []any{}, + wantErr: true, + }, + { + name: "list property checks item property validation", + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + MinLength: &minLength, + MaxLength: &maxLength, + ItemProperty: &StringProperty{AllowedValues: []string{"val"}}, + }, + value: []any{"test", "test", "test"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + resource := &construct.Resource{} + err := tt.property.Validate(resource, tt.value) + if tt.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + }) + } +} + +func Test_ListProperty_Contains(t *testing.T) { + tests := []struct { + name string + property *ListProperty + value any + expect bool + }{ + { + name: "list property", + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + }, + value: []any{"test"}, + expect: true, + }, + { + name: "list property does not conatin", + property: &ListProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + }, + value: []any{"tttt"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + result := tt.property.Contains(tt.value, "test") + assert.Equal(tt.expect, result) + }) + } +} + +func Test_ListProperty_ZeroValue(t *testing.T) { + assert := assert.New(t) + property := &ListProperty{} + assert.Nil(property.ZeroValue()) +} + +func Test_ListProperty_Type(t *testing.T) { + assert := assert.New(t) + property := &ListProperty{} + assert.Equal("list", property.Type()) + property2 := &ListProperty{ + ItemProperty: &StringProperty{}, + } + assert.Equal("list(string)", property2.Type()) +} + +func Test_ListProperty_SubProperties(t *testing.T) { + assert := assert.New(t) + property := &ListProperty{ + Properties: make(knowledgebase.Properties), + } + assert.NotNil(property.SubProperties()) +} diff --git a/pkg/knowledge_base2/properties/map_property.go b/pkg/knowledge_base2/properties/map_property.go new file mode 100644 index 000000000..25319ae50 --- /dev/null +++ b/pkg/knowledge_base2/properties/map_property.go @@ -0,0 +1,227 @@ +package properties + +import ( + "errors" + "fmt" + "reflect" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) + +type ( + MapProperty struct { + MinLength *int + MaxLength *int + KeyProperty knowledgebase.Property + ValueProperty knowledgebase.Property + Properties knowledgebase.Properties + SharedPropertyFields + knowledgebase.PropertyDetails + } +) + +func (m *MapProperty) SetProperty(resource *construct.Resource, value any) error { + if val, ok := value.(map[string]any); ok { + return resource.SetProperty(m.Path, val) + } + return fmt.Errorf("invalid resource value %v", value) +} + +func (m *MapProperty) AppendProperty(resource *construct.Resource, value any) error { + return resource.AppendProperty(m.Path, value) +} + +func (m *MapProperty) RemoveProperty(resource *construct.Resource, value any) error { + propVal, err := resource.GetProperty(m.Path) + if err != nil { + return err + } + if propVal == nil { + return nil + } + propMap, ok := propVal.(map[string]any) + if !ok { + return fmt.Errorf("error attempting to remove map property: invalid property value %v", propVal) + } + if val, ok := value.(map[string]any); ok { + for k, v := range val { + if val, found := propMap[k]; found && reflect.DeepEqual(val, v) { + delete(propMap, k) + } + } + return resource.SetProperty(m.Path, propMap) + } + return resource.RemoveProperty(m.Path, value) +} + +func (m *MapProperty) Details() *knowledgebase.PropertyDetails { + return &m.PropertyDetails +} + +func (m *MapProperty) Clone() knowledgebase.Property { + var keyProp knowledgebase.Property + if m.KeyProperty != nil { + keyProp = m.KeyProperty.Clone() + } + var valProp knowledgebase.Property + if m.ValueProperty != nil { + valProp = m.ValueProperty.Clone() + } + var props knowledgebase.Properties + if m.Properties != nil { + props = m.Properties.Clone() + } + clone := *m + clone.KeyProperty = keyProp + clone.ValueProperty = valProp + clone.Properties = props + return &clone +} + +func (m *MapProperty) GetDefaultValue(ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if m.DefaultValue == nil { + return nil, nil + } + return m.Parse(m.DefaultValue, ctx, data) +} + +func (m *MapProperty) Parse(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + result := map[string]any{} + + mapVal, ok := value.(map[string]any) + if !ok { + // before we fail, check to see if the entire value is a template + if strVal, ok := value.(string); ok { + err := ctx.ExecuteDecode(strVal, data, &result) + return result, err + } + mapVal, ok = value.(construct.Properties) + if !ok { + return nil, fmt.Errorf("invalid map value %v", value) + } + } + // If we are an object with sub properties then we know that we need to get the type of our sub properties to determine how we are parsed into a value + if len(m.Properties) != 0 { + var errs error + for key, prop := range m.Properties { + if _, found := mapVal[key]; found { + val, err := prop.Parse(mapVal[key], ctx, data) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("unable to parse value for sub property %s: %w", key, err)) + continue + } + result[key] = val + } + } + return result, nil + } + + // Else we are a set type of map and can just loop over the values + for key, v := range mapVal { + keyVal, err := m.KeyProperty.Parse(key, ctx, data) + if err != nil { + return nil, err + } + val, err := m.ValueProperty.Parse(v, ctx, data) + if err != nil { + return nil, err + } + switch keyVal := keyVal.(type) { + case string: + result[keyVal] = val + case construct.ResourceId: + result[keyVal.String()] = val + case construct.PropertyRef: + result[keyVal.String()] = val + default: + return nil, fmt.Errorf("invalid key type for map property type %s", keyVal) + } + } + return result, nil +} + +func (m *MapProperty) ZeroValue() any { + return nil +} + +func (m *MapProperty) Contains(value any, contains any) bool { + mapVal, ok := value.(map[string]any) + if !ok { + return false + } + containsMap, ok := contains.(map[string]any) + if !ok { + return false + } + for k, v := range containsMap { + if val, found := mapVal[k]; found || reflect.DeepEqual(val, v) { + return true + } + } + for _, v := range mapVal { + for _, cv := range containsMap { + if reflect.DeepEqual(v, cv) { + return true + } + } + } + return false +} + +func (m *MapProperty) Type() string { + if m.KeyProperty != nil && m.ValueProperty != nil { + return fmt.Sprintf("map(%s,%s)", m.KeyProperty.Type(), m.ValueProperty.Type()) + } + return "map" +} + +func (m *MapProperty) Validate(resource *construct.Resource, value any) error { + mapVal, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("invalid map value %v", value) + } + if m.MinLength != nil { + if len(mapVal) < *m.MinLength { + return fmt.Errorf("map value %v is too short. min length is %d", value, *m.MinLength) + } + } + if m.MaxLength != nil { + if len(mapVal) > *m.MaxLength { + return fmt.Errorf("map value %v is too long. max length is %d", value, *m.MaxLength) + } + } + var errs error + if m.KeyProperty != nil && m.ValueProperty != nil { + for k, v := range mapVal { + if err := m.KeyProperty.Validate(resource, k); err != nil { + errs = errors.Join(errs, fmt.Errorf("invalid key %v for map property type %s: %w", k, m.KeyProperty.Type(), err)) + } + if err := m.ValueProperty.Validate(resource, v); err != nil { + errs = errors.Join(errs, fmt.Errorf("invalid value %v for map property type %s: %w", v, m.ValueProperty.Type(), err)) + } + } + } else { + for _, v := range mapVal { + if err := m.ValueProperty.Validate(resource, v); err != nil { + errs = errors.Join(errs, fmt.Errorf("invalid value %v for map property type %s: %w", v, m.ValueProperty.Type(), err)) + } + } + } + if errs != nil { + return errs + } + return nil +} + +func (m *MapProperty) SubProperties() knowledgebase.Properties { + return m.Properties +} + +func (m *MapProperty) Key() knowledgebase.Property { + return m.KeyProperty +} + +func (m *MapProperty) Value() knowledgebase.Property { + return m.ValueProperty +} diff --git a/pkg/knowledge_base2/properties/map_property_test.go b/pkg/knowledge_base2/properties/map_property_test.go new file mode 100644 index 000000000..9fa59d32c --- /dev/null +++ b/pkg/knowledge_base2/properties/map_property_test.go @@ -0,0 +1,463 @@ +package properties + +import ( + "testing" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase2 "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/stretchr/testify/assert" +) + +func Test_SetMapProperty(t *testing.T) { + tests := []struct { + name string + property *MapProperty + resource *construct.Resource + value any + }{ + { + name: "Set map property value", + property: &MapProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + resource: &construct.Resource{ + Properties: make(construct.Properties), + }, + value: map[string]interface{}{ + "key": "test", + "value": "test", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + err := test.property.SetProperty(test.resource, test.value) + if !assert.NoError(err) { + return + } + assert.Equal(test.value, test.resource.Properties[test.property.Path]) + }) + } + +} + +func Test_AppendMapProperty(t *testing.T) { + tests := []struct { + name string + property *MapProperty + resource *construct.Resource + value any + expected any + }{ + { + name: "Append map property value", + property: &MapProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + resource: &construct.Resource{ + Properties: make(construct.Properties), + }, + value: map[string]interface{}{ + "key": "test", + "value": "test", + }, + expected: map[string]interface{}{ + "key": "test", + "value": "test", + }, + }, + { + name: "Append existing map property value", + property: &MapProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + resource: &construct.Resource{ + Properties: map[string]any{ + "test": map[string]interface{}{ + "key": "test", + "value": "test", + }, + }, + }, + value: map[string]interface{}{ + "key2": "test", + "value2": "test", + }, + expected: map[string]interface{}{ + "key": "test", + "value": "test", + "key2": "test", + "value2": "test", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + err := test.property.AppendProperty(test.resource, test.value) + if !assert.NoError(err) { + return + } + assert.Equal(test.expected, test.resource.Properties[test.property.Path]) + }) + } +} + +func Test_RemoveMapProperty(t *testing.T) { + tests := []struct { + name string + property *MapProperty + resource *construct.Resource + value any + expected any + }{ + { + name: "Remove map property value", + property: &MapProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + resource: &construct.Resource{ + Properties: map[string]any{ + "test": map[string]interface{}{ + "key": "test", + "value": "test", + }, + }, + }, + value: map[string]interface{}{ + "key": "test", + "value": "test", + }, + expected: map[string]interface{}{}, + }, + { + name: "Remove portion of value", + property: &MapProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + resource: &construct.Resource{ + Properties: map[string]any{ + "test": map[string]interface{}{ + "key": "test", + "value": "test", + "key2": "test", + "value2": "test", + }, + }, + }, + value: map[string]interface{}{ + "key2": "test", + "value2": "test", + }, + expected: map[string]interface{}{ + "key": "test", + "value": "test", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + err := test.property.RemoveProperty(test.resource, test.value) + if !assert.NoError(err) { + return + } + assert.Equal(test.expected, test.resource.Properties[test.property.Path]) + }) + } +} + +func Test_MapProperty_Details(t *testing.T) { + assert := assert.New(t) + property := &MapProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + details := property.Details() + assert.Same(&property.PropertyDetails, details) +} + +func Test_MapProperty_Clone(t *testing.T) { + assert := assert.New(t) + property := &MapProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + clone := property.Clone() + assert.Equal(property, clone) + assert.NotSame(property, clone) +} + +func Test_MapProperty_Type(t *testing.T) { + assert := assert.New(t) + property := &MapProperty{} + assert.Equal("map", property.Type()) + property2 := &MapProperty{KeyProperty: &StringProperty{}, ValueProperty: &StringProperty{}} + assert.Equal("map(string,string)", property2.Type()) +} + +func Test_MapProperty_Validate(t *testing.T) { + tests := []struct { + name string + property *MapProperty + value any + wantErr bool + }{ + { + name: "valid map property", + property: &MapProperty{ + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + value: map[string]interface{}{ + "key": "test", + "value": "test", + }, + }, + { + name: "tests key validity", + property: &MapProperty{ + KeyProperty: &StringProperty{AllowedValues: []string{"test"}}, + ValueProperty: &StringProperty{}, + }, + value: map[string]interface{}{ + "key": "test", + }, + wantErr: true, + }, + { + name: "tests val validity", + property: &MapProperty{ + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{AllowedValues: []string{"test"}}, + }, + value: map[string]interface{}{ + "key": "not-test", + }, + wantErr: true, + }, + { + name: "invalid map property", + property: &MapProperty{ + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + value: "test", + wantErr: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + resource := &construct.Resource{} + err := test.property.Validate(resource, test.value) + if test.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + }) + } +} + +func Test_MapProperty_GetDefaultValue(t *testing.T) { + tests := []struct { + name string + property *MapProperty + want any + }{ + { + name: "returns empty map", + property: &MapProperty{ + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + }, + { + name: "returns default map", + property: &MapProperty{ + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: map[string]interface{}{ + "key": "test", + "value": "test", + }, + }, + }, + want: map[string]interface{}{ + "key": "test", + "value": "test", + }, + }, + { + name: "returns default map with templates", + property: &MapProperty{ + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: map[string]interface{}{ + "{{ \"key\" }}": "{{ \"Name\" }}", + "{{ \"value\" }}": "{{ \"Name\" }}", + }, + }, + }, + want: map[string]interface{}{ + "key": "Name", + "value": "Name", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase2.DynamicValueContext{} + data := knowledgebase2.DynamicValueData{} + actual, err := test.property.GetDefaultValue(ctx, data) + if !assert.NoError(err) { + return + } + assert.Equal(test.want, actual) + }) + } +} + +func Test_MapProperty_Parse(t *testing.T) { + tests := []struct { + name string + property *MapProperty + value any + want any + wantErr bool + }{ + { + name: "parses map property", + property: &MapProperty{ + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + value: map[string]interface{}{ + "key": "test", + "value": "test", + }, + want: map[string]interface{}{ + "key": "test", + "value": "test", + }, + }, + { + name: "parses map property with templates", + property: &MapProperty{ + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + value: map[string]interface{}{ + "{{ \"key\" }}": "{{ \"Name\" }}", + "{{ \"value\" }}": "{{ \"Name\" }}", + }, + want: map[string]interface{}{ + "key": "Name", + "value": "Name", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase2.DynamicValueContext{} + data := knowledgebase2.DynamicValueData{} + actual, err := test.property.Parse(test.value, ctx, data) + if test.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Equal(test.want, actual) + }) + } +} + +func Test_MapProperty_Contains(t *testing.T) { + tests := []struct { + name string + property *MapProperty + value any + contains any + want bool + }{ + { + name: "contains value", + property: &MapProperty{ + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + value: map[string]interface{}{ + "key": "test", + "value": "test", + }, + contains: map[string]interface{}{ + "key": "test", + }, + want: true, + }, + { + name: "does not contain value", + property: &MapProperty{ + KeyProperty: &StringProperty{}, + ValueProperty: &StringProperty{}, + }, + value: map[string]interface{}{ + "key": "test", + "value": "test", + }, + contains: map[string]interface{}{ + "key": "not-test", + }, + want: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + actual := test.property.Contains(test.value, test.contains) + assert.Equal(test.want, actual) + }) + } +} + +func Test_MapProperty_ZeroValue(t *testing.T) { + assert := assert.New(t) + property := &MapProperty{} + assert.Nil(property.ZeroValue()) +} + +func Test_MapProperty_SubProperties(t *testing.T) { + assert := assert.New(t) + property := &MapProperty{ + Properties: knowledgebase2.Properties{}, + } + assert.NotNil(property.SubProperties()) +} diff --git a/pkg/knowledge_base2/properties/property.go b/pkg/knowledge_base2/properties/property.go new file mode 100644 index 000000000..500ea8ce8 --- /dev/null +++ b/pkg/knowledge_base2/properties/property.go @@ -0,0 +1,65 @@ +package properties + +import ( + "bytes" + "fmt" + "text/template" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) + +type ( + SharedPropertyFields struct { + DefaultValue any + ValidityChecks []PropertyValidityCheck + } + + PropertyValidityCheck struct { + template *template.Template + } + ValidityCheckData struct { + Properties construct.Properties `json:"properties" yaml:"properties"` + Value any `json:"value" yaml:"value"` + } +) + +func ParsePropertyRef(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (construct.PropertyRef, error) { + if val, ok := value.(string); ok { + result := construct.PropertyRef{} + err := ctx.ExecuteDecode(val, data, &result) + return result, err + } + if val, ok := value.(map[string]interface{}); ok { + rp := ResourceProperty{} + id, err := rp.Parse(val["resource"], ctx, data) + if err != nil { + return construct.PropertyRef{}, err + } + return construct.PropertyRef{ + Property: val["property"].(string), + Resource: id.(construct.ResourceId), + }, nil + } + if val, ok := value.(construct.PropertyRef); ok { + return val, nil + } + return construct.PropertyRef{}, fmt.Errorf("invalid property reference value %v", value) +} + +func (p *PropertyValidityCheck) Validate(value any, properties construct.Properties) error { + var buff bytes.Buffer + data := ValidityCheckData{ + Properties: properties, + Value: value, + } + err := p.template.Execute(&buff, data) + if err != nil { + return err + } + result := buff.String() + if result != "" { + return fmt.Errorf("invalid value %v: %s", value, result) + } + return nil +} diff --git a/pkg/knowledge_base2/properties/resource_property.go b/pkg/knowledge_base2/properties/resource_property.go new file mode 100644 index 000000000..5d559cfea --- /dev/null +++ b/pkg/knowledge_base2/properties/resource_property.go @@ -0,0 +1,142 @@ +package properties + +import ( + "fmt" + + "github.com/klothoplatform/klotho/pkg/collectionutil" + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) + +type ( + ResourceProperty struct { + AllowedTypes construct.ResourceList + SharedPropertyFields + knowledgebase.PropertyDetails + } +) + +func (r *ResourceProperty) SetProperty(resource *construct.Resource, value any) error { + if val, ok := value.(construct.ResourceId); ok { + return resource.SetProperty(r.Path, val) + } else if val, ok := value.(construct.PropertyRef); ok { + return resource.SetProperty(r.Path, val) + } + return fmt.Errorf("invalid resource value %v", value) +} + +func (r *ResourceProperty) AppendProperty(resource *construct.Resource, value any) error { + return r.SetProperty(resource, value) +} + +func (r *ResourceProperty) RemoveProperty(resource *construct.Resource, value any) error { + propVal, err := resource.GetProperty(r.Path) + if err != nil { + return err + } + if propVal == nil { + return nil + } + propId, ok := propVal.(construct.ResourceId) + if !ok { + return fmt.Errorf("error attempting to remove resource property: invalid property value %v", propVal) + } + valId, ok := value.(construct.ResourceId) + if !ok { + return fmt.Errorf("error attempting to remove resource property: invalid resource value %v", value) + } + if !propId.Matches(valId) { + return fmt.Errorf("error attempting to remove resource property: resource value %v does not match property value %v", value, propVal) + } + return resource.RemoveProperty(r.Path, value) +} + +func (r *ResourceProperty) Details() *knowledgebase.PropertyDetails { + return &r.PropertyDetails +} +func (r *ResourceProperty) Clone() knowledgebase.Property { + clone := *r + return &clone +} + +func (r *ResourceProperty) GetDefaultValue(ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if r.DefaultValue == nil { + return nil, nil + } + return r.Parse(r.DefaultValue, ctx, data) +} + +func (r *ResourceProperty) Parse(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if val, ok := value.(string); ok { + id, err := knowledgebase.ExecuteDecodeAsResourceId(ctx, val, data) + if !id.IsZero() && len(r.AllowedTypes) > 0 && !r.AllowedTypes.MatchesAny(id) { + return nil, fmt.Errorf("resource value %v does not match allowed types %s", value, r.AllowedTypes) + } + return id, err + } + if val, ok := value.(map[string]interface{}); ok { + id := construct.ResourceId{ + Type: val["type"].(string), + Name: val["name"].(string), + Provider: val["provider"].(string), + } + if namespace, ok := val["namespace"]; ok { + id.Namespace = namespace.(string) + } + if len(r.AllowedTypes) > 0 && !r.AllowedTypes.MatchesAny(id) { + return nil, fmt.Errorf("resource value %v does not match type %s", value, r.AllowedTypes) + } + return id, nil + } + if val, ok := value.(construct.ResourceId); ok { + if len(r.AllowedTypes) > 0 && !r.AllowedTypes.MatchesAny(val) { + return nil, fmt.Errorf("resource value %v does not match type %s", value, r.AllowedTypes) + } + return val, nil + } + val, err := ParsePropertyRef(value, ctx, data) + if err == nil { + if len(r.AllowedTypes) > 0 && !r.AllowedTypes.MatchesAny(val.Resource) { + return nil, fmt.Errorf("resource value %v does not match type %s", value, r.AllowedTypes) + } + return val, nil + } + return nil, fmt.Errorf("invalid resource value %v", value) +} + +func (r *ResourceProperty) ZeroValue() any { + return construct.ResourceId{} +} + +func (r *ResourceProperty) Contains(value any, contains any) bool { + return false +} + +func (r *ResourceProperty) Type() string { + if len(r.AllowedTypes) > 0 { + typeString := "" + for i, t := range r.AllowedTypes { + typeString += t.String() + if i < len(r.AllowedTypes)-1 { + typeString += ", " + } + } + return fmt.Sprintf("resource(%s)", typeString) + } + return "resource" +} + +func (r *ResourceProperty) Validate(resource *construct.Resource, value any) error { + id, ok := value.(construct.ResourceId) + if !ok { + return fmt.Errorf("invalid resource value %v", value) + } + if !collectionutil.Contains(r.AllowedTypes, id) { + return fmt.Errorf("resource value %v does not match allowed types %s", value, r.AllowedTypes) + } + return nil +} + +func (r *ResourceProperty) SubProperties() knowledgebase.Properties { + return nil +} diff --git a/pkg/knowledge_base2/properties/resource_property_test.go b/pkg/knowledge_base2/properties/resource_property_test.go new file mode 100644 index 000000000..456fca982 --- /dev/null +++ b/pkg/knowledge_base2/properties/resource_property_test.go @@ -0,0 +1,312 @@ +package properties + +import ( + "testing" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase2 "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/stretchr/testify/assert" +) + +func Test_SetResourceProperty(t *testing.T) { + tests := []struct { + name string + property *ResourceProperty + resource *construct.Resource + value construct.ResourceId + }{ + { + name: "resource property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: construct.ResourceId{Provider: "test"}, + }, + { + name: "existing resource property", + resource: &construct.Resource{Properties: map[string]any{"test": construct.ResourceId{Provider: "first"}}}, + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: construct.ResourceId{Provider: "test"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.SetProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value, tt.resource.Properties[tt.property.Path]) + }) + } +} + +func Test_AppendResourceProperty(t *testing.T) { + tests := []struct { + name string + property *ResourceProperty + resource *construct.Resource + value construct.ResourceId + }{ + { + name: "resource property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: construct.ResourceId{Provider: "test"}, + }, + { + name: "existing resource property", + resource: &construct.Resource{Properties: map[string]any{"test": construct.ResourceId{Provider: "first"}}}, + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: construct.ResourceId{Provider: "test"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.AppendProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value, tt.resource.Properties[tt.property.Path]) + }) + } +} + +func Test_RemoveResourceProperty(t *testing.T) { + tests := []struct { + name string + property *ResourceProperty + resource *construct.Resource + value construct.ResourceId + wantErr bool + }{ + { + name: "resource property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: construct.ResourceId{Provider: "test"}, + }, + { + name: "existing property", + resource: &construct.Resource{Properties: map[string]any{"test": construct.ResourceId{Provider: "first"}}}, + + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: construct.ResourceId{Provider: "test"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.RemoveProperty(tt.resource, tt.value) + + if tt.wantErr { + assert.Error(err) + assert.NotNil(tt.resource.Properties[tt.property.Path]) + } else { + if !assert.NoError(err) { + return + } + assert.Nil(tt.resource.Properties[tt.property.Path]) + } + }) + } +} + +func Test_ResourceProperty_GetDefaultValue(t *testing.T) { + tests := []struct { + name string + property *ResourceProperty + ctx knowledgebase2.DynamicValueContext + data knowledgebase2.DynamicValueData + want construct.ResourceId + wantErr bool + }{ + { + name: "default value", + property: &ResourceProperty{ + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: "mock:resource:r1", + }, + }, + want: construct.ResourceId{Provider: "mock", Type: "resource", Name: "r1"}, + }, + { + name: "default value with template", + property: &ResourceProperty{ + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: "{{ \"mock:resource:r1\" }}", + }, + }, + want: construct.ResourceId{Provider: "mock", Type: "resource", Name: "r1"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + actual, err := tt.property.GetDefaultValue(tt.ctx, tt.data) + if err != nil { + t.Fatal(err) + } + assert.Equal(tt.want, actual) + }) + } +} + +func Test_ResourceProperty_Parse(t *testing.T) { + tests := []struct { + name string + property *ResourceProperty + ctx knowledgebase2.DynamicValueContext + data knowledgebase2.DynamicValueData + value any + want construct.ResourceId + wantErr bool + }{ + { + name: "resource property", + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: construct.ResourceId{Provider: "mock", Type: "resource", Name: "r1"}, + want: construct.ResourceId{Provider: "mock", Type: "resource", Name: "r1"}, + }, + { + name: "resource property from string", + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: "mock:resource:r1", + want: construct.ResourceId{Provider: "mock", Type: "resource", Name: "r1"}, + }, + { + name: "resource property as template", + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: "{{ \"mock:resource:r1\" }}", + want: construct.ResourceId{Provider: "mock", Type: "resource", Name: "r1"}, + }, + { + name: "non resource throws error", + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + actual, err := tt.property.Parse(tt.value, tt.ctx, tt.data) + if tt.wantErr { + assert.Error(err) + return + } + if !assert.NoError(err) { + return + } + assert.Equal(tt.want, actual) + }) + } +} + +func Test_ResourceProperty_Clone(t *testing.T) { + assert := assert.New(t) + property := &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + actual := property.Clone() + assert.Equal(property, actual) +} + +func Test_ResourceProperty_ZeroValue(t *testing.T) { + assert := assert.New(t) + property := &ResourceProperty{} + assert.Equal(property.ZeroValue(), construct.ResourceId{}) +} + +func Test_ResourceProperty_Details(t *testing.T) { + assert := assert.New(t) + property := &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + assert.Equal(property.Details(), &property.PropertyDetails) +} + +func Test_ResourcePropertyContains(t *testing.T) { + tests := []struct { + name string + property *ResourceProperty + value construct.ResourceId + contains construct.ResourceId + want bool + }{ + { + name: "resource property", + property: &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + }, + value: construct.ResourceId{Provider: "test"}, + contains: construct.ResourceId{Provider: "test"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := tt.property.Contains(tt.value, tt.contains) + if actual != tt.want { + t.Errorf("Contains() = %v, want %v", actual, tt.want) + } + }) + } +} + +func Test_ResourcePropertySubProperties(t *testing.T) { + assert := assert.New(t) + property := &ResourceProperty{ + PropertyDetails: knowledgebase2.PropertyDetails{ + Path: "test", + }, + } + assert.Nil(property.SubProperties()) +} diff --git a/pkg/knowledge_base2/properties/set_property.go b/pkg/knowledge_base2/properties/set_property.go new file mode 100644 index 000000000..94a1ccb80 --- /dev/null +++ b/pkg/knowledge_base2/properties/set_property.go @@ -0,0 +1,193 @@ +package properties + +import ( + "errors" + "fmt" + "reflect" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/klothoplatform/klotho/pkg/set" +) + +type ( + SetProperty struct { + MinLength *int + MaxLength *int + ItemProperty knowledgebase.Property + Properties knowledgebase.Properties + SharedPropertyFields + knowledgebase.PropertyDetails + } +) + +func (s *SetProperty) SetProperty(resource *construct.Resource, value any) error { + if val, ok := value.(set.HashedSet[string, any]); ok { + return resource.SetProperty(s.Path, val) + } + return fmt.Errorf("invalid set value %v", value) +} + +func (s *SetProperty) AppendProperty(resource *construct.Resource, value any) error { + propVal, err := resource.GetProperty(s.Path) + if err != nil { + return err + } + if propVal == nil { + if val, ok := value.(set.HashedSet[string, any]); ok { + return s.SetProperty(resource, val) + } + } + return resource.AppendProperty(s.Path, value) +} + +func (s *SetProperty) RemoveProperty(resource *construct.Resource, value any) error { + propVal, err := resource.GetProperty(s.Path) + if err != nil { + return err + } + if propVal == nil { + return nil + } + propSet, ok := propVal.(set.HashedSet[string, any]) + if !ok { + return errors.New("invalid set value") + } + if val, ok := value.(set.HashedSet[string, any]); ok { + for _, v := range val.ToSlice() { + propSet.Remove(v) + } + } else { + return fmt.Errorf("invalid set value %v", value) + } + return s.SetProperty(resource, propSet) +} + +func (s *SetProperty) Details() *knowledgebase.PropertyDetails { + return &s.PropertyDetails +} + +func (s *SetProperty) Clone() knowledgebase.Property { + var itemProp knowledgebase.Property + if s.ItemProperty != nil { + itemProp = s.ItemProperty.Clone() + } + var props knowledgebase.Properties + if s.Properties != nil { + props = s.Properties.Clone() + } + clone := *s + clone.ItemProperty = itemProp + clone.Properties = props + return &clone +} + +func (s *SetProperty) GetDefaultValue(ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if s.DefaultValue == nil { + return nil, nil + } + return s.Parse(s.DefaultValue, ctx, data) +} + +func (s *SetProperty) Parse(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + var result = set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + } + + var vals []any + if valSet, ok := value.(set.HashedSet[string, any]); ok { + vals = valSet.ToSlice() + } else if val, ok := value.([]any); ok { + vals = val + } else { + // before we fail, check to see if the entire value is a template + if strVal, ok := value.(string); ok { + err := ctx.ExecuteDecode(strVal, data, &vals) + if err != nil { + return nil, err + } + } + } + + for _, v := range vals { + if len(s.Properties) != 0 { + m := MapProperty{Properties: s.Properties} + val, err := m.Parse(v, ctx, data) + if err != nil { + return nil, err + } + result.Add(val) + } else { + val, err := s.ItemProperty.Parse(v, ctx, data) + if err != nil { + return nil, err + } + result.Add(val) + } + } + return result, nil +} + +func (s *SetProperty) ZeroValue() any { + return nil +} + +func (s *SetProperty) Contains(value any, contains any) bool { + valSet, ok := value.(set.HashedSet[string, any]) + if !ok { + return false + } + + for _, val := range valSet.M { + if reflect.DeepEqual(contains, val) { + return true + } + } + + return false +} + +func (s *SetProperty) Type() string { + if s.ItemProperty != nil { + return fmt.Sprintf("set(%s)", s.ItemProperty.Type()) + } + return "set" +} + +func (s *SetProperty) Validate(resource *construct.Resource, value any) error { + setVal, ok := value.(set.HashedSet[string, any]) + if !ok { + return fmt.Errorf("could not validate set property: invalid set value %v", value) + } + if s.MinLength != nil { + if setVal.Len() < *s.MinLength { + return fmt.Errorf("value %s is too short. minimum length is %d", setVal.M, *s.MinLength) + } + } + if s.MaxLength != nil { + if setVal.Len() > *s.MaxLength { + return fmt.Errorf("value %s is too long. maximum length is %d", setVal.M, *s.MaxLength) + } + } + + var errs error + for _, item := range setVal.ToSlice() { + if err := s.ItemProperty.Validate(resource, item); err != nil { + errs = errors.Join(errs, fmt.Errorf("invalid item %v: %v", item, err)) + } + } + if errs != nil { + return errs + } + return nil +} + +func (s *SetProperty) SubProperties() knowledgebase.Properties { + return s.Properties +} + +func (s *SetProperty) Item() knowledgebase.Property { + return s.ItemProperty +} diff --git a/pkg/knowledge_base2/properties/set_property_test.go b/pkg/knowledge_base2/properties/set_property_test.go new file mode 100644 index 000000000..709138338 --- /dev/null +++ b/pkg/knowledge_base2/properties/set_property_test.go @@ -0,0 +1,591 @@ +package properties + +import ( + "fmt" + "testing" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/klothoplatform/klotho/pkg/set" + "github.com/stretchr/testify/assert" +) + +func Test_SetSetProperty(t *testing.T) { + tests := []struct { + name string + property *SetProperty + resource *construct.Resource + value any + }{ + { + name: "set property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return s.(string) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.SetProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value.(set.HashedSet[string, any]).M, tt.resource.Properties[tt.property.Path].(set.HashedSet[string, any]).M) + }) + } +} + +func Test_AppendSetProperty(t *testing.T) { + tests := []struct { + name string + property *SetProperty + resource *construct.Resource + value any + expected set.HashedSet[string, any] + }{ + { + name: "set property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return s.(string) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + expected: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return s.(string) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + }, + { + name: "existing property", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": set.HashedSet[string, any]{ + Hasher: func(s any) string { + return s.(string) + }, + M: map[string]any{ + "test1": "test1", + }, + }, + }, + }, + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return s.(string) + }, + M: map[string]any{ + "test2": "test2", + }, + }, + expected: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return s.(string) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.AppendProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.expected.M, tt.resource.Properties[tt.property.Path].(set.HashedSet[string, any]).M) + }) + } +} + +func Test_RemoveSetProperty(t *testing.T) { + tests := []struct { + name string + property *SetProperty + resource *construct.Resource + value any + expected set.HashedSet[string, any] + }{ + { + name: "existing property", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test2": "test2", + "test1": "test1", + }, + }, + }, + }, + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test2": "test2", + }, + }, + expected: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.RemoveProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.expected.M, tt.resource.Properties[tt.property.Path].(set.HashedSet[string, any]).M) + }) + } +} + +func Test_SetDetails(t *testing.T) { + assert := assert.New(t) + property := &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + } + assert.Equal(property.Details(), &property.PropertyDetails) +} + +func Test_SetClone(t *testing.T) { + assert := assert.New(t) + property := &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + } + clone := property.Clone() + assert.Equal(clone, property) +} + +func Test_SetGetDefaultValue(t *testing.T) { + tests := []struct { + name string + property *SetProperty + ctx knowledgebase.DynamicValueContext + data knowledgebase.DynamicValueData + expected set.HashedSet[string, any] + wantErr bool + }{ + { + name: "set property", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: []any{"test1", "test2"}, + }, + ItemProperty: &StringProperty{}, + }, + expected: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + }, + { + name: "set property as template", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: []any{"{{ \"test1\" }}", "{{ \"test2\" }}"}, + }, + ItemProperty: &StringProperty{}, + }, + expected: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + }, + { + name: "non set throws error", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: "test", + }, + ItemProperty: &StringProperty{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase.DynamicValueContext{} + actual, err := tt.property.GetDefaultValue(ctx, knowledgebase.DynamicValueData{}) + if tt.wantErr { + assert.Error(err) + return + } + if !assert.NoError(err) { + return + } + assert.Equal(actual.(set.HashedSet[string, any]).M, tt.expected.M, "expected %v, got %v", tt.expected, actual) + }) + } +} + +func Test_SetParse(t *testing.T) { + tests := []struct { + name string + property *SetProperty + ctx knowledgebase.DynamicValueContext + data knowledgebase.DynamicValueData + value any + expected set.HashedSet[string, any] + wantErr bool + }{ + { + name: "set property", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + }, + value: []any{"test1", "test2"}, + expected: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + }, + { + name: "set property as template", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + }, + value: []any{"{{ \"test1\" }}", "{{ \"test2\" }}"}, + expected: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + }, + { + name: "non set throws error", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase.DynamicValueContext{} + actual, err := tt.property.Parse(tt.value, ctx, knowledgebase.DynamicValueData{}) + if tt.wantErr { + assert.Error(err) + return + } + if !assert.NoError(err) { + return + } + assert.Equal(actual.(set.HashedSet[string, any]).M, tt.expected.M, "expected %v, got %v", tt.expected, actual) + }) + } +} + +func Test_SetZeroValue(t *testing.T) { + assert := assert.New(t) + property := &SetProperty{} + assert.Nil(property.ZeroValue()) +} + +func Test_SetType(t *testing.T) { + assert := assert.New(t) + property := &SetProperty{} + assert.Equal(property.Type(), "set") + property = &SetProperty{ + ItemProperty: &StringProperty{}, + } + assert.Equal(property.Type(), "set(string)") +} + +func Test_SetContain(t *testing.T) { + tests := []struct { + name string + property *SetProperty + value set.HashedSet[string, any] + contains any + expected bool + }{ + { + name: "set property", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + contains: "test1", + expected: true, + }, + { + name: "contains is in set", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + contains: "test3", + expected: false, + }, + { + name: "non set throws error", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + contains: "test", + expected: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + actual := tt.property.Contains(tt.value, tt.contains) + assert.Equal(actual, tt.expected, "expected %v, got %v", tt.expected, actual) + }) + } +} + +func Test_SetValidate(t *testing.T) { + minLength := 1 + maxLength := 2 + tests := []struct { + name string + property *SetProperty + value any + expected bool + }{ + { + name: "set property", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + MinLength: &minLength, + MaxLength: &maxLength, + ItemProperty: &StringProperty{}, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + expected: true, + }, + + { + name: "set property under min length", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + MinLength: &minLength, + MaxLength: &maxLength, + ItemProperty: &StringProperty{}, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{}, + }, + expected: false, + }, + + { + name: "set property over max length", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + MinLength: &minLength, + MaxLength: &maxLength, + ItemProperty: &StringProperty{}, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + "test3": "test3", + }, + }, + expected: false, + }, + { + name: "set checks item property", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{ + AllowedValues: []string{"test2"}, + }, + }, + value: set.HashedSet[string, any]{ + Hasher: func(s any) string { + return fmt.Sprintf("%v", s) + }, + M: map[string]any{ + "test1": "test1", + "test2": "test2", + }, + }, + expected: false, + }, + { + name: "non set throws error", + property: &SetProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + ItemProperty: &StringProperty{}, + }, + value: "test", + expected: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + resource := &construct.Resource{} + actual := tt.property.Validate(resource, tt.value) + if tt.expected { + assert.NoError(actual) + } else { + assert.Error(actual) + } + }) + } +} + +func Test_SetSubProperties(t *testing.T) { + assert := assert.New(t) + property := &SetProperty{ + Properties: knowledgebase.Properties{ + "test": &StringProperty{}, + }, + } + assert.Len(property.SubProperties(), 1) +} diff --git a/pkg/knowledge_base2/properties/string_property.go b/pkg/knowledge_base2/properties/string_property.go new file mode 100644 index 000000000..b94241919 --- /dev/null +++ b/pkg/knowledge_base2/properties/string_property.go @@ -0,0 +1,121 @@ +package properties + +import ( + "fmt" + "strings" + + "github.com/klothoplatform/klotho/pkg/collectionutil" + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) + +type ( + StringProperty struct { + SanitizeTmpl *knowledgebase.SanitizeTmpl + AllowedValues []string + SharedPropertyFields + knowledgebase.PropertyDetails + } +) + +func (str *StringProperty) SetProperty(resource *construct.Resource, value any) error { + if val, ok := value.(string); ok { + return resource.SetProperty(str.Path, val) + } else if val, ok := value.(construct.PropertyRef); ok { + return resource.SetProperty(str.Path, val) + } + return fmt.Errorf("could not set string property: invalid string value %v", value) +} + +func (str *StringProperty) AppendProperty(resource *construct.Resource, value any) error { + return str.SetProperty(resource, value) +} + +func (str *StringProperty) RemoveProperty(resource *construct.Resource, value any) error { + propVal, err := resource.GetProperty(str.Path) + if err != nil { + return err + } + if propVal == nil { + return nil + } + return resource.RemoveProperty(str.Path, nil) +} + +func (s *StringProperty) Details() *knowledgebase.PropertyDetails { + return &s.PropertyDetails +} + +func (s *StringProperty) Clone() knowledgebase.Property { + clone := *s + return &clone +} + +func (s *StringProperty) GetDefaultValue(ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + if s.DefaultValue == nil { + return nil, nil + } + return s.Parse(s.DefaultValue, ctx, data) +} + +func (str *StringProperty) Parse(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + // Here we have to try to parse to a property ref first, since a string representation of a property ref would match string parsing + val, err := ParsePropertyRef(value, ctx, data) + if err == nil { + return val, nil + } + if val, ok := value.(string); ok { + var result string + err := ctx.ExecuteDecode(val, data, &result) + return result, err + } + return nil, fmt.Errorf("could not parse string property: invalid string value %v", value) +} + +func (s *StringProperty) ZeroValue() any { + return "" +} + +func (s *StringProperty) Contains(value any, contains any) bool { + vString, ok := value.(string) + if !ok { + return false + } + cString, ok := contains.(string) + if !ok { + return false + } + return strings.Contains(vString, cString) +} + +func (s *StringProperty) Type() string { + return "string" +} + +func (s *StringProperty) Validate(resource *construct.Resource, value any) error { + stringVal, ok := value.(string) + if !ok { + return fmt.Errorf("could not validate property: invalid string value %v", value) + } + if s.AllowedValues != nil { + if !collectionutil.Contains(s.AllowedValues, stringVal) { + return fmt.Errorf("value %s is not allowed. allowed values are %s", stringVal, s.AllowedValues) + } + } + + if s.SanitizeTmpl != nil { + oldVal := stringVal + stringVal, err := s.SanitizeTmpl.Execute(stringVal) + if err != nil { + return err + } + if oldVal != stringVal { + return fmt.Errorf("value %s was sanitized to %s", oldVal, stringVal) + } + } + return nil +} + +func (s *StringProperty) SubProperties() knowledgebase.Properties { + return nil +} diff --git a/pkg/knowledge_base2/properties/string_property_test.go b/pkg/knowledge_base2/properties/string_property_test.go new file mode 100644 index 000000000..0d130034a --- /dev/null +++ b/pkg/knowledge_base2/properties/string_property_test.go @@ -0,0 +1,457 @@ +package properties + +import ( + "testing" + + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/stretchr/testify/assert" +) + +func Test_SetStringProperty(t *testing.T) { + tests := []struct { + name string + property *StringProperty + resource *construct.Resource + value string + }{ + { + name: "string property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.SetProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value, tt.resource.Properties[tt.property.Path]) + }) + } +} + +func Test_AppendStringProperty(t *testing.T) { + tests := []struct { + name string + property *StringProperty + resource *construct.Resource + value string + }{ + { + name: "string property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + }, + { + name: "existing property", + resource: &construct.Resource{ + Properties: map[string]any{ + "test": "test", + }, + }, + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.AppendProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value, tt.resource.Properties[tt.property.Path]) + }) + } +} + +func Test_RemoveStringProperty(t *testing.T) { + tests := []struct { + name string + property *StringProperty + resource *construct.Resource + value any + }{ + { + name: "string property", + resource: &construct.Resource{Properties: make(map[string]any)}, + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + }, + { + name: "existing property", + resource: &construct.Resource{ + Properties: construct.Properties{ + "test": "test", + }, + }, + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: []any{"test"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + err := tt.property.RemoveProperty(tt.resource, tt.value) + if !assert.NoError(err) { + return + } + assert.Empty(tt.resource.Properties) + }) + } +} + +func Test_StringDetails(t *testing.T) { + tests := []struct { + name string + property *StringProperty + }{ + { + name: "string property", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + details := tt.property.Details() + assert.Equal(tt.property.Path, details.Path) + }) + } +} + +func Test_StringClone(t *testing.T) { + tests := []struct { + name string + property *StringProperty + }{ + { + name: "string property", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + clone := tt.property.Clone() + assert.Equal(tt.property.Path, clone.Details().Path) + }) + } +} + +func Test_StringGetDefaultValue(t *testing.T) { + tests := []struct { + name string + property *StringProperty + ctx knowledgebase.DynamicValueContext + data knowledgebase.DynamicValueData + value any + }{ + { + name: "string property", + property: &StringProperty{ + SharedPropertyFields: SharedPropertyFields{ + DefaultValue: "test", + }, + }, + value: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + val, err := tt.property.GetDefaultValue(tt.ctx, tt.data) + if !assert.NoError(err) { + return + } + assert.Equal(tt.value, val) + }) + } +} + +func Test_StringParse(t *testing.T) { + tests := []struct { + name string + property *StringProperty + ctx knowledgebase.DynamicValueContext + data knowledgebase.DynamicValueData + value any + expected string + wantErr bool + }{ + { + name: "string property", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + expected: "test", + }, + { + name: "string property as template", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "{{ \"test\" }}", + expected: "test", + }, + { + name: "non string throws error", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + ctx := knowledgebase.DynamicValueContext{} + actual, err := tt.property.Parse(tt.value, ctx, knowledgebase.DynamicValueData{}) + if tt.wantErr { + assert.Error(err) + return + } + if !assert.NoError(err) { + return + } + assert.Equal(actual, tt.expected, "expected %s, got %s", tt.expected, actual) + }) + } +} + +func Test_StringZeroValue(t *testing.T) { + assert := assert.New(t) + property := &StringProperty{} + assert.Equal(property.ZeroValue(), "") +} + +func Test_StringType(t *testing.T) { + assert := assert.New(t) + property := &StringProperty{} + assert.Equal(property.Type(), "string") +} + +func Test_StringContain(t *testing.T) { + tests := []struct { + name string + property *StringProperty + value any + contains any + expected bool + }{ + { + name: "string property", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + contains: "test", + expected: true, + }, + { + name: "contains is substring", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test2", + contains: "test", + expected: true, + }, + { + name: "val is substring", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + contains: "test2", + expected: false, + }, + { + name: "non string throws error", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + contains: 1, + expected: false, + }, + { + name: "non string throws error", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + contains: 1, + expected: false, + }, + { + name: "non string throws error", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + contains: "test", + expected: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + actual := tt.property.Contains(tt.value, tt.contains) + assert.Equal(actual, tt.expected, "expected %v, got %v", tt.expected, actual) + }) + } +} + +func Test_StringValidate(t *testing.T) { + tests := []struct { + name string + property *StringProperty + sanitizeTemplate string + value any + expected bool + }{ + { + name: "string property", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: "test", + expected: true, + }, + { + name: "string property with allowed values", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + AllowedValues: []string{"test", "test2"}, + }, + value: "test", + expected: true, + }, + { + name: "string is sanitized", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + sanitizeTemplate: "{{ . | upper }}", + value: "TEST", + expected: true, + }, + { + name: "string not in allowed values", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + AllowedValues: []string{"test2"}, + }, + value: "test", + expected: false, + }, + { + name: "string not sanitized", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + sanitizeTemplate: "{{ . | upper }}", + value: "test", + expected: false, + }, + { + name: "non string throws error", + property: &StringProperty{ + PropertyDetails: knowledgebase.PropertyDetails{ + Path: "test", + }, + }, + value: 1, + expected: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + if tt.sanitizeTemplate != "" { + tmpl, err := knowledgebase.NewSanitizationTmpl(tt.name, tt.sanitizeTemplate) + if !assert.NoError(err) { + return + } + tt.property.SanitizeTmpl = tmpl + } + resource := &construct.Resource{} + actual := tt.property.Validate(resource, tt.value) + if tt.expected { + assert.NoError(actual) + } else { + assert.Error(actual) + } + }) + } +} + +func Test_StringSubProperties(t *testing.T) { + assert := assert.New(t) + property := &StringProperty{} + assert.Nil(property.SubProperties()) +} diff --git a/pkg/knowledge_base2/property_types.go b/pkg/knowledge_base2/property_types.go deleted file mode 100644 index 28bde51d7..000000000 --- a/pkg/knowledge_base2/property_types.go +++ /dev/null @@ -1,647 +0,0 @@ -package knowledgebase2 - -import ( - "errors" - "fmt" - "reflect" - "strings" - - "github.com/klothoplatform/klotho/pkg/collectionutil" - construct "github.com/klothoplatform/klotho/pkg/construct2" - "github.com/klothoplatform/klotho/pkg/set" -) - -type ( - PropertyType interface { - Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) - SetProperty(property *Property) - ZeroValue() any - Contains(value any, contains any) bool - } - - MapPropertyType struct { - Property *Property - Key string - Value string - } - - ListPropertyType struct { - Property *Property - Value string - } - SetPropertyType struct { - Property *Property - Value string - } - - StringPropertyType struct{} - IntPropertyType struct{} - FloatPropertyType struct{} - BoolPropertyType struct{} - ResourcePropertyType struct { - Value construct.ResourceId - } - PropertyRefPropertyType struct{} - AnyPropertyType struct{} -) - -var PropertyTypeMap = map[string]func(val string, property *Property) (PropertyType, error){ - "string": func(val string, property *Property) (PropertyType, error) { return &StringPropertyType{}, nil }, - "int": func(val string, property *Property) (PropertyType, error) { return &IntPropertyType{}, nil }, - "float": func(val string, property *Property) (PropertyType, error) { return &FloatPropertyType{}, nil }, - "bool": func(val string, property *Property) (PropertyType, error) { return &BoolPropertyType{}, nil }, - "resource": func(val string, property *Property) (PropertyType, error) { - id := construct.ResourceId{} - err := id.UnmarshalText([]byte(val)) - if err != nil { - return nil, fmt.Errorf("invalid resource id for property type %s: %w", val, err) - } - return &ResourcePropertyType{Value: id}, nil - }, - "map": func(val string, property *Property) (PropertyType, error) { - args := strings.Split(val, ",") - if len(property.Properties) != 0 { - return &MapPropertyType{Property: property}, nil - } - if len(args) != 2 { - return nil, fmt.Errorf("invalid number of arguments for map property type") - } - return &MapPropertyType{Key: args[0], Value: args[1], Property: property}, nil - }, - "list": func(val string, p *Property) (PropertyType, error) { - if len(p.Properties) != 0 { - return &ListPropertyType{Property: p}, nil - } - return &ListPropertyType{Value: val, Property: p}, nil - }, - "set": func(val string, p *Property) (PropertyType, error) { - if p.Properties != nil { - return &SetPropertyType{Property: p}, nil - } - return &SetPropertyType{Value: val, Property: p}, nil - }, - "any": func(val string, property *Property) (PropertyType, error) { return &AnyPropertyType{}, nil }, -} - -func (p Properties) Clone() Properties { - newProps := make(Properties, len(p)) - for k, v := range p { - newProps[k] = v.Clone() - } - return newProps -} - -func (p *Property) Clone() *Property { - cloned := *p - cloned.Properties = make(Properties, len(p.Properties)) - for k, v := range p.Properties { - cloned.Properties[k] = v.Clone() - } - return &cloned -} - -// ReplacePath runs a simple [strings.ReplaceAll] on the path of the property and all of its sub properties. -// NOTE: this mutates the property, so make sure to [Property.Clone] it first if you don't want that. -func (p *Property) ReplacePath(original, replacement string) { - p.Path = strings.ReplaceAll(p.Path, original, replacement) - for _, prop := range p.Properties { - prop.ReplacePath(original, replacement) - } -} - -func (p Property) IsPropertyTypeScalar() bool { - return !collectionutil.Contains([]string{"map", "list", "set"}, strings.Split(p.Type, "(")[0]) -} - -func (p Property) ModelType() *string { - typeString := strings.TrimSuffix(strings.TrimPrefix(p.Type, "list("), ")") - parts := strings.Split(typeString, "(") - if parts[0] != "model" { - return nil - } - if len(parts) == 1 { - return &p.Name - } - if len(parts) != 2 { - return nil - } - modelType := strings.TrimSuffix(parts[1], ")") - return &modelType -} - -func (p *Property) PropertyType() (PropertyType, error) { - if p.Type == "" { - return nil, fmt.Errorf("property %s does not have a type", p.Name) - } - parts := strings.Split(p.Type, "(") - ptypeGen, found := PropertyTypeMap[parts[0]] - if !found { - return nil, fmt.Errorf("unknown property type '%s' for property %s", p.Type, p.Name) - } - val := strings.TrimSuffix(strings.Join(parts[1:], "("), ")") - newPtype, err := ptypeGen(val, p) - if err != nil { - return nil, fmt.Errorf("unable to create property type for property %s: %w", p.Name, err) - } - newPtype.SetProperty(p) - return newPtype, nil -} - -func (str *StringPropertyType) Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) { - // Here we have to try to parse to a property ref first, since a string representation of a property ref would match string parsing - refPType := &PropertyRefPropertyType{} - val, err := refPType.Parse(value, ctx, data) - if err == nil { - return val, nil - } - if val, ok := value.(string); ok { - var result string - err := ctx.ExecuteDecode(val, data, &result) - return result, err - } - return nil, fmt.Errorf("invalid string value %v", value) -} - -func (i *IntPropertyType) Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) { - if val, ok := value.(string); ok { - var result int - err := ctx.ExecuteDecode(val, data, &result) - return result, err - } - if val, ok := value.(int); ok { - return val, nil - } - refPType := &PropertyRefPropertyType{} - val, err := refPType.Parse(value, ctx, data) - if err == nil { - return val, nil - } - return nil, fmt.Errorf("invalid int value %v", value) -} - -func (f *FloatPropertyType) Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) { - if val, ok := value.(string); ok { - var result float32 - err := ctx.ExecuteDecode(val, data, &result) - return result, err - } - if val, ok := value.(float32); ok { - return val, nil - } - if val, ok := value.(float64); ok { - return val, nil - } - if val, ok := value.(int); ok { - return float64(val), nil - } - refPType := &PropertyRefPropertyType{} - val, err := refPType.Parse(value, ctx, data) - if err == nil { - return val, nil - } - return nil, fmt.Errorf("invalid float value %v", value) -} - -func (b *BoolPropertyType) Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) { - if val, ok := value.(string); ok { - var result bool - err := ctx.ExecuteDecode(val, data, &result) - return result, err - } - if val, ok := value.(bool); ok { - return val, nil - } - refPType := &PropertyRefPropertyType{} - val, err := refPType.Parse(value, ctx, data) - if err == nil { - return val, nil - } - return nil, fmt.Errorf("invalid bool value %v", value) -} - -func (r *ResourcePropertyType) Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) { - if val, ok := value.(string); ok { - id, err := ExecuteDecodeAsResourceId(ctx, val, data) - if !id.IsZero() && !r.Value.Matches(id) { - return nil, fmt.Errorf("resource value %v does not match type %s", value, r.Value) - } - return id, err - } - if val, ok := value.(map[string]interface{}); ok { - id := construct.ResourceId{ - Type: val["type"].(string), - Name: val["name"].(string), - Provider: val["provider"].(string), - } - if namespace, ok := val["namespace"]; ok { - id.Namespace = namespace.(string) - } - if !r.Value.Matches(id) { - return nil, fmt.Errorf("resource value %v does not match type %s", value, r.Value) - } - return id, nil - } - if val, ok := value.(construct.ResourceId); ok { - if !r.Value.Matches(val) { - return nil, fmt.Errorf("resource value %v does not match type %s", value, r.Value) - } - return val, nil - } - refPType := &PropertyRefPropertyType{} - val, err := refPType.Parse(value, ctx, data) - if err == nil { - if ptype, ok := val.(construct.PropertyRef); ok { - if !r.Value.Matches(ptype.Resource) { - return nil, fmt.Errorf("resource value %v does not match type %s", value, r.Value) - } - } - return val, nil - } - return nil, fmt.Errorf("invalid resource value %v", value) -} - -func (p *PropertyRefPropertyType) Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) { - if val, ok := value.(string); ok { - result := construct.PropertyRef{} - err := ctx.ExecuteDecode(val, data, &result) - return result, err - } - if val, ok := value.(map[string]interface{}); ok { - rp := ResourcePropertyType{} - id, err := rp.Parse(val["resource"], ctx, data) - if err != nil { - return nil, err - } - return construct.PropertyRef{ - Property: val["property"].(string), - Resource: id.(construct.ResourceId), - }, nil - } - if val, ok := value.(construct.PropertyRef); ok { - return val, nil - } - return nil, fmt.Errorf("invalid property reference value %v", value) -} - -func (list *ListPropertyType) Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) { - - var result []any - val, ok := value.([]any) - if !ok { - // before we fail, check to see if the entire value is a template - if strVal, ok := value.(string); ok { - var result []any - err := ctx.ExecuteDecode(strVal, data, &result) - return result, err - } - return nil, fmt.Errorf("invalid list value %v", value) - } - - for _, v := range val { - if len(list.Property.Properties) != 0 { - m := MapPropertyType{Property: list.Property} - val, err := m.Parse(v, ctx, data) - if err != nil { - return nil, err - } - result = append(result, val) - } else { - tempProp := &Property{Type: list.Value} - parser, err := tempProp.PropertyType() - if err != nil { - return nil, fmt.Errorf("invalid value type for list property type %s", list.Value) - } - val, err := parser.Parse(v, ctx, data) - if err != nil { - return nil, err - } - result = append(result, val) - } - } - return result, nil -} - -func (s *SetPropertyType) Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) { - var result = set.HashedSet[string, any]{ - Hasher: func(s any) string { - return fmt.Sprintf("%v", s) - }, - } - val, ok := value.([]any) - if !ok { - // before we fail, check to see if the entire value is a template - if strVal, ok := value.(string); ok { - var result []any - err := ctx.ExecuteDecode(strVal, data, &result) - return result, err - } - return nil, fmt.Errorf("invalid list value %v", value) - } - - for _, v := range val { - if len(s.Property.Properties) != 0 { - m := MapPropertyType{Property: s.Property} - val, err := m.Parse(v, ctx, data) - if err != nil { - return nil, err - } - result.Add(val) - } else { - tempProp := &Property{Type: s.Value} - - parser, err := tempProp.PropertyType() - if err != nil { - return nil, fmt.Errorf("invalid value type for set property type %s", s.Value) - } - val, err := parser.Parse(v, ctx, data) - if err != nil { - return nil, err - } - result.Add(val) - } - } - return result, nil -} - -func (m *MapPropertyType) Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) { - result := map[string]any{} - - mapVal, ok := value.(map[string]any) - if !ok { - // before we fail, check to see if the entire value is a template - if strVal, ok := value.(string); ok { - err := ctx.ExecuteDecode(strVal, data, &result) - return result, err - } - mapVal, ok = value.(construct.Properties) - if !ok { - return nil, fmt.Errorf("invalid map value %v", value) - } - } - // If we are an object with sub properties then we know that we need to get the type of our sub properties to determine how we are parsed into a value - if len(m.Property.Properties) != 0 { - if m.Key != "" || m.Value != "" { - return nil, fmt.Errorf("invalid map property type %s", m.Property.Name) - } - - var errs error - for key := range m.Property.Properties { - if _, found := mapVal[key]; found { - propertyType, err := m.Property.Properties[key].PropertyType() - if err != nil { - return nil, fmt.Errorf("unable to get property type for sub property %s: %w", key, err) - } - propertyType.SetProperty(m.Property.Properties[key]) - val, err := propertyType.Parse(mapVal[key], ctx, data) - if err != nil { - errs = errors.Join(errs, fmt.Errorf("unable to parse value for sub property %s: %w", key, err)) - continue - } - result[key] = val - } - } - return result, nil - } - - // Else we are a set type of map and can just loop over the values - for key, v := range mapVal { - keyType := m.Key - valType := m.Value - tempProp := &Property{Type: keyType} - parser, err := tempProp.PropertyType() - if err != nil { - return nil, fmt.Errorf("invalid key type for map property type %s", keyType) - } - keyVal, err := parser.Parse(key, ctx, data) - if err != nil { - return nil, err - } - tempProp = &Property{Type: valType} - parser, err = tempProp.PropertyType() - if err != nil { - return nil, fmt.Errorf("invalid value type for map property type %s", valType) - } - val, err := parser.Parse(v, ctx, data) - if err != nil { - return nil, err - } - switch keyVal := keyVal.(type) { - case string: - result[keyVal] = val - case construct.ResourceId: - result[keyVal.String()] = val - case construct.PropertyRef: - result[keyVal.String()] = val - default: - return nil, fmt.Errorf("invalid key type for map property type %s", keyType) - } - } - return result, nil -} - -func (a *AnyPropertyType) Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) { - if val, ok := value.(string); ok { - // first check if its a resource id - rType := ResourcePropertyType{} - id, err := rType.Parse(val, ctx, data) - if err == nil { - return id, nil - } - - // check if its a property ref - pType := PropertyRefPropertyType{} - ref, err := pType.Parse(val, ctx, data) - if err == nil { - return ref, nil - } - - // check if its any other template string - var result any - err = ctx.ExecuteDecode(val, data, &result) - if err == nil { - return result, nil - } - } - - if mapVal, ok := value.(map[string]any); ok { - m := MapPropertyType{Property: &Property{}, Key: "string", Value: "any"} - return m.Parse(mapVal, ctx, data) - } - - if listVal, ok := value.([]any); ok { - l := ListPropertyType{Property: &Property{}} - return l.Parse(listVal, ctx, data) - } - - return value, nil -} -func (s *AnyPropertyType) SetProperty(property *Property) { -} - -func (m *MapPropertyType) SetProperty(property *Property) { - m.Property = property -} - -func (l *ListPropertyType) SetProperty(property *Property) { - l.Property = property -} - -func (s *SetPropertyType) SetProperty(property *Property) { - s.Property = property -} - -func (s *StringPropertyType) SetProperty(property *Property) { -} - -func (i *IntPropertyType) SetProperty(property *Property) { -} - -func (f *FloatPropertyType) SetProperty(property *Property) { -} - -func (b *BoolPropertyType) SetProperty(property *Property) { -} - -func (r *ResourcePropertyType) SetProperty(property *Property) { -} - -func (p *PropertyRefPropertyType) SetProperty(property *Property) { -} - -func (m *MapPropertyType) ZeroValue() any { - return nil -} - -func (l *ListPropertyType) ZeroValue() any { - return nil -} - -func (s *SetPropertyType) ZeroValue() any { - return nil -} - -func (s *StringPropertyType) ZeroValue() any { - return "" -} - -func (i *IntPropertyType) ZeroValue() any { - return 0 -} - -func (f *FloatPropertyType) ZeroValue() any { - return 0.0 -} - -func (b *BoolPropertyType) ZeroValue() any { - return false -} - -func (b *AnyPropertyType) ZeroValue() any { - return nil -} - -func (r *ResourcePropertyType) ZeroValue() any { - return construct.ResourceId{} -} - -func (p *PropertyRefPropertyType) ZeroValue() any { - return construct.PropertyRef{} -} - -func (m *MapPropertyType) Contains(value any, contains any) bool { - mapVal, ok := value.(map[string]any) - if !ok { - return false - } - containsMap, ok := contains.(map[string]any) - if !ok { - return false - } - for k, v := range containsMap { - if val, found := mapVal[k]; found || reflect.DeepEqual(val, v) { - return true - } - } - for _, v := range mapVal { - for _, cv := range containsMap { - if reflect.DeepEqual(v, cv) { - return true - } - } - } - return false -} - -func (l *ListPropertyType) Contains(value any, contains any) bool { - list, ok := value.([]any) - if !ok { - return false - } - containsList, ok := contains.([]any) - if !ok { - return false - } - for _, v := range list { - for _, cv := range containsList { - if reflect.DeepEqual(v, cv) { - return true - } - } - } - return false -} - -func (s *SetPropertyType) Contains(value any, contains any) bool { - valSet, ok := value.(set.HashedSet[string, any]) - if !ok { - return false - } - containsSet, ok := contains.(set.HashedSet[string, any]) - if !ok { - return false - } - for _, v := range containsSet.M { - for _, val := range valSet.M { - if reflect.DeepEqual(v, val) { - return true - } - } - } - return false -} - -func (s *StringPropertyType) Contains(value any, contains any) bool { - vString, ok := value.(string) - if !ok { - return false - } - cString, ok := contains.(string) - if !ok { - return false - } - return strings.Contains(vString, cString) -} - -func (i *IntPropertyType) Contains(value any, contains any) bool { - return value == contains -} - -func (f *FloatPropertyType) Contains(value any, contains any) bool { - return value == contains -} - -func (b *BoolPropertyType) Contains(value any, contains any) bool { - return value == contains -} - -func (b *AnyPropertyType) Contains(value any, contains any) bool { - return value == contains -} - -func (r *ResourcePropertyType) Contains(value any, contains any) bool { - return value == contains -} - -func (p *PropertyRefPropertyType) Contains(value any, contains any) bool { - return value == contains -} diff --git a/pkg/knowledge_base2/property_types_test.go b/pkg/knowledge_base2/property_types_test.go deleted file mode 100644 index 092964340..000000000 --- a/pkg/knowledge_base2/property_types_test.go +++ /dev/null @@ -1,690 +0,0 @@ -package knowledgebase2 - -import ( - "testing" - - construct "github.com/klothoplatform/klotho/pkg/construct2" - "github.com/klothoplatform/klotho/pkg/set" - "github.com/stretchr/testify/assert" -) - -func Test_getPropertyType(t *testing.T) { - tests := []struct { - name string - property Property - expected PropertyType - }{ - { - name: "Get string property type", - property: Property{ - Type: "string", - }, - expected: &StringPropertyType{}, - }, - { - name: "Get int property type", - property: Property{ - Type: "int", - }, - expected: &IntPropertyType{}, - }, - { - name: "Get float property type", - property: Property{ - Type: "float", - }, - expected: &FloatPropertyType{}, - }, - { - name: "Get bool property type", - property: Property{ - Type: "bool", - }, - expected: &BoolPropertyType{}, - }, - { - name: "Get resource property type", - property: Property{ - Type: "resource", - }, - expected: &ResourcePropertyType{}, - }, - { - name: "Get resource property type with resource value", - property: Property{ - Type: "resource(test:type)", - }, - expected: &ResourcePropertyType{ - Value: construct.ResourceId{Provider: "test", Type: "type"}, - }, - }, - { - name: "Get map with sub fields property type", - property: Property{ - Type: "map", - Properties: map[string]*Property{ - "nested": { - Name: "nested", - Type: "string", - }, - }, - }, - expected: &MapPropertyType{ - Property: &Property{ - Type: "map", - Properties: map[string]*Property{ - "nested": { - Name: "nested", - Type: "string", - }, - }, - }, - }, - }, - { - name: "Get map property type", - property: Property{ - Type: "map(string,string)", - }, - expected: &MapPropertyType{ - Key: "string", - Value: "string", - Property: &Property{ - Type: "map(string,string)", - }, - }, - }, - { - name: "Get map property type with nested value", - property: Property{ - Type: "map(string,list(string))", - }, - expected: &MapPropertyType{ - Key: "string", - Value: "list(string)", - Property: &Property{ - Type: "map(string,list(string))", - }, - }, - }, - { - name: "Get list with sub fields property type", - property: Property{ - Type: "list", - Properties: map[string]*Property{ - "nested": { - Name: "nested", - Type: "string", - }, - }, - }, - expected: &ListPropertyType{ - Property: &Property{ - Type: "list", - Properties: map[string]*Property{ - "nested": { - Name: "nested", - Type: "string", - }, - }, - }, - }, - }, - { - name: "Get list property type", - property: Property{ - Type: "list(string)", - }, - expected: &ListPropertyType{ - Value: "string", - Property: &Property{ - Type: "list(string)", - }, - }, - }, - { - name: "Get list property type nested value", - property: Property{ - Type: "list(map(string,list(string)))", - }, - expected: &ListPropertyType{ - Value: "map(string,list(string))", - Property: &Property{ - Type: "list(map(string,list(string)))", - }, - }, - }, - { - name: "Get list property type with nested value", - property: Property{ - Type: "list(map(string, string))", - }, - expected: &ListPropertyType{ - Value: "map(string, string)", - Property: &Property{ - Type: "list(map(string, string))", - }, - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert := assert.New(t) - actual, err := test.property.PropertyType() - if assert.NoError(err, "Expected no error, but got: %v", err) { - return - } - assert.Equal(actual, test.expected, "expected %v, got %v", test.expected, actual) - }) - } -} - -func Test_parsePropertyValue(t *testing.T) { - tests := []struct { - name string - property PropertyType - value any - expected any - expectedErr bool - }{ - { - name: "Parse string property value", - property: &StringPropertyType{}, - value: "test", - expected: "test", - }, - { - name: "Parse int property value", - property: &IntPropertyType{}, - value: 1, - expected: 1, - }, - { - name: "Parse int property value as string", - property: &IntPropertyType{}, - value: "{{ 1 }}", - expected: 1, - }, - { - name: "Parse float property value", - property: &FloatPropertyType{}, - value: 1.0, - expected: 1.0, - }, - { - name: "Parse float property value as string", - property: &FloatPropertyType{}, - value: "{{ 1.0 }}", - expected: float32(1.0), - }, - { - name: "Parse bool property value", - property: &BoolPropertyType{}, - value: true, - expected: true, - }, - { - name: "Parse bool property value as string template", - property: &BoolPropertyType{}, - value: "{{ true }}", - expected: true, - }, - { - name: "Parse resource id property value", - property: &ResourcePropertyType{}, - value: "test:resource:a", - expected: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, - }, - { - name: "Parse property ref property value", - property: &PropertyRefPropertyType{}, - value: "test:resource:a#HOSTNAME", - expected: construct.PropertyRef{Resource: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, Property: "HOSTNAME"}, - }, - { - name: "Parse property ref property value as map", - property: &PropertyRefPropertyType{}, - value: map[string]interface{}{ - "resource": "test:resource:a", - "property": "HOSTNAME", - }, - expected: construct.PropertyRef{Resource: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, Property: "HOSTNAME"}, - }, - { - name: "Parse property ref property value as property ref", - property: &PropertyRefPropertyType{}, - value: construct.PropertyRef{Resource: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, Property: "HOSTNAME"}, - expected: construct.PropertyRef{Resource: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, Property: "HOSTNAME"}, - }, - { - name: "Parse invalid property value", - property: &FloatPropertyType{}, - value: "test", - expectedErr: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert := assert.New(t) - ctx := DynamicValueContext{} - actual, err := test.property.Parse(test.value, ctx, DynamicValueData{}) - if test.expectedErr { - assert.Error(err) - return - } - assert.NoError(err, "Expected no error, but got: %v", err) - assert.Equal(actual, test.expected, "expected %v, got %v", test.expected, actual) - }) - } -} - -func Test_parseResourcePropertyValue(t *testing.T) { - tests := []struct { - name string - property PropertyType - value any - expected any - expectedErr bool - }{ - { - name: "Parse resource id property value as map", - property: &ResourcePropertyType{}, - value: map[string]interface{}{ - "provider": "test", - "type": "resource", - "name": "a", - }, - expected: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, - }, - { - name: "Parse resource id property value as resourceId", - property: &ResourcePropertyType{}, - value: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, - expected: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, - }, - { - name: "Parse resource id correct type", - property: &ResourcePropertyType{ - Value: construct.ResourceId{Provider: "test", Type: "resource"}, - }, - value: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, - expected: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, - }, - { - name: "Parse resource id invalid type excpet err", - property: &ResourcePropertyType{ - Value: construct.ResourceId{Provider: "test", Type: "r"}, - }, - value: construct.ResourceId{Provider: "test", Type: "resource", Name: "a"}, - expectedErr: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert := assert.New(t) - ctx := DynamicValueContext{} - actual, err := test.property.Parse(test.value, ctx, DynamicValueData{}) - if test.expectedErr { - assert.Error(err) - return - } - assert.NoError(err, "Expected no error, but got: %v", err) - assert.Equal(actual, test.expected, "expected %v, got %v", test.expected, actual) - }) - } -} - -func Test_MapParse(t *testing.T) { - tests := []struct { - name string - property MapPropertyType - value any - expected any - wantErr bool - }{ - { - name: "Parse map property value", - property: MapPropertyType{ - Key: "string", - Value: "string", - Property: &Property{ - Type: "map(string,string)", - }, - }, - value: map[string]interface{}{ - "key": "test", - "value": "test", - }, - expected: map[string]interface{}{ - "key": "test", - "value": "test", - }, - }, - { - name: "Parse map property value with template", - property: MapPropertyType{ - Key: "string", - Value: "string", - Property: &Property{ - Type: "map(string,string)", - }, - }, - value: map[string]interface{}{ - "key": "{{ \"test\" }}", - "value": "{{ \"test\" }}", - }, - expected: map[string]interface{}{ - "key": "test", - "value": "test", - }, - }, - { - name: "Parse map property value with nested type", - property: MapPropertyType{ - Key: "string", - Value: "list(string)", - Property: &Property{ - Type: "map(string,list(string))", - }, - }, - value: map[string]interface{}{ - "key": []any{"test"}, - "value": []any{"test"}, - }, - expected: map[string]interface{}{ - "key": []any{"test"}, - "value": []any{"test"}, - }, - }, - { - name: "Parse map property with sub properties", - property: MapPropertyType{ - Property: &Property{ - Type: "map", - Properties: map[string]*Property{ - "nested": { - Name: "nested", - Type: "string", - }, - "second": { - Name: "second", - Type: "bool", - }, - }, - }, - }, - value: map[string]interface{}{ - "nested": "test", - "second": true, - }, - expected: map[string]interface{}{ - "nested": "test", - "second": true, - }, - }, - { - name: "Parse map property with sub properties incorrect, should error", - property: MapPropertyType{ - Property: &Property{ - Type: "map", - Properties: map[string]*Property{ - "nested": { - Name: "nested", - Type: "object", - }, - }, - }, - }, - value: map[string]interface{}{ - "nested": "test", - "second": true, - }, - wantErr: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert := assert.New(t) - ctx := DynamicValueContext{} - actual, err := test.property.Parse(test.value, ctx, DynamicValueData{}) - if test.wantErr { - assert.Error(err) - return - } - assert.NoError(err, "Expected no error, but got: %v", err) - // Because it can be int64, int, etc just equals on the map can fail - assert.Equal(actual, test.expected, "expected %v, got %v", test.expected, actual) - - }) - } -} - -func Test_ListParse(t *testing.T) { - tests := []struct { - name string - property ListPropertyType - value any - expected any - wantErr bool - }{ - { - name: "Parse list property value", - property: ListPropertyType{ - Value: "string", - Property: &Property{ - Type: "list(string)", - }, - }, - value: []interface{}{ - "test", - "test", - }, - expected: []any{ - "test", - "test", - }, - }, - { - name: "Parse list property value with template", - property: ListPropertyType{ - Value: "string", - Property: &Property{ - Type: "list(string)", - }, - }, - value: []interface{}{ - "{{ \"test\" }}", - "{{ \"test\" }}", - }, - expected: []any{ - "test", - "test", - }, - }, - { - name: "Parse list property value with nested fields", - property: ListPropertyType{ - Value: "map(string,list(string))", - Property: &Property{ - Type: "list(map(string,list(string)))", - }, - }, - value: []interface{}{ - map[string]interface{}{ - "test": []any{"v"}, - }, - }, - expected: []any{ - map[string]any{ - "test": []any{"v"}, - }, - }, - }, - { - name: "Parse list property with sub properties", - property: ListPropertyType{ - Property: &Property{ - Type: "object", - Properties: map[string]*Property{ - "nested": { - Name: "nested", - Type: "string", - }, - "second": { - Name: "second", - Type: "bool", - }, - }, - }, - }, - value: []interface{}{ - map[string]interface{}{ - "nested": "test", - "second": true, - }, - map[string]interface{}{ - "nested": "test", - "second": true, - }, - }, - expected: []interface{}{ - map[string]interface{}{ - "nested": "test", - "second": true, - }, - map[string]interface{}{ - "nested": "test", - "second": true, - }, - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert := assert.New(t) - ctx := DynamicValueContext{} - actual, err := test.property.Parse(test.value, ctx, DynamicValueData{}) - if test.wantErr { - assert.Error(err) - return - } - assert.NoError(err, "Expected no error, but got: %v", err) - // Because it can be int64, int, etc just equals on the map can fail - assert.Equal(actual, test.expected, "expected %v, got %v", test.expected, actual) - }) - } -} - -func Test_SetParse(t *testing.T) { - tests := []struct { - name string - property SetPropertyType - value any - expected any - wantErr bool - }{ - { - name: "Parse set property value", - property: SetPropertyType{ - Value: "string", - Property: &Property{ - Type: "list(string)", - }, - }, - value: []interface{}{ - "test", - "test", - }, - expected: []any{ - "test", - }, - }, - { - name: "Parse list property value with template", - property: SetPropertyType{ - Value: "string", - Property: &Property{ - Type: "list(string)", - }, - }, - value: []interface{}{ - "{{ \"test\" }}", - "{{ \"test\" }}", - }, - expected: []any{ - "test", - }, - }, - { - name: "Parse list property value with nested fields", - property: SetPropertyType{ - Value: "map(string,list(string))", - Property: &Property{ - Type: "list(map(string,list(string)))", - }, - }, - value: []interface{}{ - map[string]interface{}{ - "test": []any{"v"}, - }, - }, - expected: []any{ - map[string]any{ - "test": []any{"v"}, - }, - }, - }, - { - name: "Parse list property with sub properties", - property: SetPropertyType{ - Property: &Property{ - Type: "object", - Properties: map[string]*Property{ - "nested": { - Name: "nested", - Type: "string", - }, - "second": { - Name: "second", - Type: "bool", - }, - }, - }, - }, - value: []interface{}{ - map[string]interface{}{ - "nested": "test", - "second": true, - }, - map[string]interface{}{ - "nested": "test", - "second": true, - }, - }, - expected: []any{ - map[string]any{ - "nested": "test", - "second": true, - }, - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert := assert.New(t) - ctx := DynamicValueContext{} - actual, err := test.property.Parse(test.value, ctx, DynamicValueData{}) - if test.wantErr { - assert.Error(err) - return - } - hashedSet, ok := actual.(set.HashedSet[string, any]) - if !ok { - assert.Fail("Expected set.HashedSet[string, any]") - } - assert.NoError(err, "Expected no error, but got: %v", err) - // Because it can be int64, int, etc just equals on the map can fail - assert.Equal(hashedSet.ToSlice(), test.expected, "expected %v, got %v", test.expected, actual) - }) - } -} diff --git a/pkg/knowledge_base2/embed.go b/pkg/knowledge_base2/reader/embed.go similarity index 71% rename from pkg/knowledge_base2/embed.go rename to pkg/knowledge_base2/reader/embed.go index 4bd699a02..e2736c49d 100644 --- a/pkg/knowledge_base2/embed.go +++ b/pkg/knowledge_base2/reader/embed.go @@ -1,4 +1,4 @@ -package knowledgebase2 +package reader import ( "errors" @@ -6,18 +6,31 @@ import ( "io/fs" construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" "go.uber.org/zap" "gopkg.in/yaml.v3" ) -func NewKBFromFs(resources, edges, models fs.FS) (*KnowledgeBase, error) { - kb := NewKB() - kbModels, err := ModelsFromFS(models) +func NewKBFromFs(resources, edges, models fs.FS) (*knowledgebase.KnowledgeBase, error) { + var errs error + kb := knowledgebase.NewKB() + readerModels, err := ModelsFromFS(models) if err != nil { return nil, err } - kb.models = kbModels - templates, err := TemplatesFromFs(resources, kbModels) + kbModels := map[string]*knowledgebase.Model{} + for name, model := range readerModels { + kbModel, err := model.Convert() + if err != nil { + errs = errors.Join(errs, fmt.Errorf("error converting model %s: %w", name, err)) + } + kbModels[name] = kbModel + } + if errs != nil { + return nil, errs + } + kb.Models = kbModels + templates, err := TemplatesFromFs(resources, readerModels) if err != nil { return nil, err } @@ -26,7 +39,6 @@ func NewKBFromFs(resources, edges, models fs.FS) (*KnowledgeBase, error) { return nil, err } - var errs error for _, template := range templates { err = kb.AddResourceTemplate(template) if err != nil { @@ -48,7 +60,7 @@ func NewKBFromFs(resources, edges, models fs.FS) (*KnowledgeBase, error) { } func ModelsFromFS(dir fs.FS) (map[string]*Model, error) { - models := map[string]*Model{} + inputModels := map[string]*Model{} err := fs.WalkDir(dir, ".", func(path string, d fs.DirEntry, nerr error) error { zap.S().Debug("Loading model: ", path) if d.IsDir() { @@ -59,26 +71,28 @@ func ModelsFromFS(dir fs.FS) (map[string]*Model, error) { return errors.Join(nerr, fmt.Errorf("error opening model file %s: %w", path, err)) } - model := &Model{} - err = yaml.NewDecoder(f).Decode(model) + model := Model{} + err = yaml.NewDecoder(f).Decode(&model) if err != nil { return errors.Join(nerr, fmt.Errorf("error decoding model file %s: %w", path, err)) } - models[model.Name] = model + + inputModels[model.Name] = &model return nil }) - for _, model := range models { - uerr := updateModels(nil, model.Properties, models) + // Update models to only reference properties and not other models and then convert property/properties to internal types + for _, model := range inputModels { + uerr := updateModels(nil, model.Properties, inputModels) if uerr != nil { err = errors.Join(err, uerr) } } - return models, err + return inputModels, err } -func TemplatesFromFs(dir fs.FS, models map[string]*Model) (map[construct.ResourceId]*ResourceTemplate, error) { - templates := map[construct.ResourceId]*ResourceTemplate{} +func TemplatesFromFs(dir fs.FS, models map[string]*Model) (map[construct.ResourceId]*knowledgebase.ResourceTemplate, error) { + templates := map[construct.ResourceId]*knowledgebase.ResourceTemplate{} err := fs.WalkDir(dir, ".", func(path string, d fs.DirEntry, nerr error) error { zap.S().Debug("Loading resource template: ", path) if d.IsDir() { @@ -105,14 +119,18 @@ func TemplatesFromFs(dir fs.FS, models map[string]*Model) (map[construct.Resourc if templates[id] != nil { return errors.Join(nerr, fmt.Errorf("duplicate template for %s in %s", id, path)) } - templates[id] = resTemplate + rt, err := resTemplate.Convert() + if err != nil { + return errors.Join(nerr, fmt.Errorf("error converting resource template %s: %w", path, err)) + } + templates[id] = rt return nil }) return templates, err } -func EdgeTemplatesFromFs(dir fs.FS) (map[string]*EdgeTemplate, error) { - templates := map[string]*EdgeTemplate{} +func EdgeTemplatesFromFs(dir fs.FS) (map[string]*knowledgebase.EdgeTemplate, error) { + templates := map[string]*knowledgebase.EdgeTemplate{} err := fs.WalkDir(dir, ".", func(path string, d fs.DirEntry, nerr error) error { zap.S().Debug("Loading edge template: ", path) if d.IsDir() { @@ -123,7 +141,7 @@ func EdgeTemplatesFromFs(dir fs.FS) (map[string]*EdgeTemplate, error) { return errors.Join(nerr, fmt.Errorf("error opening edge template %s: %w", path, err)) } - edgeTemplate := &EdgeTemplate{} + edgeTemplate := &knowledgebase.EdgeTemplate{} err = yaml.NewDecoder(f).Decode(edgeTemplate) if err != nil { return errors.Join(nerr, fmt.Errorf("error decoding edge template %s: %w", path, err)) @@ -133,13 +151,13 @@ func EdgeTemplatesFromFs(dir fs.FS) (map[string]*EdgeTemplate, error) { if err != nil { return errors.Join(nerr, fmt.Errorf("error opening edge template %s: %w", path, err)) } - multiEdgeTemplate := &MultiEdgeTemplate{} + multiEdgeTemplate := &knowledgebase.MultiEdgeTemplate{} err = yaml.NewDecoder(f).Decode(multiEdgeTemplate) if err != nil { return errors.Join(nerr, fmt.Errorf("error decoding edge template %s: %w", path, err)) } if !multiEdgeTemplate.Resource.IsZero() && (len(multiEdgeTemplate.Sources) > 0 || len(multiEdgeTemplate.Targets) > 0) { - edgeTemplates := EdgeTemplatesFromMulti(*multiEdgeTemplate) + edgeTemplates := knowledgebase.EdgeTemplatesFromMulti(*multiEdgeTemplate) for _, edgeTemplate := range edgeTemplates { id := edgeTemplate.Source.QualifiedTypeName() + "->" + edgeTemplate.Target.QualifiedTypeName() if templates[id] != nil { diff --git a/pkg/knowledge_base2/reader/models.go b/pkg/knowledge_base2/reader/models.go new file mode 100644 index 000000000..fbb4c2a38 --- /dev/null +++ b/pkg/knowledge_base2/reader/models.go @@ -0,0 +1,126 @@ +package reader + +import ( + "fmt" + "strings" + + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" +) + +type ( + Model struct { + Name string `json:"name" yaml:"name"` + Properties Properties `json:"properties" yaml:"properties"` + Property *Property `json:"property" yaml:"property"` + } +) + +func (m Model) Convert() (*knowledgebase.Model, error) { + model := &knowledgebase.Model{} + model.Name = m.Name + if m.Properties != nil { + properties, err := m.Properties.Convert() + if err != nil { + return nil, err + } + model.Properties = properties + } + if m.Property != nil { + property, err := m.Property.Convert() + if err != nil { + return nil, err + } + model.Property = property + } + return model, nil +} + +func (p Property) ModelType() *string { + typeString := strings.TrimSuffix(strings.TrimPrefix(p.Type, "list("), ")") + parts := strings.Split(typeString, "(") + if parts[0] != "model" { + return nil + } + if len(parts) == 1 { + return &p.Name + } + if len(parts) != 2 { + return nil + } + modelType := strings.TrimSuffix(parts[1], ")") + return &modelType +} + +func updateModels(property *Property, properties Properties, models map[string]*Model) error { + for name, p := range properties { + modelType := p.ModelType() + if modelType != nil { + if len(p.Properties) != 0 { + return fmt.Errorf("property %s has properties but is labeled as a model", name) + } + model := models[*modelType] + if model == nil || model.Properties == nil { + return fmt.Errorf("model %s not found", *modelType) + } + // We know that this means we want the properties to be spread onto the resource + if p.Name == *modelType { + if model.Property != nil { + return fmt.Errorf("model %s as property can not be spread into properties", *modelType) + } + delete(properties, name) + for name, prop := range model.Properties { + // since properties are pointers and models can be reused, we need to clone the property from the model itself + newProp := prop.Clone() + newProp.Path = fmt.Sprintf("%s.%s", name, prop.Path) + + // we also need to check if the current property has a default and propagate it lower + if p.DefaultValue != nil { + defaultMap, ok := p.DefaultValue.(map[string]any) + if !ok { + return fmt.Errorf("default value for %s is not a map", p.Path) + } + newProp.DefaultValue = defaultMap[name] + } + properties[name] = newProp + } + if property != nil { + if err := updateModelPaths(property); err != nil { + return err + } + } + } else { + m := models[*modelType] + if m.Properties != nil { + p.Properties = models[*modelType].Properties.Clone() + modelString := fmt.Sprintf("model(%s)", *modelType) + if p.Type == modelString { + p.Type = "map" + } else if p.Type == fmt.Sprintf("list(%s)", modelString) { + p.Type = "list" + } + if err := updateModelPaths(p); err != nil { + return err + } + } else if m.Property != nil { + p = m.Property.Clone() + } + } + } + err := updateModels(p, p.Properties, models) + if err != nil { + return err + } + } + return nil +} + +func updateModelPaths(p *Property) error { + for _, prop := range p.Properties { + prop.Path = fmt.Sprintf("%s.%s", p.Path, prop.Name) + err := updateModelPaths(prop) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/knowledge_base2/reader/properties.go b/pkg/knowledge_base2/reader/properties.go new file mode 100644 index 000000000..98224cf59 --- /dev/null +++ b/pkg/knowledge_base2/reader/properties.go @@ -0,0 +1,288 @@ +package reader + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/uuid" + construct "github.com/klothoplatform/klotho/pkg/construct2" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/klothoplatform/klotho/pkg/knowledge_base2/properties" + "gopkg.in/yaml.v3" +) + +type ( + // Properties defines the structure of properties defined in yaml as a part of a template. + Properties map[string]*Property + + // Property defines the structure of a property defined in yaml as a part of a template. + // these fields must be exactly the union of all the fields in the different property types. + Property struct { + Name string `json:"name" yaml:"name"` + // Type defines the type of the property + Type string `json:"type" yaml:"type"` + + Namespace bool `json:"namespace" yaml:"namespace"` + + DefaultValue any `json:"default_value" yaml:"default_value"` + + Required bool `json:"required" yaml:"required"` + + ConfigurationDisabled bool `json:"configuration_disabled" yaml:"configuration_disabled"` + + DeployTime bool `json:"deploy_time" yaml:"deploy_time"` + + OperationalRule *knowledgebase.PropertyRule `json:"operational_rule" yaml:"operational_rule"` + + Properties Properties `json:"properties" yaml:"properties"` + + MinLength *int `yaml:"min_length"` + MaxLength *int `yaml:"max_length"` + + MinValue *float64 `yaml:"min_value"` + MaxValue *float64 `yaml:"max_value"` + + AllowedTypes construct.ResourceList `yaml:"allowed_types"` + + SanitizeTmpl string `yaml:"sanitize"` + AllowedValues []string `yaml:"allowed_values"` + + KeyProperty knowledgebase.Property `yaml:"key_property"` + ValueProperty knowledgebase.Property `yaml:"value_property"` + + ItemProperty knowledgebase.Property `yaml:"item_property"` + + Path string `json:"-" yaml:"-"` + } +) + +func (p *Properties) UnmarshalYAML(n *yaml.Node) error { + type h Properties + var p2 h + err := n.Decode(&p2) + if err != nil { + return err + } + for name, property := range p2 { + property.Name = name + property.Path = name + setChildPaths(property, name) + p2[name] = property + } + *p = Properties(p2) + return nil +} + +func (p *Properties) Convert() (knowledgebase.Properties, error) { + var errs error + props := knowledgebase.Properties{} + for name, prop := range *p { + propertyType, err := prop.Convert() + if err != nil { + errs = fmt.Errorf("%w\n%s", errs, err.Error()) + continue + } + props[name] = propertyType + } + return props, errs +} + +func (p *Property) Convert() (knowledgebase.Property, error) { + propertyType, err := InitializeProperty(p.Type) + if err != nil { + return nil, err + } + + srcVal := reflect.ValueOf(p).Elem() + dstVal := reflect.ValueOf(propertyType).Elem() + for i := 0; i < srcVal.NumField(); i++ { + srcField := srcVal.Field(i) + fieldName := srcVal.Type().Field(i).Name + dstField := dstVal.FieldByName(fieldName) + if !dstField.IsValid() || !dstField.CanSet() { + continue + } + // Skip nil pointers + if (srcField.Kind() == reflect.Ptr || srcField.Kind() == reflect.Interface) && srcField.IsNil() { + continue + } + // Handle sub properties so we can recurse down the tree + if fieldName == "Properties" { + properties, ok := srcField.Interface().(Properties) + if !ok { + return nil, fmt.Errorf("invalid properties") + } + var errs error + props := knowledgebase.Properties{} + for name, prop := range properties { + propertyType, err := prop.Convert() + if err != nil { + errs = fmt.Errorf("%w\n%s", errs, err.Error()) + continue + } + props[name] = propertyType + } + if errs != nil { + return nil, errs + } + dstField.Set(reflect.ValueOf(props)) + continue + } + + if dstField.Type().AssignableTo(srcField.Type()) { + dstField.Set(srcField) + continue + } + + if dstField.Kind() == reflect.Ptr && srcField.Kind() == reflect.Ptr { + if srcField.Type().Elem().AssignableTo(dstField.Type().Elem()) { + dstField.Set(srcField) + continue + } else if srcField.Type().Elem().ConvertibleTo(dstField.Type().Elem()) { + val := srcField.Elem().Convert(dstField.Type().Elem()) + // set dest field to a pointer of val + dstField.Set(reflect.New(dstField.Type().Elem())) + dstField.Elem().Set(val) + continue + } + } + + if conversion, found := fieldConversion[fieldName]; found { + err := conversion(srcField, p, propertyType) + if err != nil { + return nil, err + } + continue + } + + return nil, fmt.Errorf("invalid field name %s, for property %s of type %s", fieldName, p.Name, p.Type) + + } + + details := propertyType.Details() + details.Path = p.Path + return propertyType, nil +} + +func setChildPaths(property *Property, currPath string) { + for name, child := range property.Properties { + child.Name = name + path := currPath + "." + name + child.Path = path + setChildPaths(child, path) + } +} + +func (p Properties) Clone() Properties { + newProps := make(Properties, len(p)) + for k, v := range p { + newProps[k] = v.Clone() + } + return newProps +} + +func (p *Property) Clone() *Property { + cloned := *p + cloned.Properties = make(Properties, len(p.Properties)) + for k, v := range p.Properties { + cloned.Properties[k] = v.Clone() + } + return &cloned +} + +// fieldConversion is a map providing functionality on how to convert inputs into our internal types if they are not inherently the same structure +var fieldConversion = map[string]func(val reflect.Value, p *Property, kp knowledgebase.Property) error{ + "SanitizeTmpl": func(val reflect.Value, p *Property, kp knowledgebase.Property) error { + sanitizeTmpl, ok := val.Interface().(string) + if !ok { + return fmt.Errorf("invalid sanitize template") + } + // generate random uuid as the name of the template + name := uuid.New().String() + tmpl, err := knowledgebase.NewSanitizationTmpl(name, sanitizeTmpl) + if err != nil { + return err + } + dstField := reflect.ValueOf(kp).Elem().FieldByName("SanitizeTmpl") + dstField.Set(reflect.ValueOf(tmpl)) + return nil + }, +} + +func InitializeProperty(ptype string) (knowledgebase.Property, error) { + if ptype == "" { + return nil, fmt.Errorf("property does not have a type") + } + parts := strings.Split(ptype, "(") + p, found := initializePropertyFunc[parts[0]] + if !found { + return nil, fmt.Errorf("unknown property type '%s'", ptype) + } + var val string + if len(parts) > 1 { + val = strings.TrimSuffix(strings.Join(parts[1:], "("), ")") + } + return p(val) +} + +var initializePropertyFunc map[string]func(val string) (knowledgebase.Property, error) + +func init() { + // initializePropertyFunc initialization is deferred to prevent cyclic initialization (a compiler error) with `InitializeProperty` + initializePropertyFunc = map[string]func(val string) (knowledgebase.Property, error){ + "string": func(val string) (knowledgebase.Property, error) { return &properties.StringProperty{}, nil }, + "int": func(val string) (knowledgebase.Property, error) { return &properties.IntProperty{}, nil }, + "float": func(val string) (knowledgebase.Property, error) { return &properties.FloatProperty{}, nil }, + "bool": func(val string) (knowledgebase.Property, error) { return &properties.BoolProperty{}, nil }, + "resource": func(val string) (knowledgebase.Property, error) { + id := construct.ResourceId{} + err := id.UnmarshalText([]byte(val)) + if err != nil { + return nil, fmt.Errorf("invalid resource id for property type %s: %w", val, err) + } + return &properties.ResourceProperty{ + AllowedTypes: construct.ResourceList{id}, + }, nil + }, + "map": func(val string) (knowledgebase.Property, error) { + if val == "" { + return &properties.MapProperty{}, nil + } + args := strings.Split(val, ",") + if len(args) != 2 { + return nil, fmt.Errorf("invalid number of arguments for map property type: %s", val) + } + keyVal, err := InitializeProperty(args[0]) + if err != nil { + return nil, err + } + valProp, err := InitializeProperty(args[1]) + if err != nil { + return nil, err + } + return &properties.MapProperty{KeyProperty: keyVal, ValueProperty: valProp}, nil + }, + "list": func(val string) (knowledgebase.Property, error) { + if val == "" { + return &properties.ListProperty{}, nil + } + itemProp, err := InitializeProperty(val) + if err != nil { + return nil, err + } + return &properties.ListProperty{ItemProperty: itemProp}, nil + }, + "set": func(val string) (knowledgebase.Property, error) { + if val == "" { + return &properties.SetProperty{}, nil + } + itemProp, err := InitializeProperty(val) + if err != nil { + return nil, err + } + return &properties.SetProperty{ItemProperty: itemProp}, nil + }, + "any": func(val string) (knowledgebase.Property, error) { return &properties.AnyProperty{}, nil }, + } +} diff --git a/pkg/knowledge_base2/reader/properties_test.go b/pkg/knowledge_base2/reader/properties_test.go new file mode 100644 index 000000000..00ac29b74 --- /dev/null +++ b/pkg/knowledge_base2/reader/properties_test.go @@ -0,0 +1,40 @@ +package reader + +import ( + "testing" + + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + "github.com/klothoplatform/klotho/pkg/knowledge_base2/properties" + "github.com/stretchr/testify/assert" +) + +func Test_ConvertProperty(t *testing.T) { + tests := []struct { + name string + property Property + expected knowledgebase.Property + }{ + { + name: "Get string property type", + property: Property{ + Type: "string", + Name: "test", + Path: "test", + Required: true, + AllowedValues: []string{"test1", "test2"}, + SanitizeTmpl: "test", + }, + expected: &properties.StringProperty{}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert := assert.New(t) + actual, err := test.property.Convert() + if assert.NoError(err, "Expected no error, but got: %v", err) { + return + } + assert.Equal(actual, test.expected, "expected %v, got %v", test.expected, actual) + }) + } +} diff --git a/pkg/knowledge_base2/reader/resource_template.go b/pkg/knowledge_base2/reader/resource_template.go new file mode 100644 index 000000000..ed8b3fb56 --- /dev/null +++ b/pkg/knowledge_base2/reader/resource_template.go @@ -0,0 +1,55 @@ +package reader + +import knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2" + +type ( + // ResourceTemplate defines how rules are handled by the engine in terms of making sure they are functional in the graph + ResourceTemplate struct { + QualifiedTypeName string `json:"qualified_type_name" yaml:"qualified_type_name"` + + DisplayName string `json:"display_name" yaml:"display_name"` + + Properties Properties `json:"properties" yaml:"properties"` + + Classification knowledgebase.Classification `json:"classification" yaml:"classification"` + + PathSatisfaction knowledgebase.PathSatisfaction `json:"path_satisfaction" yaml:"path_satisfaction"` + + Consumption knowledgebase.Consumption `json:"consumption" yaml:"consumption"` + + // DeleteContext defines the context in which a resource can be deleted + DeleteContext knowledgebase.DeleteContext `json:"delete_context" yaml:"delete_context"` + // Views defines the views that the resource should be added to as a distinct node + Views map[string]string `json:"views" yaml:"views"` + + NoIac bool `json:"no_iac" yaml:"no_iac"` + + SanitizeNameTmpl string `yaml:"sanitize_name"` + } +) + +func (r *ResourceTemplate) Convert() (*knowledgebase.ResourceTemplate, error) { + kbProperties, err := r.Properties.Convert() + if err != nil { + return nil, err + } + var sanitizeTmpl *knowledgebase.SanitizeTmpl + if r.SanitizeNameTmpl != "" { + sanitizeTmpl, err = knowledgebase.NewSanitizationTmpl(r.QualifiedTypeName, r.SanitizeNameTmpl) + if err != nil { + return nil, err + } + } + return &knowledgebase.ResourceTemplate{ + QualifiedTypeName: r.QualifiedTypeName, + DisplayName: r.DisplayName, + Properties: kbProperties, + Classification: r.Classification, + PathSatisfaction: r.PathSatisfaction, + Consumption: r.Consumption, + DeleteContext: r.DeleteContext, + Views: r.Views, + NoIac: r.NoIac, + SanitizeNameTmpl: sanitizeTmpl, + }, nil +} diff --git a/pkg/knowledge_base2/resource_template.go b/pkg/knowledge_base2/resource_template.go index 063b1a7f4..5bf1a07a3 100644 --- a/pkg/knowledge_base2/resource_template.go +++ b/pkg/knowledge_base2/resource_template.go @@ -1,14 +1,10 @@ package knowledgebase2 import ( - "bytes" - "crypto/sha256" "errors" "fmt" "reflect" - "regexp" "strings" - "text/template" "github.com/klothoplatform/klotho/pkg/collectionutil" construct "github.com/klothoplatform/klotho/pkg/construct2" @@ -19,16 +15,22 @@ import ( type ( // ResourceTemplate defines how rules are handled by the engine in terms of making sure they are functional in the graph ResourceTemplate struct { + // QualifiedTypeName is the qualified type name of the resource QualifiedTypeName string `json:"qualified_type_name" yaml:"qualified_type_name"` + // DisplayName is the common name that refers to the resource DisplayName string `json:"display_name" yaml:"display_name"` + // Properties defines the properties that the resource has Properties Properties `json:"properties" yaml:"properties"` + // Classification defines the classification of the resource Classification Classification `json:"classification" yaml:"classification"` + // PathSatisfaction defines what paths must exist for the resource must be connected to and from PathSatisfaction PathSatisfaction `json:"path_satisfaction" yaml:"path_satisfaction"` + // Consumption defines properties the resource may emit or consume from other resources it is connected to or expanded from Consumption Consumption `json:"consumption" yaml:"consumption"` // DeleteContext defines the context in which a resource can be deleted @@ -36,42 +38,92 @@ type ( // Views defines the views that the resource should be added to as a distinct node Views map[string]string `json:"views" yaml:"views"` + // NoIac defines if the resource should be ignored by the IaC engine NoIac bool `json:"no_iac" yaml:"no_iac"` - SanitizeNameTmpl string `yaml:"sanitize_name"` + // SanitizeNameTmpl defines a template that is used to sanitize the name of the resource + SanitizeNameTmpl *SanitizeTmpl `yaml:"sanitize_name"` } - Properties map[string]*Property - - Property struct { + // PropertyDetails defines the common details of a property + PropertyDetails struct { Name string `json:"name" yaml:"name"` - // Type defines the type of the property - Type string `json:"type" yaml:"type"` - - Namespace bool `json:"namespace" yaml:"namespace"` - - DefaultValue any `json:"default_value" yaml:"default_value"` - + // DefaultValue has to be any because it may be a template and it may be a value of the correct type + Namespace bool `yaml:"namespace"` + // Required defines if the property is required Required bool `json:"required" yaml:"required"` - + // ConfigurationDisabled defines if the property is allowed to be configured by the user ConfigurationDisabled bool `json:"configuration_disabled" yaml:"configuration_disabled"` - + // DeployTime defines if the property is only available at deploy time DeployTime bool `json:"deploy_time" yaml:"deploy_time"` + // OperationalRule defines a rule that is executed at runtime to determine the value of the property + OperationalRule *PropertyRule `json:"operational_rule" yaml:"operational_rule"` + // Path is the path to the property in the resource + Path string `json:"-" yaml:"-"` + } - OperationalRule *OperationalRule `json:"operational_rule" yaml:"operational_rule"` + // Property is an interface used to define a property that exists on a resource + // Properties are used to define the structure of a resource and how it is configured + // Each property implementation refers to a specific type of property, such as a string or a list, etc + Property interface { + // SetProperty sets the value of the property on the resource + SetProperty(resource *construct.Resource, value any) error + // AppendProperty appends the value to the property on the resource + AppendProperty(resource *construct.Resource, value any) error + // RemoveProperty removes the value from the property on the resource + RemoveProperty(resource *construct.Resource, value any) error + // Details returns the property details for the property + Details() *PropertyDetails + // Clone returns a clone of the property + Clone() Property + // Type returns the string representation of the type of the property, as it should appear in the resource template + Type() string + // GetDefaultValue returns the default value for the property, pertaining to the specific data being passed in for execution + GetDefaultValue(ctx DynamicContext, data DynamicValueData) (any, error) + // Validate ensures the value is valid for the property and returns an error if it is not + Validate(resource *construct.Resource, value any) error + // SubProperties returns the sub properties of the property, if any. This is used for properties that are complex structures, + // such as lists, sets, or maps + SubProperties() Properties + // Parse parses a given value to ensure it is the correct type for the property. If the given value cannot be converted + // to the respective property type an error is returned. The returned value will always be the correct type for the property + Parse(value any, ctx DynamicContext, data DynamicValueData) (any, error) + // ZeroValue returns the zero value for the property type + ZeroValue() any + // Contains returns true if the value contains the given value + Contains(value any, contains any) bool + } - Properties map[string]*Property `json:"properties" yaml:"properties"` + // MapProperty is an interface for properties that implement map structures + MapProperty interface { + // Key returns the property representing the keys of the map + Key() Property + // Value returns the property representing the values of the map + Value() Property + } - Path string `json:"-" yaml:"-"` + // CollectionProperty is an interface for properties that implement collection structures + CollectionProperty interface { + // Item returns the structure of the items within the collection + Item() Property } + // Properties is a map of properties + Properties map[string]Property + + // Classification defines the classification of a resource Classification struct { - Is []string `json:"is" yaml:"is"` - Gives []Gives `json:"gives" yaml:"gives"` + // Is defines the classifications that the resource belongs to + Is []string `json:"is" yaml:"is"` + // Gives defines the attributes that the resource gives to other resources + Gives []Gives `json:"gives" yaml:"gives"` } + // Gives defines an attribute that can be provided to other functionalities for the resource it belongs to Gives struct { - Attribute string + // Attribute is the attribute that is given + Attribute string + // Functionality is the list of functionalities that the attribute is given to Functionality []string } @@ -97,33 +149,6 @@ const ( Unknown Functionality = "Unknown" ) -func (p *Properties) UnmarshalYAML(n *yaml.Node) error { - type h Properties - var p2 h - err := n.Decode(&p2) - if err != nil { - return err - } - for name, property := range p2 { - property.Name = name - property.Path = name - setChildPaths(property, name) - p2[name] = property - } - *p = Properties(p2) - return nil -} - -func setChildPaths(property *Property, currPath string) { - for name, child := range property.Properties { - child.Name = name - path := currPath + "." + name - child.Path = path - setChildPaths(child, path) - property.Properties[name] = child - } -} - func (g *Gives) UnmarshalJSON(content []byte) error { givesString := string(content) if givesString == "" { @@ -154,6 +179,14 @@ func (g *Gives) UnmarshalYAML(n *yaml.Node) error { return nil } +func (p *Properties) Clone() Properties { + newProps := make(Properties, len(*p)) + for k, v := range *p { + newProps[k] = v.Clone() + } + return newProps +} + func (template ResourceTemplate) Id() construct.ResourceId { args := strings.Split(template.QualifiedTypeName, ":") return construct.ResourceId{ @@ -180,67 +213,11 @@ func CreateResource(kb TemplateKB, id construct.ResourceId) (*construct.Resource }, nil } -func SanitizeEdge(kb TemplateKB, e construct.SimpleEdge) (construct.SimpleEdge, error) { - sRT, err := kb.GetResourceTemplate(e.Source) - if err != nil { - return e, fmt.Errorf("could not get source resource template: %w", err) - } - tRT, err := kb.GetResourceTemplate(e.Target) - if err != nil { - return e, fmt.Errorf("could not get target resource template: %w", err) - } - e.Source.Name, err = sRT.SanitizeName(e.Source.Name) - if err != nil { - return e, fmt.Errorf("could not sanitize source name: %w", err) - } - e.Target.Name, err = tRT.SanitizeName(e.Target.Name) - if err != nil { - return e, fmt.Errorf("could not sanitize target name: %w", err) - } - return e, nil -} - func (rt ResourceTemplate) SanitizeName(name string) (string, error) { - if rt.SanitizeNameTmpl == "" { + if rt.SanitizeNameTmpl == nil { return name, nil } - nt, err := template.New(rt.QualifiedTypeName + "/sanitize_name"). - Funcs(template.FuncMap{ - "replace": func(pattern, replace, name string) (string, error) { - re, err := regexp.Compile(pattern) - if err != nil { - return name, err - } - return re.ReplaceAllString(name, replace), nil - }, - - "length": func(min, max int, name string) string { - if len(name) < min { - return name + strings.Repeat("0", min-len(name)) - } - if len(name) > max { - base := name[:max-8] - h := sha256.New() - fmt.Fprint(h, name) - x := fmt.Sprintf("%x", h.Sum(nil)) - return base + x[:8] - } - return name - }, - - "lower": strings.ToLower, - "upper": strings.ToUpper, - }). - Parse(rt.SanitizeNameTmpl) - if err != nil { - return name, fmt.Errorf("could not parse sanitize name template %q: %w", rt.SanitizeNameTmpl, err) - } - buf := new(bytes.Buffer) - err = nt.Execute(buf, name) - if err != nil { - return name, fmt.Errorf("could not execute sanitize name template on %q: %w", name, err) - } - return strings.TrimSpace(buf.String()), nil + return rt.SanitizeNameTmpl.Execute(name) } func (template ResourceTemplate) GivesAttributeForFunctionality(attribute string, functionality Functionality) bool { @@ -293,53 +270,51 @@ func (template ResourceTemplate) ResourceContainsClassifications(needs []string) return true } -func (template ResourceTemplate) GetNamespacedProperty() *Property { +func (template ResourceTemplate) GetNamespacedProperty() Property { for _, property := range template.Properties { - if property.Namespace { + if property.Details().Namespace { return property } } return nil } -func (template ResourceTemplate) GetProperty(name string) *Property { - fields := strings.Split(name, ".") +func (template ResourceTemplate) GetProperty(path string) Property { + fields := strings.Split(path, ".") properties := template.Properties +FIELDS: for i, field := range fields { currFieldName := strings.Split(field, "[")[0] found := false for _, property := range properties { - if property.Name != currFieldName { + if property.Details().Name != currFieldName { continue } found = true if len(fields) == i+1 { - return property + // use a clone resource so we can modify the name in case anywhere in the path + // has index strings or map keys + clone := property.Clone() + details := clone.Details() + details.Path = path + return clone } else { - pType, err := property.PropertyType() - if err != nil { - return nil - } - // If the property types are a list or map, without sub fields - // we want to just return the property since we are setting an index or key of the end value - switch p := pType.(type) { - case *MapPropertyType: - if p.Value != "" { - return &Property{ - Type: p.Value, - Path: name, - } - } - case *ListPropertyType: - if p.Value != "" { - return &Property{ - Type: p.Value, - Path: name, - } + properties = property.SubProperties() + if len(properties) == 0 { + if mp, ok := property.(MapProperty); ok { + clone := mp.Value().Clone() + details := clone.Details() + details.Path = path + return clone + } else if cp, ok := property.(CollectionProperty); ok { + clone := cp.Item().Clone() + details := clone.Details() + details.Path = path + return clone } } - properties = property.Properties } + continue FIELDS } if !found { return nil @@ -350,7 +325,16 @@ func (template ResourceTemplate) GetProperty(name string) *Property { var ErrStopWalk = errors.New("stop walk") -func (tmpl ResourceTemplate) LoopProperties(res *construct.Resource, addProp func(*Property) error) error { +// ReplacePath runs a simple [strings.ReplaceAll] on the path of the property and all of its sub properties. +// NOTE: this mutates the property, so make sure to [Property.Clone] it first if you don't want that. +func ReplacePath(p Property, original, replacement string) { + p.Details().Path = strings.ReplaceAll(p.Details().Path, original, replacement) + for _, prop := range p.SubProperties() { + ReplacePath(prop, original, replacement) + } +} + +func (tmpl ResourceTemplate) LoopProperties(res *construct.Resource, addProp func(Property) error) error { queue := []Properties{tmpl.Properties} var props Properties var errs error @@ -366,34 +350,38 @@ func (tmpl ResourceTemplate) LoopProperties(res *construct.Resource, addProp fun continue } - if strings.HasPrefix(prop.Type, "list") || strings.HasPrefix(prop.Type, "set") { - p, err := res.GetProperty(prop.Path) + if strings.HasPrefix(prop.Type(), "list") || strings.HasPrefix(prop.Type(), "set") { + p, err := res.GetProperty(prop.Details().Path) if err != nil || p == nil { continue } // Because lists/sets will start as empty, do not recurse into their sub-properties if its not set. // To allow for defaults within list objects and operational rules to be run, we will look in the property // to see if there are values. - if strings.HasPrefix(prop.Type, "list") { + if strings.HasPrefix(prop.Type(), "list") { length := reflect.ValueOf(p).Len() for i := 0; i < length; i++ { subProperties := make(Properties) - for subK, subProp := range prop.Properties { + for subK, subProp := range prop.SubProperties() { propTemplate := subProp.Clone() - propTemplate.ReplacePath(prop.Path, fmt.Sprintf("%s[%d]", prop.Path, i)) + ReplacePath(propTemplate, prop.Details().Path, fmt.Sprintf("%s[%d]", prop.Details().Path, i)) subProperties[subK] = propTemplate } if len(subProperties) > 0 { queue = append(queue, subProperties) } } - } else if strings.HasPrefix(prop.Type, "set") { - hs := p.(set.HashedSet[string, any]) + } else if strings.HasPrefix(prop.Type(), "set") { + hs, ok := p.(set.HashedSet[string, any]) + if !ok { + errs = errors.Join(errs, fmt.Errorf("could not cast property to set")) + continue + } for k := range hs.ToMap() { subProperties := make(Properties) - for subK, subProp := range prop.Properties { + for subK, subProp := range prop.SubProperties() { propTemplate := subProp.Clone() - propTemplate.ReplacePath(prop.Path, fmt.Sprintf("%s[%s]", prop.Path, k)) + ReplacePath(propTemplate, prop.Details().Path, fmt.Sprintf("%s[%s]", prop.Details().Path, k)) subProperties[subK] = propTemplate } if len(subProperties) > 0 { @@ -402,8 +390,8 @@ func (tmpl ResourceTemplate) LoopProperties(res *construct.Resource, addProp fun } } - } else if prop.Properties != nil { - queue = append(queue, prop.Properties) + } else if prop.SubProperties() != nil { + queue = append(queue, prop.SubProperties()) } } } diff --git a/pkg/knowledge_base2/resource_template_test.go b/pkg/knowledge_base2/resource_template_test.go deleted file mode 100644 index 012db6adf..000000000 --- a/pkg/knowledge_base2/resource_template_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package knowledgebase2 - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" -) - -var testTemplate = ResourceTemplate{ - Properties: map[string]*Property{ - "name": { - Name: "name", - Type: "list", - Properties: map[string]*Property{ - "nested": { - Name: "nested", - Type: "string", - }, - }, - }, - }, -} - -func Test_PropertiesUnmarshalYaml(t *testing.T) { - tests := []struct { - name string - yaml string - expected Properties - }{ - { - name: "propagate path and name", - yaml: ` -name: - type: string - properties: - nested: - type: string`, - expected: map[string]*Property{ - "name": { - Name: "name", - Path: "name", - Type: "string", - Properties: map[string]*Property{ - "nested": { - Name: "nested", - Path: "name.nested", - Type: "string", - }, - }, - }, - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert := assert.New(t) - p := Properties{} - node := &yaml.Node{} - err := yaml.Unmarshal([]byte(test.yaml), node) - assert.NoError(err, "Expected no error") - err = p.UnmarshalYAML(node) - assert.NoError(err, "Expected no error") - assert.Equal(p, test.expected, "Expected unmarshalled yaml to equal expected") - }) - } -} - -func Test_GetProperty(t *testing.T) { - tests := []struct { - name string - template ResourceTemplate - property string - expected *Property - }{ - { - name: "Get top level property", - template: testTemplate, - property: "name", - expected: &Property{ - Name: "name", - Type: "list", - }, - }, - { - name: "Get nested property", - template: testTemplate, - property: "name.nested", - expected: &Property{ - Name: "nested", - Type: "string", - }, - }, - { - name: "Get nested property with array index", - template: testTemplate, - property: "name[0].nested", - expected: &Property{ - Name: "nested", - Type: "string", - }, - }, - { - name: "Get nested property with array index and array property", - template: testTemplate, - property: "name[0].nested[0]", - expected: &Property{ - Name: "nested", - Type: "string", - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert := assert.New(t) - actual := test.template.GetProperty(test.property) - assert.NotNil(actual, "Expected property %s to exist", test.property) - assert.Equal(actual.Name, test.expected.Name, "Expected property name %s to equal %s", actual.Name, test.expected.Name) - assert.Equal(actual.Type, test.expected.Type, "Expected property Type %s to equal %s", actual.Type, test.expected.Type) - }) - } -} - -func Test_GetNamespacedProperty(t *testing.T) { - tests := []struct { - name string - template ResourceTemplate - expected *Property - }{ - { - name: "Get namespaced property", - template: ResourceTemplate{Properties: map[string]*Property{"name": {Name: "name", Namespace: true}}}, - expected: &Property{Name: "name", Namespace: true}, - }, - { - name: "Get namespaced property with nested properties only looks top level", - template: ResourceTemplate{Properties: map[string]*Property{"name": {Name: "name", Properties: map[string]*Property{"nested": {Name: "nested", Namespace: true}}}}}, - expected: nil, - }, - { - name: "Get non namespaced property", - template: ResourceTemplate{Properties: map[string]*Property{"name": {Name: "name"}}}, - expected: nil, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert := assert.New(t) - actual := test.template.GetNamespacedProperty() - if test.expected == nil { - assert.Nil(actual, "Expected property to be nil") - return - } - assert.Equal(actual, test.expected, "Expected property %s to equal %s", actual, test.expected) - }) - } -} diff --git a/pkg/knowledge_base2/sanitization.go b/pkg/knowledge_base2/sanitization.go new file mode 100644 index 000000000..40881aa3d --- /dev/null +++ b/pkg/knowledge_base2/sanitization.go @@ -0,0 +1,59 @@ +package knowledgebase2 + +import ( + "bytes" + "crypto/sha256" + "fmt" + "regexp" + "strings" + "text/template" +) + +type ( + SanitizeTmpl struct { + template *template.Template + } +) + +func NewSanitizationTmpl(name string, tmpl string) (*SanitizeTmpl, error) { + t, err := template.New(name + "/sanitize"). + Funcs(template.FuncMap{ + "replace": func(pattern, replace, name string) (string, error) { + re, err := regexp.Compile(pattern) + if err != nil { + return name, err + } + return re.ReplaceAllString(name, replace), nil + }, + + "length": func(min, max int, name string) string { + if len(name) < min { + return name + strings.Repeat("0", min-len(name)) + } + if len(name) > max { + base := name[:max-8] + h := sha256.New() + fmt.Fprint(h, name) + x := fmt.Sprintf("%x", h.Sum(nil)) + return base + x[:8] + } + return name + }, + + "lower": strings.ToLower, + "upper": strings.ToUpper, + }). + Parse(tmpl) + return &SanitizeTmpl{ + template: t, + }, err +} + +func (t SanitizeTmpl) Execute(name string) (string, error) { + buf := new(bytes.Buffer) + err := t.template.Execute(buf, name) + if err != nil { + return name, fmt.Errorf("could not execute sanitize name template on %q: %w", name, err) + } + return strings.TrimSpace(buf.String()), nil +} diff --git a/pkg/templates/aws/resources/api_deployment.yaml b/pkg/templates/aws/resources/api_deployment.yaml index 7a5711b44..0912ae732 100644 --- a/pkg/templates/aws/resources/api_deployment.yaml +++ b/pkg/templates/aws/resources/api_deployment.yaml @@ -6,10 +6,10 @@ properties: type: resource(aws:rest_api) namespace: true operational_rule: - steps: - - direction: downstream - resources: - - aws:rest_api + step: + direction: downstream + resources: + - aws:rest_api Triggers: type: map(string,string) diff --git a/pkg/templates/aws/resources/api_integration.yaml b/pkg/templates/aws/resources/api_integration.yaml index 11481188f..24bc6348e 100644 --- a/pkg/templates/aws/resources/api_integration.yaml +++ b/pkg/templates/aws/resources/api_integration.yaml @@ -8,46 +8,42 @@ properties: type: resource(aws:rest_api) namespace: true operational_rule: - steps: - - direction: upstream - resources: - - aws:rest_api + step: + direction: upstream + resources: + - aws:rest_api Resource: type: resource(aws:api_resource) operational_rule: if: | {{ ne (fieldValue "Route" .Self) "/" }} - steps: - - direction: upstream - resources: - - selector: 'aws:api_resource' - properties: - FullPath: '{{ fieldValue "Route" .Self }}' - RestApi: '{{ fieldValue "RestApi" .Self }}' + step: + direction: upstream + resources: + - selector: 'aws:api_resource' + properties: + FullPath: '{{ fieldValue "Route" .Self }}' + RestApi: '{{ fieldValue "RestApi" .Self }}' Method: type: resource(aws:api_method) operational_rule: - steps: - - direction: upstream - resources: - - selector: aws:api_method - properties: - RestApi: '{{ fieldValue "RestApi" .Self }}' - Resource: '{{ fieldValue "Resource" .Self }}' + step: + direction: upstream + resources: + - selector: aws:api_method + properties: + RestApi: '{{ fieldValue "RestApi" .Self }}' + Resource: '{{ fieldValue "Resource" .Self }}' RequestParameters: type: map(string,string) operational_rule: - configuration_rules: - - resource: '{{ .Self }}' - configuration: - field: RequestParameters - value: | - {{ $params := split (fieldValue "Route" .Self) "/" | filterMatch "^{\\w+\\+?}$" }} - {{ zipToMap - ($params | mapString "{([^+}]*)\\+?}" "integration.request.path.$1") - ($params | mapString "{([^+}]*)\\+?}" "method.request.path.$1") - | toJson - }} + value: | + {{ $params := split (fieldValue "Route" .Self) "/" | filterMatch "^{\\w+\\+?}$" }} + {{ zipToMap + ($params | mapString "{([^+}]*)\\+?}" "integration.request.path.$1") + ($params | mapString "{([^+}]*)\\+?}" "method.request.path.$1") + | toJson + }} IntegrationHttpMethod: type: string default_value: ANY diff --git a/pkg/templates/aws/resources/api_method.yaml b/pkg/templates/aws/resources/api_method.yaml index 1211cc456..62bc27011 100644 --- a/pkg/templates/aws/resources/api_method.yaml +++ b/pkg/templates/aws/resources/api_method.yaml @@ -8,21 +8,21 @@ properties: type: resource(aws:rest_api) namespace: true operational_rule: - steps: - - direction: upstream - resources: - - aws:rest_api + step: + direction: upstream + resources: + - aws:rest_api Resource: type: resource(aws:api_resource) operational_rule: if: | {{ ne (fieldValue "Route" (downstream "aws:api_integration" .Self)) "/" }} - steps: - - direction: upstream - resources: - - selector: aws:api_resource - properties: - FullPath: '{{ fieldValue "Route" (downstream "aws:api_integration" .Self) }}' + step: + direction: upstream + resources: + - selector: aws:api_resource + properties: + FullPath: '{{ fieldValue "Route" (downstream "aws:api_integration" .Self) }}' HttpMethod: type: string default_value: ANY diff --git a/pkg/templates/aws/resources/api_resource.yaml b/pkg/templates/aws/resources/api_resource.yaml index fedf6ec50..f45a5bc4f 100644 --- a/pkg/templates/aws/resources/api_resource.yaml +++ b/pkg/templates/aws/resources/api_resource.yaml @@ -6,10 +6,10 @@ properties: type: resource(aws:rest_api) namespace: true operational_rule: - steps: - - direction: upstream - resources: - - aws:rest_api + step: + direction: upstream + resources: + - aws:rest_api ParentResource: type: resource(aws:api_resource) configuration_disabled: true @@ -17,19 +17,19 @@ properties: if: | # Only need a parent resource if this one isn't the root {{ $parts := slice (split (fieldValue "FullPath" .Self) "/") 1 }} {{ gt (len $parts) 1 }} - steps: - - direction: upstream - resources: - - selector: | + step: + direction: upstream + resources: + - selector: | + {{ $parts := split (fieldValue "FullPath" .Self) "/" -}} + {{ $parts := slice $parts 1 (sub (len $parts) 1) -}} + aws:api_resource:{{ join $parts "-" | sanitizeName }} + properties: + FullPath: | {{ $parts := split (fieldValue "FullPath" .Self) "/" -}} - {{ $parts := slice $parts 1 (sub (len $parts) 1) -}} - aws:api_resource:{{ join $parts "-" | sanitizeName }} - properties: - FullPath: | - {{ $parts := split (fieldValue "FullPath" .Self) "/" -}} - {{ $parts := slice $parts 0 (sub (len $parts) 1) -}} - {{ join $parts "/" }} - RestApi: '{{ fieldValue "RestApi" .Self }}' + {{ $parts := slice $parts 0 (sub (len $parts) 1) -}} + {{ join $parts "/" }} + RestApi: '{{ fieldValue "RestApi" .Self }}' FullPath: type: string configuration_disabled: true diff --git a/pkg/templates/aws/resources/api_stage.yaml b/pkg/templates/aws/resources/api_stage.yaml index c6d05b509..b966ae98f 100644 --- a/pkg/templates/aws/resources/api_stage.yaml +++ b/pkg/templates/aws/resources/api_stage.yaml @@ -9,17 +9,17 @@ properties: type: resource(aws:rest_api) namespace: true operational_rule: - steps: - - direction: downstream - resources: - - aws:rest_api + step: + direction: downstream + resources: + - aws:rest_api Deployment: type: resource(aws:api_deployment) operational_rule: - steps: - - direction: downstream - resources: - - aws:api_deployment + step: + direction: downstream + resources: + - aws:api_deployment StageInvokeUrl: type: string configuration_disabled: true diff --git a/pkg/templates/aws/resources/app_runer_service.yaml b/pkg/templates/aws/resources/app_runer_service.yaml index 493ae920e..2bbda6fff 100644 --- a/pkg/templates/aws/resources/app_runer_service.yaml +++ b/pkg/templates/aws/resources/app_runer_service.yaml @@ -14,19 +14,19 @@ properties: Image: type: resource(aws:ecr_image) operational_rule: - steps: - - direction: downstream - resources: - - aws:ecr_image:{{ .Self.Name }}-image - unique: true + step: + direction: downstream + resources: + - aws:ecr_image:{{ .Self.Name }}-image + unique: true InstanceRole: type: resource(aws:iam_role) operational_rule: - steps: - - direction: downstream - resources: - - aws:iam_role:{{ .Self.Name }}-instance-role - unique: true + step: + direction: downstream + resources: + - aws:iam_role:{{ .Self.Name }}-instance-role + unique: true EnvironmentVariables: type: map(string,string) Port: diff --git a/pkg/templates/aws/resources/availability_zone.yaml b/pkg/templates/aws/resources/availability_zone.yaml index 82611b3ab..fc44b505c 100644 --- a/pkg/templates/aws/resources/availability_zone.yaml +++ b/pkg/templates/aws/resources/availability_zone.yaml @@ -9,10 +9,10 @@ properties: type: resource(aws:region) namespace: true operational_rule: - steps: - - direction: downstream - resources: - - aws:region + step: + direction: downstream + resources: + - aws:region delete_context: requires_no_upstream: true diff --git a/pkg/templates/aws/resources/ec2_instance.yaml b/pkg/templates/aws/resources/ec2_instance.yaml index 0b34b72c2..96ab619a6 100644 --- a/pkg/templates/aws/resources/ec2_instance.yaml +++ b/pkg/templates/aws/resources/ec2_instance.yaml @@ -5,35 +5,35 @@ properties: InstanceProfile: type: resource(aws:iam_instance_profile) operational_rule: - steps: - - direction: downstream - resources: - - aws:iam_instance_profile:{{ .Self.Name }} + step: + direction: downstream + resources: + - aws:iam_instance_profile:{{ .Self.Name }} SecurityGroup: type: list(resource(aws:security_group)) operational_rule: - steps: - - direction: upstream - resources: - - aws:security_group - unique: true + step: + direction: upstream + resources: + - aws:security_group + unique: true Subnet: type: resource(aws:subnet) operational_rule: - steps: - - direction: downstream - resources: - - selector: aws:subnet - properties: - Type: private - - aws:subnet + step: + direction: downstream + resources: + - selector: aws:subnet + properties: + Type: private + - aws:subnet AMI: type: resource(aws:ami) operational_rule: - steps: - - direction: downstream - resources: - - aws:ami + step: + direction: downstream + resources: + - aws:ami InstanceType: type: string default: t3.medium diff --git a/pkg/templates/aws/resources/ecr_image.yaml b/pkg/templates/aws/resources/ecr_image.yaml index cdac13539..8ce6c0992 100644 --- a/pkg/templates/aws/resources/ecr_image.yaml +++ b/pkg/templates/aws/resources/ecr_image.yaml @@ -9,10 +9,10 @@ properties: Repo: type: resource(aws:ecr_repo) operational_rule: - steps: - - direction: downstream - resources: - - aws:ecr_repo + step: + direction: downstream + resources: + - aws:ecr_repo Context: type: string default_value: '.' diff --git a/pkg/templates/aws/resources/ecs_service.yaml b/pkg/templates/aws/resources/ecs_service.yaml index a241e1bee..120ad4f8a 100644 --- a/pkg/templates/aws/resources/ecs_service.yaml +++ b/pkg/templates/aws/resources/ecs_service.yaml @@ -8,10 +8,10 @@ properties: Cluster: type: resource(aws:ecs_cluster) operational_rule: - steps: - - direction: downstream - resources: - - aws:ecs_cluster + step: + direction: downstream + resources: + - aws:ecs_cluster DeploymentCircuitBreaker: type: map properties: @@ -40,30 +40,30 @@ properties: SecurityGroups: type: list(resource(aws:security_group)) operational_rule: - steps: - - direction: upstream - resources: - - aws:security_group - unique: true + step: + direction: upstream + resources: + - aws:security_group + unique: true Subnets: type: list(resource(aws:subnet)) operational_rule: - steps: - - direction: downstream - num_needed: 2 - resources: - - selector: aws:subnet - properties: - Type: private - - aws:subnet + step: + direction: downstream + num_needed: 2 + resources: + - selector: aws:subnet + properties: + Type: private + - aws:subnet TaskDefinition: type: resource(aws:ecs_task_definition) operational_rule: - steps: - - direction: downstream - resources: - - aws:ecs_task_definition:{{ .Self.Name }} - unique: true + step: + direction: downstream + resources: + - aws:ecs_task_definition:{{ .Self.Name }} + unique: true consumption: consumed: diff --git a/pkg/templates/aws/resources/ecs_task_definition.yaml b/pkg/templates/aws/resources/ecs_task_definition.yaml index d1959f269..cf2b00fa6 100644 --- a/pkg/templates/aws/resources/ecs_task_definition.yaml +++ b/pkg/templates/aws/resources/ecs_task_definition.yaml @@ -5,11 +5,11 @@ properties: Image: type: resource(aws:ecr_image) operational_rule: - steps: - - direction: downstream - resources: - - aws:ecr_image:{{ .Self.Name }}-image - unique: true + step: + direction: downstream + resources: + - aws:ecr_image:{{ .Self.Name }}-image + unique: true MountPoints: type: set properties: @@ -30,29 +30,29 @@ properties: LogGroup: type: resource(aws:log_group) operational_rule: - steps: - - direction: downstream - resources: - - aws:log_group:{{ .Self.Name }}-log-group - unique: true + step: + direction: downstream + resources: + - aws:log_group:{{ .Self.Name }}-log-group + unique: true ExecutionRole: type: resource(aws:iam_role) operational_rule: - steps: - - direction: downstream - resources: - - aws:iam_role:{{ .Self.Name }}-execution-role - unique: true + step: + direction: downstream + resources: + - aws:iam_role:{{ .Self.Name }}-execution-role + unique: true TaskRole: type: resource(aws:iam_role) default_value: '{{ fieldValue "ExecutionRole" .Self }}' Region: type: resource(aws:region) operational_rule: - steps: - - direction: downstream - resources: - - aws:region + step: + direction: downstream + resources: + - aws:region NetworkMode: type: string default_value: awsvpc diff --git a/pkg/templates/aws/resources/efs_access_point.yaml b/pkg/templates/aws/resources/efs_access_point.yaml index d795fed18..5a3be3081 100644 --- a/pkg/templates/aws/resources/efs_access_point.yaml +++ b/pkg/templates/aws/resources/efs_access_point.yaml @@ -6,10 +6,10 @@ properties: type: resource(aws:efs_file_system) namespace: true operational_rule: - steps: - - direction: downstream - resources: - - aws:efs_file_system + step: + direction: downstream + resources: + - aws:efs_file_system PosixUser: type: map properties: diff --git a/pkg/templates/aws/resources/efs_file_system.yaml b/pkg/templates/aws/resources/efs_file_system.yaml index 393bf4b37..1c5955a0f 100644 --- a/pkg/templates/aws/resources/efs_file_system.yaml +++ b/pkg/templates/aws/resources/efs_file_system.yaml @@ -25,10 +25,10 @@ properties: AvailabilityZone: type: resource(aws:availability_zone) operational_rule: - steps: - - direction: downstream - resources: - - aws:availability_zone + step: + direction: downstream + resources: + - aws:availability_zone CreationToken: type: string Id: diff --git a/pkg/templates/aws/resources/efs_mount_target.yaml b/pkg/templates/aws/resources/efs_mount_target.yaml index d169ca0a8..ba6d3b9f6 100644 --- a/pkg/templates/aws/resources/efs_mount_target.yaml +++ b/pkg/templates/aws/resources/efs_mount_target.yaml @@ -6,28 +6,28 @@ properties: type: resource(aws:efs_file_system) namespace: true operational_rule: - steps: - - direction: downstream - resources: - - aws:efs_file_system + step: + direction: downstream + resources: + - aws:efs_file_system Subnet: type: resource(aws:subnet) operational_rule: - steps: - - direction: downstream - resources: - - selector: aws:subnet - properties: - Type: private - - aws:subnet + step: + direction: downstream + resources: + - selector: aws:subnet + properties: + Type: private + - aws:subnet SecurityGroups: type: list(resource(aws:security_group)) operational_rule: - steps: - - direction: upstream - resources: - - aws:security_group - unique: true + step: + direction: upstream + resources: + - aws:security_group + unique: true IpAddress: type: string diff --git a/pkg/templates/aws/resources/eks_cluster.yaml b/pkg/templates/aws/resources/eks_cluster.yaml index 1f4b92af5..c2877487a 100644 --- a/pkg/templates/aws/resources/eks_cluster.yaml +++ b/pkg/templates/aws/resources/eks_cluster.yaml @@ -5,51 +5,42 @@ properties: ClusterRole: type: resource(aws:iam_role) operational_rule: - steps: - - direction: downstream - resources: - - aws:iam_role:ClusterRole-{{ .Self.Name }} - unique: true + step: + direction: downstream + resources: + - aws:iam_role:ClusterRole-{{ .Self.Name }} + unique: true Vpc: type: resource(aws:vpc) operational_rule: - steps: - - direction: downstream - resources: - - aws:vpc + step: + direction: downstream + resources: + - aws:vpc Subnets: type: list(resource(aws:subnet)) operational_rule: - steps: - - direction: downstream - num_needed: 2 - resources: - - selector: aws:subnet - properties: - Type: private - - aws:subnet - - direction: downstream - num_needed: 2 - resources: - - selector: aws:subnet - properties: - Type: public + step: + direction: downstream + num_needed: 2 + resources: + - selector: aws:subnet SecurityGroups: type: list(resource(aws:security_group)) operational_rule: - steps: - - direction: upstream - resources: - - aws:security_group - unique: true + step: + direction: upstream + resources: + - aws:security_group + unique: true KubeConfig: type: resource(kubernetes:kube_config) operational_rule: - steps: - - direction: upstream - resources: - - kubernetes:kube_config - unique: true + step: + direction: upstream + resources: + - kubernetes:kube_config + unique: true Name: type: string configuration_disabled: true diff --git a/pkg/templates/aws/resources/eks_fargate_profile.yaml b/pkg/templates/aws/resources/eks_fargate_profile.yaml index 804543c03..b9b799c20 100644 --- a/pkg/templates/aws/resources/eks_fargate_profile.yaml +++ b/pkg/templates/aws/resources/eks_fargate_profile.yaml @@ -5,36 +5,36 @@ properties: Cluster: type: resource(aws:eks_cluster) operational_rule: - steps: - - direction: downstream - resources: - - aws:eks_cluster + step: + direction: downstream + resources: + - aws:eks_cluster PodExecutionRole: type: resource(aws:iam_role) operational_rule: - steps: - - direction: downstream - resources: - - aws:iam_role - unique: true + step: + direction: downstream + resources: + - aws:iam_role + unique: true Subnets: type: list(resource(aws:subnet)) operational_rule: - steps: - - direction: downstream - resources: - - selector: aws:subnet - properties: - Type: private - - aws:subnet - num_needed: 2 + step: + direction: downstream + resources: + - selector: aws:subnet + properties: + Type: private + - aws:subnet + num_needed: 2 Selectors: type: list properties: Namespace: type: string Labels: - type: map(string) + type: map(string,string) classifications: is: diff --git a/pkg/templates/aws/resources/eks_node_group.yaml b/pkg/templates/aws/resources/eks_node_group.yaml index 3edd07fbf..57e763929 100644 --- a/pkg/templates/aws/resources/eks_node_group.yaml +++ b/pkg/templates/aws/resources/eks_node_group.yaml @@ -5,29 +5,29 @@ properties: Cluster: type: resource(aws:eks_cluster) operational_rule: - steps: - - direction: downstream - resources: - - aws:eks_cluster + step: + direction: downstream + resources: + - aws:eks_cluster NodeRole: type: resource(aws:iam_role) operational_rule: - steps: - - direction: downstream - resources: - - aws:iam_role - unique: true + step: + direction: downstream + resources: + - aws:iam_role + unique: true Subnets: type: list(resource(aws:subnet)) operational_rule: - steps: - - direction: downstream - resources: - - selector: aws:subnet - properties: - Type: private - - aws:subnet - num_needed: 2 + step: + direction: downstream + resources: + - selector: aws:subnet + properties: + Type: private + - aws:subnet + num_needed: 2 AmiType: type: string default_value: AL2_x86_64 diff --git a/pkg/templates/aws/resources/elasticache_cluster.yaml b/pkg/templates/aws/resources/elasticache_cluster.yaml index 8280c2524..646ad3a48 100644 --- a/pkg/templates/aws/resources/elasticache_cluster.yaml +++ b/pkg/templates/aws/resources/elasticache_cluster.yaml @@ -8,26 +8,26 @@ properties: CloudwatchGroup: type: resource(aws:log_group) operational_rule: - steps: - - direction: downstream - resources: - - aws:log_group - unique: true + step: + direction: downstream + resources: + - aws:log_group + unique: true SubnetGroup: type: resource(aws:elasticache_subnet_group) operational_rule: - steps: - - direction: downstream - resources: - - aws:elasticache_subnet_group + step: + direction: downstream + resources: + - aws:elasticache_subnet_group SecurityGroups: type: list(resource(aws:security_group)) operational_rule: - steps: - - direction: upstream - resources: - - aws:security_group - unique: true + step: + direction: upstream + resources: + - aws:security_group + unique: true NodeType: type: string default_value: cache.t2.micro diff --git a/pkg/templates/aws/resources/elasticache_subnet_group.yaml b/pkg/templates/aws/resources/elasticache_subnet_group.yaml index 26301ac8f..596c32f71 100644 --- a/pkg/templates/aws/resources/elasticache_subnet_group.yaml +++ b/pkg/templates/aws/resources/elasticache_subnet_group.yaml @@ -5,14 +5,14 @@ properties: Subnets: type: list(resource(aws:subnet)) operational_rule: - steps: - - direction: downstream - num_needed: 2 - resources: - - selector: aws:subnet - properties: - Type: private - - aws:subnet + step: + direction: downstream + num_needed: 2 + resources: + - selector: aws:subnet + properties: + Type: private + - aws:subnet delete_context: requires_no_upstream: true diff --git a/pkg/templates/aws/resources/iam_instance_profile.yaml b/pkg/templates/aws/resources/iam_instance_profile.yaml index fe2b14656..0b16a62f3 100644 --- a/pkg/templates/aws/resources/iam_instance_profile.yaml +++ b/pkg/templates/aws/resources/iam_instance_profile.yaml @@ -5,8 +5,8 @@ properties: Role: type: resource(aws:iam_role) operational_rule: - steps: - - direction: downstream - resources: - - aws:iam_role - unique: true + step: + direction: downstream + resources: + - aws:iam_role + unique: true diff --git a/pkg/templates/aws/resources/iam_oidc_provider.yaml b/pkg/templates/aws/resources/iam_oidc_provider.yaml index 69e1614d0..57bcb0e4b 100644 --- a/pkg/templates/aws/resources/iam_oidc_provider.yaml +++ b/pkg/templates/aws/resources/iam_oidc_provider.yaml @@ -10,10 +10,10 @@ properties: Region: type: resource(aws:region) operational_rule: - steps: - - direction: downstream - resources: - - aws:region + step: + direction: downstream + resources: + - aws:region Arn: type: string configuration_disabled: true diff --git a/pkg/templates/aws/resources/internet_gateway.yaml b/pkg/templates/aws/resources/internet_gateway.yaml index 14a6a5458..c75072bb4 100644 --- a/pkg/templates/aws/resources/internet_gateway.yaml +++ b/pkg/templates/aws/resources/internet_gateway.yaml @@ -7,10 +7,10 @@ properties: namespace: true required: true operational_rule: - steps: - - direction: downstream - resources: - - aws:vpc + step: + direction: downstream + resources: + - aws:vpc classification: gives: diff --git a/pkg/templates/aws/resources/lambda_event_source_mapping.yaml b/pkg/templates/aws/resources/lambda_event_source_mapping.yaml index 6a722003b..ed4081b56 100644 --- a/pkg/templates/aws/resources/lambda_event_source_mapping.yaml +++ b/pkg/templates/aws/resources/lambda_event_source_mapping.yaml @@ -5,19 +5,19 @@ properties: Function: type: resource(aws:lambda_function) operational_rule: - steps: - - direction: downstream - resources: - - aws:lambda_function + step: + direction: downstream + resources: + - aws:lambda_function # fail_if_missing: true EventSource: type: resource operational_rule: - steps: - - direction: upstream - resources: - - aws:sqs_queue - # fail_if_missing: true + step: + direction: upstream + resources: + - aws:sqs_queue + # fail_if_missing: true FilterCriteria: type: list diff --git a/pkg/templates/aws/resources/lambda_function.yaml b/pkg/templates/aws/resources/lambda_function.yaml index 217f8d3ba..2e7882429 100644 --- a/pkg/templates/aws/resources/lambda_function.yaml +++ b/pkg/templates/aws/resources/lambda_function.yaml @@ -5,19 +5,19 @@ properties: ExecutionRole: type: resource(aws:iam_role) operational_rule: - steps: - - direction: downstream - resources: - - aws:iam_role:{{ .Self.Name }}-ExecutionRole - unique: true + step: + direction: downstream + resources: + - aws:iam_role:{{ .Self.Name }}-ExecutionRole + unique: true Image: type: resource(aws:ecr_image) operational_rule: - steps: - - direction: downstream - resources: - - aws:ecr_image:{{ .Self.Name }}-image - unique: true + step: + direction: downstream + resources: + - aws:ecr_image:{{ .Self.Name }}-image + unique: true EnvironmentVariables: type: map(string,string) SecurityGroups: @@ -25,42 +25,46 @@ properties: operational_rule: if: | {{ hasDownstream "aws:vpc" .Self }} - steps: - - direction: upstream - resources: - - aws:security_group - unique: true + step: + direction: upstream + resources: + - aws:security_group + unique: true Subnets: type: list(resource(aws:subnet)) operational_rule: if: | {{ hasDownstream "aws:vpc" .Self }} - steps: - - direction: downstream - num_needed: 2 - resources: - - selector: aws:subnet - properties: - Type: private - - aws:subnet + step: + direction: downstream + num_needed: 2 + resources: + - selector: aws:subnet + properties: + Type: private + - aws:subnet Timeout: type: int default_value: 180 + min_value: 1 + max_value: 900 MemorySize: type: int default_value: 512 + min_value: 128 + max_value: 10240 EfsAccessPoint: type: resource(aws:efs_access_point) LogGroup: type: resource(aws:log_group) operational_rule: - steps: - - direction: downstream - resources: - - selector: aws:log_group:{{ .Self.Name }}-log-group - properties: - LogGroupName: /aws/lambda/{{ .Self.Name }} - unique: true + step: + direction: downstream + resources: + - selector: aws:log_group:{{ .Self.Name }}-log-group + properties: + LogGroupName: /aws/lambda/{{ .Self.Name }} + unique: true LambdaIntegrationUri: type: string configuration_disabled: true @@ -76,7 +80,7 @@ path_satisfaction: - permissions as_source: - network#Subnets - - network # This is required as well in case the lambda does not already exist in a vpc, we need the path evaluation done in 2 steps + - network # This is required as well in case the lambda does not already exist in a vpc, we need the path evaluation done in 2 step consumption: consumed: diff --git a/pkg/templates/aws/resources/lambda_permission.yaml b/pkg/templates/aws/resources/lambda_permission.yaml index 9f511ae5c..949abb975 100644 --- a/pkg/templates/aws/resources/lambda_permission.yaml +++ b/pkg/templates/aws/resources/lambda_permission.yaml @@ -5,10 +5,10 @@ properties: Function: type: resource(aws:lambda_function) operational_rule: - steps: - - direction: downstream - resources: - - aws:lambda_function + step: + direction: downstream + resources: + - aws:lambda_function Principal: type: string Action: diff --git a/pkg/templates/aws/resources/listener_certificate.yaml b/pkg/templates/aws/resources/listener_certificate.yaml index 8c5cc6c7b..e226b78f1 100644 --- a/pkg/templates/aws/resources/listener_certificate.yaml +++ b/pkg/templates/aws/resources/listener_certificate.yaml @@ -5,10 +5,10 @@ properties: Certificate: type: resource(aws:acm_certificate) operational_rule: - steps: - - direction: downstream - resources: - - aws:acm_certificate + step: + direction: downstream + resources: + - aws:acm_certificate required: true Listener: type: resource(aws:load_balancer_listener) diff --git a/pkg/templates/aws/resources/load_balancer.yaml b/pkg/templates/aws/resources/load_balancer.yaml index 5955cd9af..8fd42f0f8 100644 --- a/pkg/templates/aws/resources/load_balancer.yaml +++ b/pkg/templates/aws/resources/load_balancer.yaml @@ -31,31 +31,34 @@ properties: type: list(resource(aws:security_group)) operational_rule: if: '{{ fieldValue "Scheme" .Self | eq "internet-facing" }}' - steps: - - direction: upstream - resources: - - aws:security_group - unique: true + step: + direction: upstream + resources: + - aws:security_group + unique: true Subnets: type: list(resource(aws:subnet)) operational_rule: - steps: - - direction: downstream - resources: - - selector: aws:subnet - properties: - Type: | - {{- if eq (fieldValue "Scheme" .Self) "internet-facing"}} - public - {{- else}} - private - {{- end}} - num_needed: 2 + step: + direction: downstream + resources: + - selector: aws:subnet + properties: + Type: | + {{- if eq (fieldValue "Scheme" .Self) "internet-facing"}} + public + {{- else}} + private + {{- end}} + num_needed: 2 Tags: type: map(string,string) Type: type: string default_value: network + allowed_values: + - network + - application NlbUri: type: string configuration_disabled: true diff --git a/pkg/templates/aws/resources/nat_gateway.yaml b/pkg/templates/aws/resources/nat_gateway.yaml index 36be39e51..f660eaa4a 100644 --- a/pkg/templates/aws/resources/nat_gateway.yaml +++ b/pkg/templates/aws/resources/nat_gateway.yaml @@ -6,23 +6,23 @@ properties: type: resource(aws:elastic_ip) required: true operational_rule: - steps: - - direction: downstream - resources: - - aws:elastic_ip - unique: true + step: + direction: downstream + resources: + - aws:elastic_ip + unique: true Subnet: type: resource(aws:subnet) required: true namespace: true operational_rule: - steps: - - direction: downstream - resources: - - selector: aws:subnet - properties: - Type: public - selection_operator: spread + step: + direction: downstream + resources: + - selector: aws:subnet + properties: + Type: public + selection_operator: spread delete_context: requires_no_upstream: true diff --git a/pkg/templates/aws/resources/private_dns_namespace.yaml b/pkg/templates/aws/resources/private_dns_namespace.yaml index e88eb3a4d..e29a12d9d 100644 --- a/pkg/templates/aws/resources/private_dns_namespace.yaml +++ b/pkg/templates/aws/resources/private_dns_namespace.yaml @@ -5,10 +5,10 @@ properties: Vpc: type: resource(aws:vpc) operational_rule: - steps: - - direction: downstream - resources: - - aws:vpc + step: + direction: downstream + resources: + - aws:vpc classification: is: diff --git a/pkg/templates/aws/resources/rds_instance.yaml b/pkg/templates/aws/resources/rds_instance.yaml index b2c3d48f5..74587f763 100644 --- a/pkg/templates/aws/resources/rds_instance.yaml +++ b/pkg/templates/aws/resources/rds_instance.yaml @@ -20,18 +20,18 @@ properties: SubnetGroup: type: resource(aws:rds_subnet_group) operational_rule: - steps: - - direction: downstream - resources: - - aws:rds_subnet_group + step: + direction: downstream + resources: + - aws:rds_subnet_group SecurityGroups: type: list(resource(aws:security_group)) operational_rule: - steps: - - direction: upstream - resources: - - aws:security_group - unique: true + step: + direction: upstream + resources: + - aws:security_group + unique: true DatabaseName: type: string default_value: main diff --git a/pkg/templates/aws/resources/rds_proxy.yaml b/pkg/templates/aws/resources/rds_proxy.yaml index 339e451f7..22ef1bb29 100644 --- a/pkg/templates/aws/resources/rds_proxy.yaml +++ b/pkg/templates/aws/resources/rds_proxy.yaml @@ -17,30 +17,30 @@ properties: Role: type: resource(aws:iam_role) operational_rule: - steps: - - direction: downstream - resources: - - aws:iam_role - unique: true + step: + direction: downstream + resources: + - aws:iam_role + unique: true SecurityGroups: type: list(resource(aws:security_group)) operational_rule: - steps: - - direction: upstream - resources: - - aws:security_group - unique: true + step: + direction: upstream + resources: + - aws:security_group + unique: true Subnets: type: list(resource(aws:subnet)) operational_rule: - steps: - - direction: downstream - resources: - - selector: aws:subnet - properties: - Type: private - - aws:subnet - num_needed: 2 + step: + direction: downstream + resources: + - selector: aws:subnet + properties: + Type: private + - aws:subnet + num_needed: 2 Auths: type: set(map) properties: diff --git a/pkg/templates/aws/resources/rds_subnet_group.yaml b/pkg/templates/aws/resources/rds_subnet_group.yaml index fbbabfd28..812b68477 100644 --- a/pkg/templates/aws/resources/rds_subnet_group.yaml +++ b/pkg/templates/aws/resources/rds_subnet_group.yaml @@ -15,14 +15,14 @@ properties: Subnets: type: list(resource(aws:subnet)) operational_rule: - steps: - - direction: downstream - num_needed: 2 - resources: - - selector: aws:subnet - properties: - Type: private - - aws:subnet + step: + direction: downstream + num_needed: 2 + resources: + - selector: aws:subnet + properties: + Type: private + - aws:subnet Tags: type: map(string,string) diff --git a/pkg/templates/aws/resources/rest_api.yaml b/pkg/templates/aws/resources/rest_api.yaml index 3e5feed1c..8df2f83f9 100644 --- a/pkg/templates/aws/resources/rest_api.yaml +++ b/pkg/templates/aws/resources/rest_api.yaml @@ -12,10 +12,10 @@ properties: Stages: type: list(resource(aws:api_stage)) operational_rule: - steps: - - direction: upstream - resources: - - aws:api_stage + step: + direction: upstream + resources: + - aws:api_stage ChildResources: type: string configuration_disabled: true diff --git a/pkg/templates/aws/resources/route_table.yaml b/pkg/templates/aws/resources/route_table.yaml index bac58e2e7..bd720e40f 100644 --- a/pkg/templates/aws/resources/route_table.yaml +++ b/pkg/templates/aws/resources/route_table.yaml @@ -5,10 +5,10 @@ properties: Vpc: type: resource(aws:vpc) operational_rule: - steps: - - direction: downstream - resources: - - aws:vpc + step: + direction: downstream + resources: + - aws:vpc Routes: type: list properties: diff --git a/pkg/templates/aws/resources/secret_version.yaml b/pkg/templates/aws/resources/secret_version.yaml index cb426065d..01436a142 100644 --- a/pkg/templates/aws/resources/secret_version.yaml +++ b/pkg/templates/aws/resources/secret_version.yaml @@ -6,11 +6,11 @@ properties: type: resource(aws:secret) namespace: true operational_rule: - steps: - - direction: upstream - resources: - - aws:secret - unique: true + step: + direction: upstream + resources: + - aws:secret + unique: true Type: type: string Content: diff --git a/pkg/templates/aws/resources/security_group.yaml b/pkg/templates/aws/resources/security_group.yaml index d9677971c..18fcc1af7 100644 --- a/pkg/templates/aws/resources/security_group.yaml +++ b/pkg/templates/aws/resources/security_group.yaml @@ -5,10 +5,10 @@ properties: type: resource(aws:vpc) namespace: true operational_rule: - steps: - - direction: downstream - resources: - - aws:vpc + step: + direction: downstream + resources: + - aws:vpc IngressRules: type: set properties: diff --git a/pkg/templates/aws/resources/subnet.yaml b/pkg/templates/aws/resources/subnet.yaml index 65f8e0531..d8f0a434f 100644 --- a/pkg/templates/aws/resources/subnet.yaml +++ b/pkg/templates/aws/resources/subnet.yaml @@ -7,27 +7,27 @@ properties: required: true namespace: true operational_rule: - steps: - - direction: downstream - resources: - - aws:vpc + step: + direction: downstream + resources: + - aws:vpc AvailabilityZone: type: resource(aws:availability_zone) required: true operational_rule: - steps: - - direction: downstream - resources: - - aws:availability_zone - selection_operator: spread + step: + direction: downstream + resources: + - aws:availability_zone + selection_operator: spread RouteTable: type: resource(aws:route_table) operational_rule: - steps: - - direction: downstream - resources: - - aws:route_table - unique: true + step: + direction: downstream + resources: + - aws:route_table + unique: true CidrBlock: type: string default_value: | diff --git a/pkg/templates/aws/resources/vpc_endpoint.yaml b/pkg/templates/aws/resources/vpc_endpoint.yaml index 7472ffa81..aeedf65fb 100644 --- a/pkg/templates/aws/resources/vpc_endpoint.yaml +++ b/pkg/templates/aws/resources/vpc_endpoint.yaml @@ -6,17 +6,17 @@ properties: type: resource(aws:vpc) namespace: true operational_rule: - steps: - - direction: downstream - resources: - - aws:vpc + step: + direction: downstream + resources: + - aws:vpc Region: type: resource(aws:region) operational_rule: - steps: - - direction: downstream - resources: - - aws:region + step: + direction: downstream + resources: + - aws:region ServiceName: type: string VpcEndpointType: diff --git a/pkg/templates/kubernetes/models/container.yaml b/pkg/templates/kubernetes/models/container.yaml index bb8a7150b..49c7e45c4 100644 --- a/pkg/templates/kubernetes/models/container.yaml +++ b/pkg/templates/kubernetes/models/container.yaml @@ -6,13 +6,13 @@ properties: image: type: string operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - image - unique: true - use_property_ref: ImageName + step: + direction: downstream + resources: + - classifications: + - image + unique: true + use_property_ref: ImageName required: true command: type: list(string) diff --git a/pkg/templates/kubernetes/models/pod_spec.yaml b/pkg/templates/kubernetes/models/pod_spec.yaml index f016bfb22..1d1ceb1fa 100644 --- a/pkg/templates/kubernetes/models/pod_spec.yaml +++ b/pkg/templates/kubernetes/models/pod_spec.yaml @@ -24,11 +24,11 @@ properties: serviceAccountName: type: resource(kubernetes:service_account) operational_rule: - steps: - - direction: downstream - resources: - - kubernetes:service_account:{{ .Self.Name }} - unique: true + step: + direction: downstream + resources: + - kubernetes:service_account:{{ .Self.Name }} + unique: true automountServiceAccountToken: type: bool default_value: true \ No newline at end of file diff --git a/pkg/templates/kubernetes/resources/cluster_set.yaml b/pkg/templates/kubernetes/resources/cluster_set.yaml index 4f5e9fa4b..2c9575a7b 100644 --- a/pkg/templates/kubernetes/resources/cluster_set.yaml +++ b/pkg/templates/kubernetes/resources/cluster_set.yaml @@ -14,12 +14,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: diff --git a/pkg/templates/kubernetes/resources/config_map.yaml b/pkg/templates/kubernetes/resources/config_map.yaml index 73222801b..ad11b544d 100644 --- a/pkg/templates/kubernetes/resources/config_map.yaml +++ b/pkg/templates/kubernetes/resources/config_map.yaml @@ -14,12 +14,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: diff --git a/pkg/templates/kubernetes/resources/deployment.yaml b/pkg/templates/kubernetes/resources/deployment.yaml index 0719ff4bc..ba0a1b39a 100644 --- a/pkg/templates/kubernetes/resources/deployment.yaml +++ b/pkg/templates/kubernetes/resources/deployment.yaml @@ -14,12 +14,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: diff --git a/pkg/templates/kubernetes/resources/helm_chart.yaml b/pkg/templates/kubernetes/resources/helm_chart.yaml index 4c69d3018..f1fdfc159 100644 --- a/pkg/templates/kubernetes/resources/helm_chart.yaml +++ b/pkg/templates/kubernetes/resources/helm_chart.yaml @@ -18,12 +18,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Repo: type: string Directory: diff --git a/pkg/templates/kubernetes/resources/horizontal_pod_autoscaler.yaml b/pkg/templates/kubernetes/resources/horizontal_pod_autoscaler.yaml index 48b5b22b8..06f7efdfa 100644 --- a/pkg/templates/kubernetes/resources/horizontal_pod_autoscaler.yaml +++ b/pkg/templates/kubernetes/resources/horizontal_pod_autoscaler.yaml @@ -14,12 +14,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: diff --git a/pkg/templates/kubernetes/resources/kustomize_directory.yaml b/pkg/templates/kubernetes/resources/kustomize_directory.yaml index 46b40f990..4039540eb 100644 --- a/pkg/templates/kubernetes/resources/kustomize_directory.yaml +++ b/pkg/templates/kubernetes/resources/kustomize_directory.yaml @@ -6,12 +6,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Directory: type: string diff --git a/pkg/templates/kubernetes/resources/manifest.yaml b/pkg/templates/kubernetes/resources/manifest.yaml index c49dc390a..3603ab6e7 100644 --- a/pkg/templates/kubernetes/resources/manifest.yaml +++ b/pkg/templates/kubernetes/resources/manifest.yaml @@ -18,12 +18,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes delete_context: requires_no_upstream: true diff --git a/pkg/templates/kubernetes/resources/namespace.yaml b/pkg/templates/kubernetes/resources/namespace.yaml index f28b10003..df7a06ae8 100644 --- a/pkg/templates/kubernetes/resources/namespace.yaml +++ b/pkg/templates/kubernetes/resources/namespace.yaml @@ -16,12 +16,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: diff --git a/pkg/templates/kubernetes/resources/persistent_volume.yaml b/pkg/templates/kubernetes/resources/persistent_volume.yaml index fb244da1a..14fe6252a 100644 --- a/pkg/templates/kubernetes/resources/persistent_volume.yaml +++ b/pkg/templates/kubernetes/resources/persistent_volume.yaml @@ -16,12 +16,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: @@ -48,10 +48,10 @@ properties: storageClassName: type: resource(kubernetes:storage_class) operational_rule: - steps: - - direction: upstream - resources: - - kubernetes:storage_class + step: + direction: upstream + resources: + - kubernetes:storage_class volumeMode: type: string diff --git a/pkg/templates/kubernetes/resources/persistent_volume_claim.yaml b/pkg/templates/kubernetes/resources/persistent_volume_claim.yaml index f8978dd3e..80b5bd884 100644 --- a/pkg/templates/kubernetes/resources/persistent_volume_claim.yaml +++ b/pkg/templates/kubernetes/resources/persistent_volume_claim.yaml @@ -16,12 +16,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: @@ -44,17 +44,17 @@ properties: volumeName: type: resource(kubernetes:persistent_volume) operational_rule: - steps: - - direction: upstream - resources: - - kubernetes:persistent_volume + step: + direction: upstream + resources: + - kubernetes:persistent_volume storageClassName: type: resource(kubernetes:storage_class) operational_rule: - steps: - - direction: upstream - resources: - - kubernetes:storage_class + step: + direction: upstream + resources: + - kubernetes:storage_class volumeMode: type: string diff --git a/pkg/templates/kubernetes/resources/pod.yaml b/pkg/templates/kubernetes/resources/pod.yaml index 13543b11e..436687684 100644 --- a/pkg/templates/kubernetes/resources/pod.yaml +++ b/pkg/templates/kubernetes/resources/pod.yaml @@ -16,12 +16,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: diff --git a/pkg/templates/kubernetes/resources/service.yaml b/pkg/templates/kubernetes/resources/service.yaml index f7f92c5e4..92e611e5a 100644 --- a/pkg/templates/kubernetes/resources/service.yaml +++ b/pkg/templates/kubernetes/resources/service.yaml @@ -15,12 +15,12 @@ properties: Cluster: type: resource operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: diff --git a/pkg/templates/kubernetes/resources/service_account.yaml b/pkg/templates/kubernetes/resources/service_account.yaml index 61f05b2a8..175b0b90a 100644 --- a/pkg/templates/kubernetes/resources/service_account.yaml +++ b/pkg/templates/kubernetes/resources/service_account.yaml @@ -16,12 +16,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: diff --git a/pkg/templates/kubernetes/resources/service_export.yaml b/pkg/templates/kubernetes/resources/service_export.yaml index 08f10f4f4..9abff12da 100644 --- a/pkg/templates/kubernetes/resources/service_export.yaml +++ b/pkg/templates/kubernetes/resources/service_export.yaml @@ -16,12 +16,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: diff --git a/pkg/templates/kubernetes/resources/storage_class.yaml b/pkg/templates/kubernetes/resources/storage_class.yaml index 8895e98cc..18f08be23 100644 --- a/pkg/templates/kubernetes/resources/storage_class.yaml +++ b/pkg/templates/kubernetes/resources/storage_class.yaml @@ -16,12 +16,12 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + step: + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: @@ -39,7 +39,7 @@ properties: mountOptions: type: list(string) allowVolumeExpansion: - type: boolean + type: bool reclaimPolicy: type: string volumeBindingMode: diff --git a/pkg/templates/kubernetes/resources/target_group_binding.yaml b/pkg/templates/kubernetes/resources/target_group_binding.yaml index 5475ee7c1..f20b1e07c 100644 --- a/pkg/templates/kubernetes/resources/target_group_binding.yaml +++ b/pkg/templates/kubernetes/resources/target_group_binding.yaml @@ -16,12 +16,11 @@ properties: type: resource namespace: true operational_rule: - steps: - - direction: downstream - resources: - - classifications: - - cluster - - kubernetes + direction: downstream + resources: + - classifications: + - cluster + - kubernetes Object: type: map properties: