Repository: klothoplatform/klotho
Pull request #889 (2024-01-17 23:45:04)

refactor ecs to support multiple containers
refactors the ecs model to allow for multiple containers being supported

ability to specify path in dynamic data to get parent/ancestor path relations (used to make a new image per container in ecs)

Note: Need to modify isOperationalSideEffect as a follow up to vix dataflow children in ecs-rds example

### Standard checks

- **Unit tests**: Any special considerations?
- **Docs**: Do we need to update any docs, internal or public?
- **Backwards compatibility**: Will this break existing apps? If so, what would be the extra work required to keep them working?



Patch:
diff --git a/pkg/engine2/operational_eval/evaluator.go b/pkg/engine2/operational_eval/evaluator.go
index 45d3a1a0..db2938d5 100644
--- a/pkg/engine2/operational_eval/evaluator.go
+++ b/pkg/engine2/operational_eval/evaluator.go
@@ -382,6 +382,7 @@ func (changes graphChanges) AddVertexAndDeps(eval *Evaluator, v Vertex) error {
 		return fmt.Errorf("could not get dependencies for %s: %w", v.Key(), err)
 	}
 	changes.Merge(depCaptureChanges)
+
 	return nil
 }
 
diff --git a/pkg/engine2/operational_eval/vertex_property.go b/pkg/engine2/operational_eval/vertex_property.go
index 7101182d..3f313900 100644
--- a/pkg/engine2/operational_eval/vertex_property.go
+++ b/pkg/engine2/operational_eval/vertex_property.go
@@ -18,8 +18,9 @@ type (
 	propertyVertex struct {
 		Ref construct.PropertyRef
 
-		Template  knowledgebase.Property
-		EdgeRules map[construct.SimpleEdge][]knowledgebase.OperationalRule
+		Template      knowledgebase.Property
+		EdgeRules     map[construct.SimpleEdge][]knowledgebase.OperationalRule
+		ResourceRules map[string][]knowledgebase.OperationalRule
 	}
 )
 
@@ -28,7 +29,18 @@ func (prop propertyVertex) Key() Key {
 }
 
 func (prop *propertyVertex) Dependencies(eval *Evaluator, propCtx dependencyCapturer) error {
-	resData := knowledgebase.DynamicValueData{Resource: prop.Ref.Resource}
+
+	res, err := eval.Solution.RawView().Vertex(prop.Ref.Resource)
+	if err != nil {
+		return fmt.Errorf("could not get resource for property vertex dependency calculation %s: %w", prop.Ref, err)
+	}
+	path, err := res.PropertyPath(prop.Ref.Property)
+
+	if err != nil {
+		return fmt.Errorf("could not get property path for %s: %w", prop.Ref, err)
+	}
+
+	resData := knowledgebase.DynamicValueData{Resource: prop.Ref.Resource, Path: path}
 
 	// Template can be nil when checking for dependencies from a propertyVertex when adding an edge template
 	if prop.Template != nil {
@@ -90,14 +102,20 @@ func (v *propertyVertex) Evaluate(eval *Evaluator) error {
 	if err != nil {
 		return fmt.Errorf("could not get resource to evaluate %s: %w", v.Ref, err)
 	}
+	path, err := res.PropertyPath(v.Ref.Property)
 
-	if err := v.evaluateConstraints(sol, res); err != nil {
+	if err != nil {
+		return fmt.Errorf("could not get property path for %s: %w", v.Ref, err)
+	}
+
+	dynData := knowledgebase.DynamicValueData{Resource: res.ID, Path: path}
+	if err := v.evaluateConstraints(sol, res, dynData); err != nil {
 		return err
 	}
 	opCtx := operational_rule.OperationalRuleContext{
 		Solution: sol,
 		Property: v.Template,
-		Data:     knowledgebase.DynamicValueData{Resource: res.ID},
+		Data:     dynData,
 	}
 
 	// we know we cannot change properties of imported resources only users through constraints
@@ -160,9 +178,11 @@ func (v *propertyVertex) Evaluate(eval *Evaluator) error {
 	return nil
 }
 
-func (v *propertyVertex) evaluateConstraints(sol solution_context.SolutionContext, res *construct.Resource) error {
-	dynData := knowledgebase.DynamicValueData{Resource: res.ID}
-
+func (v *propertyVertex) evaluateConstraints(
+	sol solution_context.SolutionContext,
+	res *construct.Resource,
+	dynData knowledgebase.DynamicValueData,
+) error {
 	var setConstraint constraints.ResourceConstraint
 	var addConstraints []constraints.ResourceConstraint
 	for _, c := range sol.Constraints().Resources {
@@ -316,7 +336,18 @@ func (v *propertyVertex) Ready(eval *Evaluator) (ReadyPriority, error) {
 		return NotReadyHigh, nil
 	}
 	// properties that have values set via edge rules dont' have default values
-	defaultVal, err := v.Template.GetDefaultValue(solution_context.DynamicCtx(eval.Solution), knowledgebase.DynamicValueData{Resource: v.Ref.Resource})
+	res, err := eval.Solution.RawView().Vertex(v.Ref.Resource)
+	if err != nil {
+		return NotReadyHigh, fmt.Errorf("could not get resource for property vertex dependency calculation %s: %w", v.Ref, err)
+	}
+	path, err := res.PropertyPath(v.Ref.Property)
+
+	if err != nil {
+		return NotReadyHigh, fmt.Errorf("could not get property path for %s: %w", v.Ref, err)
+	}
+
+	defaultVal, err := v.Template.GetDefaultValue(solution_context.DynamicCtx(eval.Solution),
+		knowledgebase.DynamicValueData{Resource: v.Ref.Resource, Path: path})
 	if err != nil {
 		return NotReadyMid, nil
 	}
@@ -371,6 +402,7 @@ func addConfigurationRuleToPropertyVertex(
 			))
 			continue
 		}
+
 		key := Key{Ref: ref}
 		vertex, err := eval.graph.Vertex(key)
 		if err != nil {
@@ -422,6 +454,7 @@ func addConfigurationRuleToPropertyVertex(
 				fmt.Errorf("existing vertex for %s is not able to add configuration rules to property vertex", ref),
 			)
 		}
+
 	}
 	return configuration, errs
 }
diff --git a/pkg/engine2/operational_rule/operational_action.go b/pkg/engine2/operational_rule/operational_action.go
index 1913b26f..5b68031c 100644
--- a/pkg/engine2/operational_rule/operational_action.go
+++ b/pkg/engine2/operational_rule/operational_action.go
@@ -97,6 +97,7 @@ func (action *operationalResourceAction) createUniqueResources(resource *constru
 					return err
 				}
 				if action.numNeeded > 0 {
+
 					err := action.ruleCtx.addDependencyForDirection(action.Step, resource, res)
 					if err != nil {
 						return err
@@ -319,7 +320,7 @@ func (action *operationalResourceAction) getPriorityResourceType() (
 			if err != nil && !errors.Is(err, graph.ErrVertexNotFound) {
 				return construct.ResourceId{}, resourceSelector, err
 			}
-			if id.IsZero() || res != nil {
+			if id.IsZero() || (res != nil && !action.Step.Unique) {
 				continue
 			}
 			return construct.ResourceId{Provider: id.Provider, Type: id.Type, Namespace: id.Namespace, Name: id.Name}, resourceSelector, nil
diff --git a/pkg/infra/iac3/templates/aws/ecs_task_definition/factory.ts b/pkg/infra/iac3/templates/aws/ecs_task_definition/factory.ts
index c5818644..f0aedfbd 100644
--- a/pkg/infra/iac3/templates/aws/ecs_task_definition/factory.ts
+++ b/pkg/infra/iac3/templates/aws/ecs_task_definition/factory.ts
@@ -5,20 +5,13 @@ import * as awsInputs from '@pulumi/aws/types/input'
 import { TemplateWrapper } from '../../wrappers'
 
 interface Args {
-    LogGroup: aws.cloudwatch.LogGroup
-    Region: pulumi.Output<pulumi.UnwrappedObject<aws.GetRegionResult>>
     Name: string
-    Cpu?: string
-    Memory?: string
     NetworkMode?: string
     ExecutionRole: aws.iam.Role
     TaskRole: aws.iam.Role
-    EnvironmentVariables: TemplateWrapper<Record<string, pulumi.Output<string>>>
-    Image: docker.Image
-    PortMappings?: Record<string, object>
     RequiresCompatibilities?: string[]
     EfsVolumes: TemplateWrapper<awsInputs.ecs.TaskDefinitionVolumeEfsVolumeConfiguration[]>
-    MountPoints: Record<string, string>
+    ContainerDefinitions: TemplateWrapper<awsInputs.ecs.TaskDefinitionContainerDefinitions[]>
 }
 
 // noinspection JSUnusedLocalSymbols
@@ -44,26 +37,6 @@ function create(args: Args): aws.ecs.TaskDefinition {
         //TMPL {{- if .EfsVolumes }}
         volumes: args.EfsVolumes,
         //TMPL {{- end }}
-        containerDefinitions: pulumi.jsonStringify([
-            {
-                name: args.Name,
-                image: args.Image.imageName,
-                portMappings: args.PortMappings,
-                //TMPL {{- if .EnvironmentVariables }}
-                environment: args.EnvironmentVariables,
-                //TMPL {{- end }}
-                //TMPL {{- if .MountPoints }}
-                mountPoints: args.MountPoints,
-                //TMPL {{- end }}
-                logConfiguration: {
-                    logDriver: 'awslogs',
-                    options: {
-                        'awslogs-group': args.LogGroup.name,
-                        'awslogs-region': args.Region.name,
-                        'awslogs-stream-prefix': args.Name,
-                    },
-                },
-            },
-        ]),
+        containerDefinitions: pulumi.jsonStringify(args.ContainerDefinitions),
     })
 }
diff --git a/pkg/infra/iac3/templates/aws/rds_instance/factory.ts b/pkg/infra/iac3/templates/aws/rds_instance/factory.ts
index 2a132301..ca1aea19 100644
--- a/pkg/infra/iac3/templates/aws/rds_instance/factory.ts
+++ b/pkg/infra/iac3/templates/aws/rds_instance/factory.ts
@@ -49,6 +49,7 @@ function properties(object: aws.rds.Instance, args: Args) {
         }),
         RdsConnectionArn: pulumi.interpolate`arn:aws:rds-db:${region.name}:${accountId.accountId}:dbuser:${object.resourceId}/${object.username}`,
         Endpoint: object.endpoint,
+        ConnectionString: pulumi.interpolate`${args.Engine}://${object.username}:${object.password}@${object.endpoint.address}:${object.endpoint.port}/${args.DatabaseName}`,
     }
 }
 
diff --git a/pkg/knowledge_base2/dynamic_value.go b/pkg/knowledge_base2/dynamic_value.go
index e088da60..f124e411 100644
--- a/pkg/knowledge_base2/dynamic_value.go
+++ b/pkg/knowledge_base2/dynamic_value.go
@@ -38,6 +38,7 @@ type (
 	DynamicValueData struct {
 		Resource construct.ResourceId
 		Edge     *construct.Edge
+		Path     construct.PropertyPath
 	}
 )
 
@@ -64,6 +65,7 @@ func (ctx DynamicValueContext) TemplateFunctions() template.FuncMap {
 		"fieldValue":        ctx.FieldValue,
 		"hasField":          ctx.HasField,
 		"fieldRef":          ctx.FieldRef,
+		"pathAncestor":      ctx.PathAncestor,
 
 		"toJson": ctx.toJson,
 
@@ -560,6 +562,19 @@ func (ctx DynamicValueContext) toJson(value any) (string, error) {
 	return string(j), nil
 }
 
+func (ctx DynamicValueContext) PathAncestor(path construct.PropertyPath, depth int) (string, error) {
+	if depth < 0 {
+		return "", fmt.Errorf("depth must be >= 0")
+	}
+	if depth == 0 {
+		return path.String(), nil
+	}
+	if len(path) <= depth {
+		return "", fmt.Errorf("depth %d is greater than path length %d", depth, len(path))
+	}
+	return path[:len(path)-depth].String(), nil
+}
+
 // filterMatch returns a json array by filtering the values array with the regex pattern
 func filterMatch(pattern string, values []string) ([]string, error) {
 	re, err := regexp.Compile(pattern)
diff --git a/pkg/knowledge_base2/properties/map_property.go b/pkg/knowledge_base2/properties/map_property.go
index 86998d61..7a3fe8a9 100644
--- a/pkg/knowledge_base2/properties/map_property.go
+++ b/pkg/knowledge_base2/properties/map_property.go
@@ -114,6 +114,9 @@ func (m *MapProperty) Parse(value any, ctx knowledgebase.DynamicContext, data kn
 				result[key] = val
 			}
 		}
+	}
+
+	if m.KeyProperty == nil || m.ValueProperty == nil {
 		return result, nil
 	}
 
diff --git a/pkg/knowledge_base2/reader/resource_template.go b/pkg/knowledge_base2/reader/resource_template.go
index ed8b3fb5..f8765ee6 100644
--- a/pkg/knowledge_base2/reader/resource_template.go
+++ b/pkg/knowledge_base2/reader/resource_template.go
@@ -15,6 +15,8 @@ type (
 
 		PathSatisfaction knowledgebase.PathSatisfaction `json:"path_satisfaction" yaml:"path_satisfaction"`
 
+		AdditionalRules []knowledgebase.AdditionalRule `json:"additional_rules" yaml:"additional_rules"`
+
 		Consumption knowledgebase.Consumption `json:"consumption" yaml:"consumption"`
 
 		// DeleteContext defines the context in which a resource can be deleted
@@ -44,6 +46,7 @@ func (r *ResourceTemplate) Convert() (*knowledgebase.ResourceTemplate, error) {
 		QualifiedTypeName: r.QualifiedTypeName,
 		DisplayName:       r.DisplayName,
 		Properties:        kbProperties,
+		AdditionalRules:   r.AdditionalRules,
 		Classification:    r.Classification,
 		PathSatisfaction:  r.PathSatisfaction,
 		Consumption:       r.Consumption,
diff --git a/pkg/templates/aws/edges/ecs_task_definition-log_group.yaml b/pkg/templates/aws/edges/ecs_task_definition-log_group.yaml
index 72a1b12d..c0937619 100644
--- a/pkg/templates/aws/edges/ecs_task_definition-log_group.yaml
+++ b/pkg/templates/aws/edges/ecs_task_definition-log_group.yaml
@@ -1,9 +1,7 @@
 source: aws:ecs_task_definition
 target: aws:log_group
 operational_rules:
-  - if: |
-      {{ eq .Target (fieldValue "LogGroup" .Source)}}
-    configuration_rules:
+  - configuration_rules:
       - resource: '{{ .Target }}'
         configuration:
           field: LogGroupName
diff --git a/pkg/templates/aws/edges/target_group-ecs_service.yaml b/pkg/templates/aws/edges/target_group-ecs_service.yaml
index 9a2769e8..f447b210 100644
--- a/pkg/templates/aws/edges/target_group-ecs_service.yaml
+++ b/pkg/templates/aws/edges/target_group-ecs_service.yaml
@@ -9,7 +9,7 @@ operational_rules:
           value:
             - TargetGroup: '{{ .Source }}'
               ContainerName: '{{ (downstream "aws:ecs_task_definition" .Target).Name }}'
-              ContainerPort: '{{ fieldValue "PortMappings[0].ContainerPort" (fieldValue "TaskDefinition" .Target)}}'
+              ContainerPort: '{{ fieldValue "ContainerDefinitions[0].PortMappings[0].ContainerPort" (fieldValue "TaskDefinition" .Target)}}'
       - resource: '{{ .Source }}'
         configuration:
           field: TargetType
diff --git a/pkg/templates/aws/resources/ecs_service.yaml b/pkg/templates/aws/resources/ecs_service.yaml
index f19e4dde..a86939da 100644
--- a/pkg/templates/aws/resources/ecs_service.yaml
+++ b/pkg/templates/aws/resources/ecs_service.yaml
@@ -79,8 +79,18 @@ properties:
 consumption:
   consumed:
     - model: EnvironmentVariables
-      property_path: EnvironmentVariables
+      property_path: ContainerDefinitions[0].Environment
       resource: '{{ fieldValue "TaskDefinition" .Self }}'
+      converter: |
+        [
+        {{ $i := 0}}
+        {{ range $key, $value := . }}
+          {
+            "name": "{{ $key }}",
+            "value": "{{ $value }}"
+          }{{if ne $i (sub (len $) 1)}},{{end}}{{ $i = add $i 1 }}
+        {{ end }}
+        ]
   emitted:
     - model: EnvironmentVariables
       value:
diff --git a/pkg/templates/aws/resources/ecs_task_definition.yaml b/pkg/templates/aws/resources/ecs_task_definition.yaml
index c85da016..42b0fbf7 100644
--- a/pkg/templates/aws/resources/ecs_task_definition.yaml
+++ b/pkg/templates/aws/resources/ecs_task_definition.yaml
@@ -2,58 +2,139 @@ qualified_type_name: aws:ecs_task_definition
 display_name: ECS Task Definition
 
 properties:
-  Image:
-    type: resource(aws:ecr_image)
-    operational_rule:
-      step:
-        direction: downstream
-        resources:
-          - aws:ecr_image:{{ .Self.Name }}-image
-        unique: true
-    description: Reference to an Amazon Elastic Container Registry (ECR) image that
-      will be used for the container within the task
-  MountPoints:
-    type: set
+  ContainerDefinitions:
+    default_value:
+      - Name: '{{ .Self.Name }}'
+    type: list
     properties:
-      ContainerPath:
+      Command:
+        type: list(string)
+        description: The command that is passed to the container
+      Cpu:
         type: string
-        description: The path on the container to mount the volume at
-      SourceVolume:
+        default_value: '256'
+        description: The number of CPU units reserved for the container
+      DependsOn:
+        type: list
+        properties:
+          Condition:
+            type: string
+            description: The dependency condition of the container
+          ContainerName:
+            type: string
+            description: The name of the container to depend on
+        description: The dependencies defined for container startup and shutdown
+      EntryPoint:
+        type: list(string)
+        description: The entry point that is passed to the container
+      Environment:
+        type: list
+        properties:
+          Name:
+            type: string
+            sanitize: |
+              {{ .
+                | replace `[^[:alnum:]_]+` "_"
+                | replace `^[^a-zA-Z]+` ""
+                | upper
+              }}
+            description: The name of the environment variable
+          Value:
+            type: string
+            description: The value of the environment variable
+        description: The environment variables to pass to a container
+      Image:
+        type: resource(aws:ecr_image)
+        operational_rule:
+          step:
+            direction: downstream
+            resources:
+              - aws:ecr_image:{{ .Self.Name }}-{{ fieldValue (printf "%s.Name" (pathAncestor .Path 1)) .Self}}
+            unique: true
+        description: Reference to an Amazon Elastic Container Registry (ECR) image that
+          will be used for the container within the task
+      LogConfiguration:
+        type: map
+        properties:
+          LogDriver:
+            type: string
+            description: The log driver to use for the container
+            default_value: awslogs
+          Options:
+            type: map(string,string)
+            description: The configuration options to send to the log driver
+            properties:
+              awslogs-group:
+                type: string
+                description: The log group to send stdout to
+                operational_rule:
+                  step:
+                    direction: downstream
+                    resources:
+                      - aws:log_group:{{ .Self.Name }}-log-group
+                    unique: true
+                    use_property_ref: LogGroupName
+              awslogs-region:
+                type: string
+                description: The region which your log group will exist in
+                default_value: '{{ fieldRef "RegionName" (downstream "aws:region" (upstream "aws:ecs_service" .Self)) }}'
+              awslogs-stream-prefix:
+                type: string
+                description: The prefix to use when creating the log stream
+                default_value: '{{ .Self.Name }}-{{ fieldValue (printf "%s.Name" (pathAncestor .Path 3)) .Self }}'
+          SecretOptions:
+            type: list
+            properties:
+              Name:
+                type: string
+                description: The name of the secret
+              ValueFrom:
+                type: string
+                description: The secret to expose to the container
+            description: The secrets to pass to the log configuration
+        description: The log configuration specification for the container
+      Memory:
         type: string
-        description: The name of the volume to mount
-      ReadOnly:
-        type: bool
-        description: Determines if the volume should be mounted as read-only
-    description: A list of mount points for data volumes in your container
-    important: true
-  EnvironmentVariables:
-    type: map(string,string)
-    description: A map of the environment variables to pass to the container
-    important: true
-    key_property:
-      sanitize: |
-        {{ .
-          | replace `[^[:alnum:]_]+` "_"
-          | replace `^[^a-zA-Z]+` ""
-          | upper
-        }}
-  Cpu:
-    type: string
-    default_value: '256'
-    description: The amount of CPU to allocate for the task
-  Memory:
-    type: string
-    default_value: '512'
-    description: The amount of memory (in MiB) used by the task
-  LogGroup:
-    type: resource(aws:log_group)
-    operational_rule:
-      step:
-        direction: downstream
-        resources:
-          - aws:log_group:{{ .Self.Name }}-log-group
-        unique: true
-    description: Specifies the log group for the ECS task's logs
+        default_value: '512'
+        description: The hard limit (in MiB) of memory to present to the container
+      MountPoints:
+        type: set
+        properties:
+          ContainerPath:
+            type: string
+            description: The path on the container to mount the volume at
+          SourceVolume:
+            type: string
+            description: The name of the volume to mount
+          ReadOnly:
+            type: bool
+            description: Determines if the volume should be mounted as read-only
+        description: A list of mount points for data volumes in your container
+      Name:
+        type: string
+        description: The name of the container
+      PortMappings:
+        type: list
+        important: true
+        default_value:
+          - ContainerPort: 80
+            HostPort: 80
+            Protocol: TCP
+        properties:
+          ContainerPort:
+            type: int
+            description: The port number on the container
+          HostPort:
+            type: int
+            description: The port number on the host where the container is exposed
+          Protocol:
+            type: string
+            description: The protocol used for the port mapping, such as TCP or UDP
+        description: A set of port mappings between the container and host
+
+      
+      
+      
   ExecutionRole:
     type: resource(aws:iam_role)
     operational_rule:
@@ -69,35 +150,10 @@ properties:
     default_value: '{{ fieldValue "ExecutionRole" .Self }}'
     description: The IAM role that determines the permissions for making AWS API calls
       from within the ECS task
-  Region:
-    type: resource(aws:region)
-    operational_rule:
-      step:
-        direction: downstream
-        resources:
-          - aws:region
-    description: The AWS region where the ECS task is deployed
   NetworkMode:
     type: string
     default_value: awsvpc
     description: The Docker networking mode to use for the containers in the task
-  PortMappings:
-    type: list
-    default_value:
-      - ContainerPort: 80
-        HostPort: 80
-        Protocol: TCP
-    properties:
-      ContainerPort:
-        type: int
-        description: The port number on the container
-      HostPort:
-        type: int
-        description: The port number on the host where the container is exposed
-      Protocol:
-        type: string
-        description: The protocol used for the port mapping, such as TCP or UDP
-    description: A set of port mappings between the container and host
   RequiresCompatibilities:
     type: list(string)
     default_value:
@@ -137,10 +193,21 @@ properties:
         description: The authorization configuration details for the EFS volume
     description: An array of Amazon Elastic File System (EFS) volumes to be attached
       to containers
+  
 consumption:
   consumed:
     - model: EnvironmentVariables
-      property_path: EnvironmentVariables
+      property_path: ContainerDefinitions[0].Environment
+      converter: |
+        [
+        {{ $i := 0}}
+        {{ range $key, $value := . }}
+          {
+            "name": "{{ $key }}",
+            "value": "{{ $value }}"
+          }{{if ne $i (sub (len $) 1)}},{{end}}{{ $i = add $i 1 }}
+        {{ end }}
+        ]
 
 delete_context:
   requires_no_upstream: true
diff --git a/pkg/templates/aws/resources/rds_instance.yaml b/pkg/templates/aws/resources/rds_instance.yaml
index 0617f570..65f49b8d 100644
--- a/pkg/templates/aws/resources/rds_instance.yaml
+++ b/pkg/templates/aws/resources/rds_instance.yaml
@@ -55,6 +55,26 @@ properties:
   Engine:
     type: string
     default_value: postgres
+    allowed_values:
+      - postgres
+      - mysql
+      - aurora-postgresql
+      - aurora-mysql
+      - aurora-postgresql
+      - mariadb
+      - oracle-ee
+      - oracle-ee-cdb
+      - oracle-se2
+      - oracle-se2-cdb
+      - custom-oracle-ee
+      - custom-oracle-ee-cdb
+      - custom-sqlserver-ee
+      - custom-sqlserver-se
+      - custom-sqlserver-web
+      - sqlserver-ee
+      - sqlserver-ex
+      - sqlserver-se
+      - sqlserver-web
   EngineVersion:
     type: string
     default_value: '13.7'
@@ -75,6 +95,10 @@ properties:
     type: string
     configuration_disabled: true
     deploy_time: true
+  ConnectionString:
+    type: string
+    configuration_disabled: true
+    deploy_time: true
   Endpoint:
     type: string
     configuration_disabled: true
diff --git a/pkg/templates/aws/resources/region.yaml b/pkg/templates/aws/resources/region.yaml
index 67cbb0df..1866b941 100644
--- a/pkg/templates/aws/resources/region.yaml
+++ b/pkg/templates/aws/resources/region.yaml
@@ -1,5 +1,12 @@
 qualified_type_name: aws:region
 display_name: Region
 
+properties:
+  RegionName:
+    description: The name of the region
+    type: string
+    deploy_time: true
+    configuration_disabled: true
+
 delete_context:
   requires_no_upstream: true
diff --git a/pkg/templates/kubernetes/resources/helm_chart.yaml b/pkg/templates/kubernetes/resources/helm_chart.yaml
index f1fdfc15..0f003633 100644
--- a/pkg/templates/kubernetes/resources/helm_chart.yaml
+++ b/pkg/templates/kubernetes/resources/helm_chart.yaml
@@ -37,6 +37,12 @@ properties:
   Internal:
     type: bool
 
+classification:
+  is:
+    - kubernetes
+    - helm
+    - chart
+
 delete_context:
   requires_no_upstream: true
 views:


Test patch:
diff --git a/pkg/engine2/operational_eval/vertex_property_test.go b/pkg/engine2/operational_eval/vertex_property_test.go
index 19357ede..e284c01e 100644
--- a/pkg/engine2/operational_eval/vertex_property_test.go
+++ b/pkg/engine2/operational_eval/vertex_property_test.go
@@ -1,16 +1,37 @@
 package operational_eval
 
 import (
+	"fmt"
+	reflect "reflect"
 	"testing"
 
 	"github.com/dominikbraun/graph"
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"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"
 	gomock "go.uber.org/mock/gomock"
 )
 
+type dynDataMatcher struct {
+	data knowledgebase.DynamicValueData
+}
+
+func (d dynDataMatcher) Matches(x interface{}) bool {
+	dynData, ok := x.(knowledgebase.DynamicValueData)
+	if !ok {
+		return false
+	}
+
+	return dynData.Resource.Matches(d.data.Resource) && dynData.Path.String() == d.data.Path.String() && reflect.DeepEqual(dynData.Edge, d.data.Edge)
+}
+
+func (d dynDataMatcher) String() string {
+	return fmt.Sprintf("is equal to %v", d.data)
+}
+
 func Test_propertyVertex_evaluateResourceOperational(t *testing.T) {
 	rule := &knowledgebase.PropertyRule{
 		Value: "test",
@@ -115,10 +136,11 @@ func Test_propertyVertex_evaluateEdgeOperational(t *testing.T) {
 }
 
 func Test_propertyVertex_Dependencies(t *testing.T) {
+
 	tests := []struct {
 		name    string
 		v       *propertyVertex
-		mocks   func(dcap *MockdependencyCapturer)
+		mocks   func(dcap *MockdependencyCapturer, resource *construct.Resource, path construct.PropertyPath)
 		wantErr bool
 	}{
 		{
@@ -136,12 +158,17 @@ func Test_propertyVertex_Dependencies(t *testing.T) {
 					},
 				},
 			},
-			mocks: func(dcap *MockdependencyCapturer) {
-				dcap.EXPECT().ExecutePropertyRule(knowledgebase.DynamicValueData{
-					Resource: construct.ResourceId{Name: "test"},
-				}, knowledgebase.PropertyRule{
-					If: "test",
-				}).Return(nil)
+			mocks: func(dcap *MockdependencyCapturer, resource *construct.Resource, path construct.PropertyPath) {
+				dcap.EXPECT().ExecutePropertyRule(dynDataMatcher{
+					data: knowledgebase.DynamicValueData{
+						Resource: resource.ID,
+						Path:     path,
+					},
+				},
+					knowledgebase.PropertyRule{
+						If: "test",
+					},
+				).Return(nil)
 			},
 		},
 		{
@@ -162,9 +189,9 @@ func Test_propertyVertex_Dependencies(t *testing.T) {
 					},
 				},
 			},
-			mocks: func(dcap *MockdependencyCapturer) {
+			mocks: func(dcap *MockdependencyCapturer, resource *construct.Resource, path construct.PropertyPath) {
 				dcap.EXPECT().ExecuteOpRule(knowledgebase.DynamicValueData{
-					Resource: construct.ResourceId{Name: "test"},
+					Resource: resource.ID,
 					Edge:     &graph.Edge[construct.ResourceId]{Source: construct.ResourceId{Name: "test"}, Target: construct.ResourceId{Name: "test2"}},
 				}, knowledgebase.OperationalRule{
 					If: "testE",
@@ -176,9 +203,24 @@ func Test_propertyVertex_Dependencies(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			ctrl := gomock.NewController(t)
 			dcap := NewMockdependencyCapturer(ctrl)
-			tt.mocks(dcap)
-			eval := &Evaluator{}
-			err := tt.v.Dependencies(eval, dcap)
+			resource := &construct.Resource{ID: construct.ResourceId{Name: "test"}, Properties: construct.Properties{
+				"test": "test",
+			}}
+			path, err := resource.PropertyPath("test")
+			if !assert.NoError(t, err) {
+				return
+			}
+			tt.mocks(dcap, resource, path)
+			testSol := enginetesting.NewTestSolution()
+			testSol.KB.On("GetResourceTemplate", mock.Anything).Return(&knowledgebase.ResourceTemplate{}, nil)
+			err = testSol.RawView().AddVertex(resource)
+			if !assert.NoError(t, err) {
+				return
+			}
+			eval := &Evaluator{
+				Solution: testSol,
+			}
+			err = tt.v.Dependencies(eval, dcap)
 			if tt.wantErr {
 				assert.Error(t, err)
 				return
diff --git a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
index 9f728f2c..902067bc 100755
--- a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
@@ -23,11 +23,8 @@ resources:
 
   ecs_service/ecs_service_0:
     children:
-        - aws:ecr_image:ecs_service_0-image
-        - aws:ecr_repo:ecr_repo-0
         - aws:ecs_task_definition:ecs_service_0
         - aws:iam_role:ecs_service_0-execution-role
-        - aws:log_group:ecs_service_0-log-group
     parent: vpc/vpc-0
     tag: big
 
diff --git a/pkg/engine2/testdata/ecs_rds.expect.yaml b/pkg/engine2/testdata/ecs_rds.expect.yaml
index 674b9038..798155f5 100755
--- a/pkg/engine2/testdata/ecs_rds.expect.yaml
+++ b/pkg/engine2/testdata/ecs_rds.expect.yaml
@@ -28,28 +28,34 @@ resources:
         TaskDefinition: aws:ecs_task_definition:ecs_service_0
     aws:ecs_cluster:ecs_cluster-0:
     aws:ecs_task_definition:ecs_service_0:
-        Cpu: "256"
-        EnvironmentVariables:
-            RDS_INSTANCE_2_RDS_CONNECTION_ARN: aws:rds_instance:rds-instance-2#RdsConnectionArn
-            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
+        ContainerDefinitions:
+            - Cpu: "256"
+              Environment:
+                - {}
+                - {}
+                - {}
+                - {}
+              Image: aws:ecr_image:ecs_service_0-ecs_service_0
+              LogConfiguration:
+                LogDriver: awslogs
+                Options:
+                    awslogs-group: aws:log_group:ecs_service_0-log-group#LogGroupName
+                    awslogs-region: aws:region:region-0#RegionName
+                    awslogs-stream-prefix: ecs_service_0-ecs_service_0
+              Memory: "512"
+              Name: ecs_service_0
+              PortMappings:
+                - ContainerPort: 80
+                  HostPort: 80
+                  Protocol: TCP
         ExecutionRole: aws:iam_role:ecs_service_0-execution-role
-        Image: aws:ecr_image:ecs_service_0-image
-        LogGroup: aws:log_group:ecs_service_0-log-group
-        Memory: "512"
         NetworkMode: awsvpc
-        PortMappings:
-            - ContainerPort: 80
-              HostPort: 80
-              Protocol: TCP
-        Region: aws:region:region-0
         RequiresCompatibilities:
             - FARGATE
         TaskRole: aws:iam_role:ecs_service_0-execution-role
-    aws:ecr_image:ecs_service_0-image:
+    aws:ecr_image:ecs_service_0-ecs_service_0:
         Context: .
-        Dockerfile: ecs_service_0-image.Dockerfile
+        Dockerfile: ecs_service_0-ecs_service_0.Dockerfile
         Repo: aws:ecr_repo:ecr_repo-0
     aws:iam_role:ecs_service_0-execution-role:
         AssumeRolePolicyDoc:
@@ -169,6 +175,11 @@ 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
@@ -181,11 +192,6 @@ 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:vpc-0:subnet-0-route_table:
         Routes:
@@ -208,11 +214,10 @@ edges:
     aws:ecs_service:ecs_service_0 -> aws:ecs_task_definition:ecs_service_0:
     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:ecr_image:ecs_service_0-ecs_service_0:
     aws:ecs_task_definition:ecs_service_0 -> aws:iam_role:ecs_service_0-execution-role:
     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:ecr_image:ecs_service_0-ecs_service_0 -> aws:ecr_repo:ecr_repo-0:
     aws:iam_role:ecs_service_0-execution-role -> aws:rds_instance:rds-instance-2:
     aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway -> aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip:
     aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway -> aws:subnet:vpc-0:subnet-2:
diff --git a/pkg/engine2/testdata/ecs_rds.iac-viz.yaml b/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
index 1f9e8b1c..77740931 100755
--- a/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
+++ b/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
@@ -56,9 +56,9 @@ resources:
 
   aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway -> elastic_ip/subnet-0-route_table-nat_gateway-elastic_ip:
   aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway -> aws:subnet:vpc-0/subnet-2:
-  ecr_image/ecs_service_0-image:
+  ecr_image/ecs_service_0-ecs_service_0:
 
-  ecr_image/ecs_service_0-image -> ecr_repo/ecr_repo-0:
+  ecr_image/ecs_service_0-ecs_service_0 -> ecr_repo/ecr_repo-0:
   iam_role/ecs_service_0-execution-role:
 
   iam_role/ecs_service_0-execution-role -> rds_instance/rds-instance-2:
@@ -84,10 +84,9 @@ 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 -> ecr_image/ecs_service_0-ecs_service_0:
   ecs_task_definition/ecs_service_0 -> iam_role/ecs_service_0-execution-role:
   ecs_task_definition/ecs_service_0 -> log_group/ecs_service_0-log-group:
-  ecs_task_definition/ecs_service_0 -> rds_instance/rds-instance-2:
   ecs_task_definition/ecs_service_0 -> region/region-0:
   aws:security_group:vpc-0/ecs_service_0-security_group: