Repository: klothoplatform/klotho
Pull request #857 (2024-01-10 19:29:35)

Adds ALB Support to the Engine
This change's primary goal was adding support for specifying ALB Listener rules.

It includes a few other changes:
- Improved support for ACM Certificates
- Improved valid edge detection
- Added support for more template property validations


Patch:
diff --git a/go.mod b/go.mod
index e31344ef..85e77178 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.21
 
 require (
 	github.com/Masterminds/sprig/v3 v3.2.3
+	github.com/alitto/pond v1.8.3
 	github.com/aws/aws-cloud-map-mcs-controller-for-k8s v0.3.1
 	github.com/beevik/etree v1.1.0
 	github.com/bmatcuk/doublestar/v4 v4.2.0
@@ -42,7 +43,6 @@ require (
 )
 
 require (
-	github.com/alitto/pond v1.8.3 // indirect
 	github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 // indirect
 	github.com/gomodule/redigo v2.0.0+incompatible // indirect
 	github.com/kr/pretty v0.3.0 // indirect
diff --git a/pkg/engine2/cli.go b/pkg/engine2/cli.go
index 9fc9105d..c74f9bd9 100644
--- a/pkg/engine2/cli.go
+++ b/pkg/engine2/cli.go
@@ -170,7 +170,7 @@ type resourceInfo struct {
 	Views           map[string]string `json:"views"`
 }
 
-var validationFields = []string{"MinLength", "MaxLength", "MinValue", "MaxValue", "AllowedValues"}
+var validationFields = []string{"MinLength", "MaxLength", "MinValue", "MaxValue", "AllowedValues", "UniqueItems", "UniqueKeys", "MinSize", "MaxSize"}
 
 func addSubProperties(properties map[string]any, subProperties map[string]knowledgebase.Property) {
 	for _, subProperty := range subProperties {
diff --git a/pkg/engine2/edge_targets.go b/pkg/engine2/edge_targets.go
index 68886a94..f9fcd9d2 100644
--- a/pkg/engine2/edge_targets.go
+++ b/pkg/engine2/edge_targets.go
@@ -1,6 +1,7 @@
 package engine2
 
 import (
+	"github.com/klothoplatform/klotho/pkg/engine2/solution_context"
 	"os"
 	"slices"
 	"sync"
@@ -10,7 +11,6 @@ import (
 	"github.com/dominikbraun/graph"
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
 	"github.com/klothoplatform/klotho/pkg/engine2/path_selection"
-	"github.com/klothoplatform/klotho/pkg/set"
 	"go.uber.org/zap"
 	"gopkg.in/yaml.v3"
 )
@@ -38,70 +38,92 @@ type (
 //
 // This is used to determine (on a best-effort basis) if an edge can be expanded
 // without fully solving the graph (which is expensive).
-func (e *Engine) EdgeCanBeExpanded(source construct.ResourceId, target construct.ResourceId) (bool, error) {
+func (e *Engine) EdgeCanBeExpanded(ctx *solutionContext, source construct.ResourceId, target construct.ResourceId) (result bool, cacheable bool, err error) {
+	cacheable = true
 
 	if source.Matches(target) {
-		return false, nil
+		return false, cacheable, nil
 	}
 
-	edge := construct.SimpleEdge{
-		Source: source,
-		Target: target,
-	}
-
-	sourceTemplate, err := e.Kb.GetResourceTemplate(edge.Source)
-	if err != nil {
-		return false, err
-	}
-	targetTemplate, err := e.Kb.GetResourceTemplate(edge.Target)
+	satisfactions, err := e.Kb.GetPathSatisfactionsFromEdge(source, target)
 	if err != nil {
-		return false, err
+		return false, cacheable, err
 	}
-	classifications := make(set.Set[string])
-	sourceClassifications := sourceTemplate.PathSatisfaction.AsSource
-	if len(sourceClassifications) == 0 {
-		return false, nil
-	}
-	for _, satisfaction := range sourceClassifications {
-		if satisfaction.Classification == "" {
-			continue
+	sourceSatisfactionCount := 0
+	targetSatisfactionCount := 0
+	for _, satisfaction := range satisfactions {
+		if satisfaction.Source.Classification != "" {
+			sourceSatisfactionCount++
+		}
+		if satisfaction.Target.Classification != "" {
+			targetSatisfactionCount++
 		}
-		classifications.Add(satisfaction.Classification)
 	}
-	targetClassifications := targetTemplate.PathSatisfaction.AsTarget
-	if len(targetClassifications) == 0 {
-		return false, nil
+	if sourceSatisfactionCount == 0 || targetSatisfactionCount == 0 {
+		return false, cacheable, nil
 	}
-	for _, satisfaction := range targetClassifications {
-		if satisfaction.Classification == "" {
+
+	for _, satisfaction := range satisfactions {
+		classification := satisfaction.Classification
+		if classification == "" {
 			continue
 		}
-		classifications.Add(satisfaction.Classification)
-	}
+		var sourceReferencedResources []construct.ResourceId
+		var targetReferencedResources []construct.ResourceId
+
+		if satisfaction.Source.PropertyReference != "" {
+			cacheable = false
+			sourceReferencedResources, err = solution_context.GetResourcesFromPropertyReference(ctx, source, satisfaction.Source.PropertyReference)
+			if len(sourceReferencedResources) == 0 || err != nil {
+				continue // ignore satisfaction if we can't resolve the property reference
+			}
+		}
+		if satisfaction.Target.PropertyReference != "" {
+			cacheable = false
+			targetReferencedResources, err = solution_context.GetResourcesFromPropertyReference(ctx, target, satisfaction.Target.PropertyReference)
+			if len(targetReferencedResources) == 0 || err != nil {
+				continue // ignore satisfaction if we can't resolve the property reference
+			}
+		}
 
-	for classification := range classifications {
-		zap.S().Debugf("Checking for valid path between %s and %s for classification %s", source, target, classification)
-		pathSelectionGraph, err := path_selection.BuildPathSelectionGraph(edge, e.Kb, classification)
-		if err != nil {
-			return false, err
+		tempSource := source
+		if len(sourceReferencedResources) > 0 {
+			tempSource = sourceReferencedResources[len(sourceReferencedResources)-1]
 		}
-		paths, err := graph.AllPathsBetween(pathSelectionGraph, edge.Source, edge.Target)
-		if len(paths) == 0 || err != nil {
-			return false, err
+		tempTarget := target
+		if len(targetReferencedResources) > 0 {
+			tempTarget = targetReferencedResources[len(targetReferencedResources)-1]
 		}
 
-		hasValidPath := false
-		for _, path := range paths {
-			if path_selection.PathSatisfiesClassification(e.Kb, path, classification) {
-				hasValidPath = true
-				break
-			}
+		tempGraph, err := path_selection.BuildPathSelectionGraph(
+			construct.SimpleEdge{
+				Source: tempSource,
+				Target: tempTarget,
+			}, ctx.KnowledgeBase(), classification)
+
+		tempSourceResource, err := tempGraph.Vertex(tempSource)
+		if err != nil {
+			continue
 		}
-		if !hasValidPath {
-			return false, nil
+		tempTargetResource, err := tempGraph.Vertex(tempTarget)
+		if err != nil {
+			continue
+		}
+
+		_, err = path_selection.ExpandEdge(ctx, path_selection.ExpansionInput{
+			Dep: construct.ResourceEdge{
+				Source: tempSourceResource,
+				Target: tempTargetResource,
+			},
+			Classification: classification,
+			TempGraph:      tempGraph,
+		})
+		if err != nil {
+			return false, cacheable, err
 		}
 	}
-	return true, nil
+
+	return true, cacheable, nil
 }
 
 func ReadGetValidEdgeTargetsConfig(path string) (GetValidEdgeTargetsConfig, error) {
@@ -219,21 +241,21 @@ func (e *Engine) GetValidEdgeTargets(context *GetPossibleEdgesContext) (map[stri
 				// check if we already know the validity of this edge
 				sourceType := source.QualifiedTypeName()
 				targetType := target.QualifiedTypeName()
-
+				//
 				isValid := false
-				previouslyEvaluated := false
+				previouslCached := false
 
 				rwLock.RLock()
 				if _, ok := knownTargetValidity[sourceType]; ok {
 					if isValid, ok = knownTargetValidity[sourceType][targetType]; ok {
-						previouslyEvaluated = true
+						previouslCached = true
 					}
 				}
 				rwLock.RUnlock()
-
+				cacheable := false
 				// only evaluate the edge if we haven't already done so for the same source and target types
-				if !previouslyEvaluated {
-					isValid, _ = e.EdgeCanBeExpanded(source, target)
+				if !previouslCached {
+					isValid, cacheable, _ = e.EdgeCanBeExpanded(solutionCtx, source, target)
 				} else {
 					zap.S().Debugf("Using cached result for %s -> %s: %t", source, target, isValid)
 				}
@@ -243,18 +265,20 @@ func (e *Engine) GetValidEdgeTargets(context *GetPossibleEdgesContext) (map[stri
 					Target:  target,
 					IsValid: isValid,
 				}
-				if previouslyEvaluated {
+				if previouslCached {
 					return
 				}
 
 				// cache the result, so we don't have to recompute it for the same source and target types
 				// performance benefit is unclear given potential lock contention between goroutines
-				rwLock.Lock()
-				if _, ok := knownTargetValidity[sourceType]; !ok {
-					knownTargetValidity[sourceType] = make(map[string]bool)
+				if cacheable {
+					rwLock.Lock()
+					if _, ok := knownTargetValidity[sourceType]; !ok {
+						knownTargetValidity[sourceType] = make(map[string]bool)
+					}
+					knownTargetValidity[sourceType][targetType] = isValid
+					rwLock.Unlock()
 				}
-				knownTargetValidity[sourceType][targetType] = isValid
-				rwLock.Unlock()
 			})
 		}
 	}
diff --git a/pkg/engine2/operational_eval/eval.go b/pkg/engine2/operational_eval/eval.go
index b2763009..c60c9e0f 100644
--- a/pkg/engine2/operational_eval/eval.go
+++ b/pkg/engine2/operational_eval/eval.go
@@ -3,9 +3,12 @@ package operational_eval
 import (
 	"errors"
 	"fmt"
+	"reflect"
 	"sort"
+	"strings"
 
 	"github.com/dominikbraun/graph"
+	construct "github.com/klothoplatform/klotho/pkg/construct2"
 	"github.com/klothoplatform/klotho/pkg/graph_addons"
 	"github.com/klothoplatform/klotho/pkg/set"
 	"go.uber.org/zap"
@@ -208,3 +211,28 @@ func (eval *Evaluator) RecalculateUnevaluated() error {
 	}
 	return errs
 }
+
+func (eval *Evaluator) cleanupPropertiesSubVertices(ref construct.PropertyRef, val any) error {
+	if reflect.ValueOf(val).Kind() != reflect.Array && reflect.ValueOf(val).Kind() != reflect.Slice {
+		return fmt.Errorf("error while cleaning up properties sub verticies: expected array or slice, got %T", val)
+	}
+	topo, err := graph.TopologicalSort(eval.unevaluated)
+	if err != nil {
+		return err
+	}
+
+	var errs error
+	for _, key := range topo {
+		if key.keyType() != keyTypeProperty {
+			continue
+		}
+		if key.Ref.Resource != ref.Resource {
+			continue
+		}
+		if strings.HasPrefix(key.Ref.Property, ref.Property) {
+			err = eval.unevaluated.RemoveVertex(key)
+			errs = errors.Join(errs, err)
+		}
+	}
+	return errs
+}
diff --git a/pkg/engine2/operational_eval/vertex_property.go b/pkg/engine2/operational_eval/vertex_property.go
index ac5a8b80..a423a5f3 100644
--- a/pkg/engine2/operational_eval/vertex_property.go
+++ b/pkg/engine2/operational_eval/vertex_property.go
@@ -10,6 +10,7 @@ import (
 	"github.com/klothoplatform/klotho/pkg/engine2/operational_rule"
 	"github.com/klothoplatform/klotho/pkg/engine2/solution_context"
 	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
+	"github.com/klothoplatform/klotho/pkg/set"
 )
 
 type (
@@ -140,6 +141,24 @@ func (v *propertyVertex) Evaluate(eval *Evaluator) error {
 	}
 	propertyType := v.Template.Type()
 	if strings.HasPrefix(propertyType, "list") || strings.HasPrefix(propertyType, "set") {
+		property, err := res.GetProperty(v.Ref.Property)
+		if err != nil {
+			return fmt.Errorf("could not get property %s on resource %s: %w", v.Ref.Property, v.Ref.Resource, err)
+		}
+		if property != nil {
+			if strings.HasPrefix(propertyType, "set") {
+				if set, ok := property.(set.HashedSet[string, any]); ok {
+					property = set.ToSlice()
+				} else {
+					return fmt.Errorf("could not convert property %s on resource %s to set", v.Ref.Property, v.Ref.Resource)
+				}
+			}
+
+			err = eval.cleanupPropertiesSubVertices(v.Ref, property)
+			if err != nil {
+				return fmt.Errorf("could not cleanup sub vertices for %s: %w", v.Ref, err)
+			}
+		}
 		// 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)
diff --git a/pkg/infra/iac3/templates/aws/acm_certificate/factory.ts b/pkg/infra/iac3/templates/aws/acm_certificate/factory.ts
index d706d276..50ea5a36 100644
--- a/pkg/infra/iac3/templates/aws/acm_certificate/factory.ts
+++ b/pkg/infra/iac3/templates/aws/acm_certificate/factory.ts
@@ -1,9 +1,48 @@
 import * as aws from '@pulumi/aws'
+import * as awsInputs from '@pulumi/aws/types/input'
+import * as pulumi from '@pulumi/pulumi'
+import { ModelCaseWrapper } from '../../wrappers'
+import * as inputs from '@pulumi/aws/types/input'
 
 interface Args {
     Name: string
+    CertificateTransparencyLoggingPreference?: string
+    DomainName: string
+    EarlyRenewalDuration?: string
+    SubjectAlternativeNames?: string[]
+    Tags?: ModelCaseWrapper<Record<string, string>>
+    ValidationMethod?: string
+    DomainValidationOptions?: pulumi.Input<pulumi.Input<inputs.acm.CertificateValidationOption>[]>
 }
 
 function create(args: Args): aws.acm.Certificate {
-    return new aws.acm.Certificate(args.Name, {})
+    return new aws.acm.Certificate(args.Name, {
+        //TMPL {{- if .CertificateTransparencyLoggingPreference }}
+        options: {
+            certificateTransparencyLoggingPreference: args.CertificateTransparencyLoggingPreference,
+        },
+        //TMPL {{- end }}
+        //TMPL {{- if .EarlyRenewalDuration }}
+        earlyRenewalDuration: args.EarlyRenewalDuration,
+        //TMPL {{- end }}
+        //TMPL {{- if .SubjectAlternativeNames }}
+        subjectAlternativeNames: args.SubjectAlternativeNames,
+        //TMPL {{- end }}
+        //TMPL {{- if .ValidationMethod }}
+        validationMethod: args.ValidationMethod,
+        //TMPL {{- end }}
+        //TMPL {{- if .DomainValidationOptions }}
+        validationOptions: args.DomainValidationOptions,
+        //TMPL {{- end }}
+        domainName: args.DomainName,
+        //TMPL {{- if .Tags }}
+        tags: args.Tags,
+        //TMPL {{- end }}
+    })
+}
+
+function properties(object: aws.acm.Certificate, args: Args) {
+    return {
+        Arn: object.arn,
+    }
 }
diff --git a/pkg/infra/iac3/templates/aws/load_balancer_listener_rule/actions.ts.tmpl b/pkg/infra/iac3/templates/aws/load_balancer_listener_rule/actions.ts.tmpl
new file mode 100644
index 00000000..91500c15
--- /dev/null
+++ b/pkg/infra/iac3/templates/aws/load_balancer_listener_rule/actions.ts.tmpl
@@ -0,0 +1,27 @@
+[
+{{- range $index, $action := . }}
+    {
+        type: "{{ $action.Type}}",
+        {{- if eq $action.Type "forward" }}
+        targetGroupArn: {{ getVar $action.TargetGroup}}.arn,
+        {{- else if eq $action.Type "redirect" }}
+        redirect: {
+            protocol: "{{ $action.Redirect.Protocol }}",
+            port: "{{ $action.Redirect.Port }}",
+            host: "{{ $action.Redirect.Host }}",
+            path: "{{ $action.Redirect.Path }}",
+            query: "{{ $action.Redirect.Query }}",
+            statusCode: "{{ $action.Redirect.StatusCode }}",
+        },
+        {{- else if eq $action.Type "fixed" }}
+        fixedResponse: {
+            contentType: "{{ $action.FixedResponse.ContentType }}",
+            messageBody: "{{ $action.FixedResponse.MessageBody }}",
+            statusCode: "{{ $action.FixedResponse.StatusCode }}",
+        },
+        {{- else if eq $action.Type "authenticate-oidc" }}
+        {{- end }}
+    },
+{{- end}}
+]
+
diff --git a/pkg/infra/iac3/templates/aws/load_balancer_listener_rule/factory.ts b/pkg/infra/iac3/templates/aws/load_balancer_listener_rule/factory.ts
new file mode 100644
index 00000000..56040265
--- /dev/null
+++ b/pkg/infra/iac3/templates/aws/load_balancer_listener_rule/factory.ts
@@ -0,0 +1,25 @@
+import * as aws from '@pulumi/aws'
+import * as inputs from '@pulumi/aws/types/input'
+import { ModelCaseWrapper, TemplateWrapper } from '../../wrappers'
+
+interface Args {
+    Name: string
+    Listener: aws.lb.Listener
+    Priority: number
+    Conditions: []
+    Actions: TemplateWrapper<inputs.lb.ListenerRuleAction[]>
+    Tags?: ModelCaseWrapper<Record<string, string>>
+}
+
+// noinspection JSUnusedLocalSymbols
+function create(args: Args): aws.lb.ListenerRule {
+    return new aws.lb.ListenerRule(args.Name, {
+        listenerArn: args.Listener.arn,
+        priority: args.Priority,
+        conditions: args.Conditions,
+        actions: args.Actions,
+        //TMPL {{- if .Tags }}
+        tags: args.Tags,
+        //TMPL {{- end }}
+    })
+}
diff --git a/pkg/infra/iac3/templates/aws/load_balancer_listener_rule/package.json b/pkg/infra/iac3/templates/aws/load_balancer_listener_rule/package.json
new file mode 100644
index 00000000..1c21f64b
--- /dev/null
+++ b/pkg/infra/iac3/templates/aws/load_balancer_listener_rule/package.json
@@ -0,0 +1,6 @@
+{
+    "name": "listener_rule",
+    "dependencies": {
+        "@pulumi/aws": "^5.37.0"
+    }
+}
diff --git a/pkg/infra/iac3/templates/aws/target_group/factory.ts b/pkg/infra/iac3/templates/aws/target_group/factory.ts
index bd538941..e05ebb5b 100644
--- a/pkg/infra/iac3/templates/aws/target_group/factory.ts
+++ b/pkg/infra/iac3/templates/aws/target_group/factory.ts
@@ -10,6 +10,7 @@ interface Args {
     Tags: Record<string, string>
     Targets: { Id: string; Port: number }[]
     HealthCheck: TemplateWrapper<Record<string, any>>
+    LambdaMultiValueHeadersEnabled?: boolean
 }
 
 // noinspection JSUnusedLocalSymbols
@@ -24,15 +25,25 @@ function create(args: Args): aws.lb.TargetGroup {
             tags: args.Tags,
             //TMPL {{- end }}
             healthCheck: args.HealthCheck,
+            //TMPL {{- if .LambdaMultiValueHeadersEnabled }}
+            lambdaMultiValueHeadersEnabled: args.LambdaMultiValueHeadersEnabled,
+            //TMPL {{- end }}
         })
 
         //TMPL {{- if .Targets }}
         for (const target of args.Targets) {
+            //TMPL {{- if eq .TargetType "instance" }}
             new aws.lb.TargetGroupAttachment(target.Id, {
                 port: target.Port,
                 targetGroupArn: tg.arn,
                 targetId: target.Id,
             })
+            //TMPL {{- else if eq .TargetType "lambda" }}
+            new aws.lb.TargetGroupAttachment(target.Id, {
+                targetGroupArn: tg.arn,
+                targetId: target.Id,
+            })
+            //TMPL {{- end }}
         }
         //TMPL {{- end }}
         return tg
diff --git a/pkg/knowledge_base2/reader/properties.go b/pkg/knowledge_base2/reader/properties.go
index 84741bb5..3e8dd6d4 100644
--- a/pkg/knowledge_base2/reader/properties.go
+++ b/pkg/knowledge_base2/reader/properties.go
@@ -40,12 +40,18 @@ type (
 
 		Properties Properties `json:"properties" yaml:"properties"`
 
+		// MinLength defines the minimum length of a string, list, set, or map (number of entries)
 		MinLength *int `yaml:"min_length"`
 		MaxLength *int `yaml:"max_length"`
 
 		MinValue *float64 `yaml:"min_value"`
 		MaxValue *float64 `yaml:"max_value"`
 
+		// UniqueItems defines whether the items in a list or set must be unique
+		UniqueItems *bool `yaml:"unique_items"`
+		// UniqueKeys defines whether the keys in a map must be unique (default true)
+		UniqueKeys *bool `yaml:"unique_keys"`
+
 		AllowedTypes construct.ResourceList `yaml:"allowed_types"`
 
 		SanitizeTmpl  string   `yaml:"sanitize"`
diff --git a/pkg/templates/aws/edges/load_balancer-load_balancer_listener.yaml b/pkg/templates/aws/edges/load_balancer-load_balancer_listener.yaml
index 7afda32e..d9143aa5 100644
--- a/pkg/templates/aws/edges/load_balancer-load_balancer_listener.yaml
+++ b/pkg/templates/aws/edges/load_balancer-load_balancer_listener.yaml
@@ -2,12 +2,6 @@ source: aws:load_balancer
 target: aws:load_balancer_listener
 deployment_order_reversed: true
 operational_rules:
-  - if: '{{eq (fieldValue "Type" .Source) "application"}}'
-    configuration_rules:
-      - resource: '{{ .Target }}'
-        configuration:
-          field: Protocol
-          value: HTTP
   - if: '{{ eq (fieldValue "Type" .Source) "network" }}'
     configuration_rules:
       - resource: '{{ .Target }}'
diff --git a/pkg/templates/aws/edges/load_balancer_listener-acm-certificate.yaml b/pkg/templates/aws/edges/load_balancer_listener-acm-certificate.yaml
new file mode 100644
index 00000000..e9042fb8
--- /dev/null
+++ b/pkg/templates/aws/edges/load_balancer_listener-acm-certificate.yaml
@@ -0,0 +1,8 @@
+source: aws:load_balancer_listener
+target: aws:acm_certificate
+operational_rules:
+  - resource: '{{ .Source }}'
+    configuration:
+      field: Certificate
+      value: '{{ .Target }}'
+
diff --git a/pkg/templates/aws/edges/load_balancer_listener-load_balancer_listener_rule.yaml b/pkg/templates/aws/edges/load_balancer_listener-load_balancer_listener_rule.yaml
new file mode 100644
index 00000000..bc09f609
--- /dev/null
+++ b/pkg/templates/aws/edges/load_balancer_listener-load_balancer_listener_rule.yaml
@@ -0,0 +1,3 @@
+source: aws:load_balancer_listener
+target: aws:load_balancer_listener_rule
+deployment_order_reversed: true
diff --git a/pkg/templates/aws/edges/load_balancer_listener_rule-target_group.yaml b/pkg/templates/aws/edges/load_balancer_listener_rule-target_group.yaml
new file mode 100644
index 00000000..addbbf3c
--- /dev/null
+++ b/pkg/templates/aws/edges/load_balancer_listener_rule-target_group.yaml
@@ -0,0 +1,35 @@
+source: aws:load_balancer_listener_rule
+target: aws:target_group
+operational_rules:
+  - configuration_rules:
+      - resource: '{{ .Target }}'
+        configuration:
+          field: Protocol
+          value: '{{ fieldValue "Protocol" (fieldValue "Listener" .Source) }}'
+      - resource: '{{ .Target }}'
+        configuration:
+          field: HealthCheck.Protocol
+          value: '{{ fieldValue "Protocol" (fieldValue "Listener" .Source) }}'
+      - resource: '{{ (fieldValue "Listener" .Source) }}'
+        configuration:
+          field: DefaultActions
+          value:
+            - TargetGroup: '{{ .Target }}'
+              Type: forward
+      - resource: '{{ .Source }}'
+        configuration:
+          field: Actions
+          value:
+            - TargetGroup: '{{ .Target }}'
+              Type: forward
+
+  - if: '{{eq (fieldValue "Protocol" (fieldValue "Listener" .Source)) "HTTPS"}}'
+    steps:
+      - resource: '{{ fieldValue "Listener" .Source }}'
+        direction: downstream
+        resources:
+          - aws:listener_certificate
+
+classification:
+  - target
+  - service_endpoint
\ No newline at end of file
diff --git a/pkg/templates/aws/models/load_balancer_listener_action.yaml b/pkg/templates/aws/models/load_balancer_listener_action.yaml
new file mode 100644
index 00000000..d4e45250
--- /dev/null
+++ b/pkg/templates/aws/models/load_balancer_listener_action.yaml
@@ -0,0 +1,47 @@
+name: aws:load_balancer_listener:action
+properties:
+  Type:
+    type: string
+    allowed_values:
+      - forward
+      - fixed-response
+      - redirect
+    default_value: forward
+  TargetGroup:
+    type: resource(aws:target_group)
+  FixedResponse:
+    type: map
+    properties:
+      ContentType:
+        type: string
+      MessageBody:
+        type: string
+        min_length: 0
+        max_length: 1024
+      StatusCode:
+        # todo: validate pattern ^(2|4|5)\d\d$
+        type: string
+  Redirect:
+    type: map
+    properties:
+      Host:
+        type: string
+        min_length: 1
+        max_length: 128
+      Path:
+        type: string
+        min_length: 1
+        max_length: 128
+      Port:
+        type: any
+        validation: '{{ or (eq "#{port}") (and (gt 0) (lt 65536)) }}'
+      Protocol:
+        type: string
+        validation: '{{ or (eq "#{protocol}") (or (eq "HTTP") ( eq "HTTPS")) }}'
+      Query:
+        type: string
+      StatusCode:
+        type: string
+        allowed_values:
+          - HTTP_301
+          - HTTP_302
\ No newline at end of file
diff --git a/pkg/templates/aws/resources/acm_certificate.yaml b/pkg/templates/aws/resources/acm_certificate.yaml
index c4903304..2503480a 100644
--- a/pkg/templates/aws/resources/acm_certificate.yaml
+++ b/pkg/templates/aws/resources/acm_certificate.yaml
@@ -1,6 +1,45 @@
 qualified_type_name: aws:acm_certificate
 display_name: ACM Certificate
 
+properties:
+  Arn:
+    type: string
+    configuration_disabled: true
+    deploy_time: true
+  CertificateTransparencyLoggingPreference:
+    type: string
+    allowed_values:
+      - ENABLED
+      - DISABLED
+  DomainName:
+    type: string
+    description: Fully qualified domain name (FQDN), such as www.example.com, that you want to secure with an ACM certificate
+    min_length: 1
+    max_length: 253
+  EarlyRenewalDuration:
+    type: string
+    description: The number of days prior to the certificate expiration date when ACM starts automatic renewal
+  SubjectAlternativeNames:
+    type: list(string)
+    description: Additional FQDNs to be included in the Subject Alternative Name extension of the ACM certificate
+#    min_length: 1
+#    max_length: 253
+  Tags:
+    type: map(string,string)
+  ValidationMethod:
+    type: string
+    default_value: DNS
+    allowed_values:
+      - DNS
+      - EMAIL
+  DomainValidationOptions:
+    type: list(map)
+    properties:
+      DomainName:
+        type: string
+      ValidationDomain:
+        type: string
+
 classification:
   is:
     - certificate
diff --git a/pkg/templates/aws/resources/load_balancer.yaml b/pkg/templates/aws/resources/load_balancer.yaml
index e95f7e10..14da1282 100644
--- a/pkg/templates/aws/resources/load_balancer.yaml
+++ b/pkg/templates/aws/resources/load_balancer.yaml
@@ -19,6 +19,9 @@ properties:
   IpAddressType:
     type: string
     description: Specifies the type of IP addresses used by the load balancer's subnets
+    allowed_values:
+      - ipv4
+      - dualstack
   LoadBalancerAttributes:
     type: map(string,string)
     description: A map of key-value pairs defining attributes of the load balancer
@@ -26,6 +29,9 @@ properties:
     type: string
     default_value: internal
     description: Indicates whether the load balancer is internet-facing or internal
+    allowed_values:
+      - internet-facing
+      - internal
   SecurityGroups:
     # We should require an upstream security group if the LB is in a VPC
     # once we have the ability to disable inbound rule processing for PrivateLink traffic,
@@ -68,6 +74,8 @@ properties:
       - network
       - application
     description: "The type of load balancer: either 'network' or 'application'"
+    required: true
+    important: true
   NlbUri:
     type: string
     configuration_disabled: true
@@ -91,4 +99,4 @@ classification:
 delete_context:
   requires_no_downstream: true
 views:
-  dataflow: big
+  dataflow: parent
diff --git a/pkg/templates/aws/resources/load_balancer_listener.yaml b/pkg/templates/aws/resources/load_balancer_listener.yaml
index 52c7f3c0..0389b5b2 100644
--- a/pkg/templates/aws/resources/load_balancer_listener.yaml
+++ b/pkg/templates/aws/resources/load_balancer_listener.yaml
@@ -2,23 +2,43 @@ qualified_type_name: aws:load_balancer_listener
 display_name: Load Balancer Listener
 
 properties:
+  Certificate:
+    type: resource(aws:acm_certificate)
+    operational_rule:
+      if: '{{ eq (fieldValue "Protocol" .Self) "HTTPS" }}'
+      step:
+        direction: downstream
+        resources:
+          - aws:acm_certificate
   Port:
     type: int
     default_value: 80
+    min_value: 1
+    max_value: 65535
+    required: true
   Protocol:
     type: string
+    allowed_values:
+        - GENEVE
+        - HTTP
+        - HTTPS
+        - TCP
+        - TCP_UDP
+        - TLS
+        - UDP
+    required: true
   LoadBalancer:
     type: resource(aws:load_balancer)
-    default_value: '{{ upstream "aws:load_balancer" .Self }}'
+    namespace: true
+    operational_rule:
+      step:
+        direction: upstream
+        resources:
+          - aws:load_balancer
   DefaultActions:
-    type: list
-    properties:
-      TargetGroup:
-        type: resource(aws:target_group)
-      Type:
-        type: string
+    type: list(model(aws:load_balancer_listener:action))
 
 delete_context:
-  requires_no_upstream_or_downstream: true
+  requires_no_upstream: true
 views:
-  dataflow: small
+  dataflow: small
\ No newline at end of file
diff --git a/pkg/templates/aws/resources/load_balancer_listener_rule.yaml b/pkg/templates/aws/resources/load_balancer_listener_rule.yaml
index 34e144a9..dd94e6c0 100644
--- a/pkg/templates/aws/resources/load_balancer_listener_rule.yaml
+++ b/pkg/templates/aws/resources/load_balancer_listener_rule.yaml
@@ -3,29 +3,74 @@ display_name: Listener Rule
 
 properties:
   Actions:
-    type: list
-    properties:
-      Type:
-        type: string
-        default_value: forward
-      TargetGroup:
-        type: resource(aws:target_group)
-        default_value: '{{ downstream "aws:target_group" .Self }}'
+    type: list(model(aws:load_balancer_listener:action))
   Conditions:
-    type: list
+    type: list(map)
+    max_length: 5
     properties:
-      PathPattern:
-        type: list(string)
-        default_value: path-pattern
+      HostHeader:
+        type: map
+        properties:
+          Values:
+            type: list(string)
+      HttpHeader:
+        type: map
+        properties:
+          HttpHeaderName:
+            type: string
+          Values:
+            type: list(string)
       HttpRequestMethod:
-        type: list(string)
+        type: map
+        properties:
+          Values:
+            type: list(string)
+      PathPattern:
+        type: map
+        properties:
+          Values:
+            type: list(string)
+      QueryString:
+        type: map
+        properties:
+          Values:
+            type: list(map)
+            properties:
+              Key:
+                type: string
+              Value:
+                type: string
+      SourceIp:
+        type: map
+        properties:
+          Values:
+            type: list(string)
   Listener:
     type: resource(aws:load_balancer_listener)
-    default_value: '{{ upstream "aws:load_balancer_listener" .Self }}'
+    namespace: true
+    operational_rule:
+      step:
+        direction: upstream
+        resources:
+          - aws:load_balancer_listener
   Priority:
     type: int
+    required: true
+    min_value: 1
+    max_value: 50000
+    default_value: 1
+  Tags:
+    type: map(string,string)
 
 delete_context:
-  requires_no_upstream_or_downstream: true
+  requires_no_upstream: true
 views:
-  dataflow: small
+  dataflow: big
+
+classification:
+  is:
+    - listener_rule
+
+path_satisfaction:
+  as_source:
+    - network#Listener#LoadBalancer#Subnets
diff --git a/pkg/templates/aws/resources/target_group.yaml b/pkg/templates/aws/resources/target_group.yaml
index 2c8ef474..6589cb06 100644
--- a/pkg/templates/aws/resources/target_group.yaml
+++ b/pkg/templates/aws/resources/target_group.yaml
@@ -17,13 +17,31 @@ properties:
   Port:
     type: int
     default_value: 80
+    min_value: 1
+    max_value: 65535
   Protocol:
     type: string
+    allowed_values:
+      - GENEVE
+      - HTTP
+      - HTTPS
+      - TCP
+      - TCP_UDP
+      - TLS
+      - UDP
   Vpc:
     type: resource(aws:vpc)
     default_value: '{{ closestDownstream "aws:vpc" .Self }}'
   TargetType:
     type: string
+    allowed_values:
+#      - alb # only valid for Network Load Balancers
+      - instance
+      - ip
+      - lambda # only valid for Application Load Balancers
+    description: |
+      The type of target that you must specify when registering targets with this target group.
+      'lambda` is only valid for Application Load Balancers.
   Targets:
     type: list
     properties:
@@ -32,6 +50,8 @@ properties:
       Port:
         type: int
         default_value: 80
+        min_value: 1
+        max_value: 65535
   HealthCheck:
     type: map
     properties:
@@ -43,8 +63,14 @@ properties:
       Protocol:
         type: string
         default_value: '{{ fieldValue "Protocol" .Self }}'
+        allowed_values:
+          - HTTP
+          - HTTPS
+          - TCP
       Port:
         type: int
+        min_value: 1
+        max_value: 65535
       Interval:
         type: int
         default_value: 30
@@ -59,6 +85,8 @@ properties:
         default_value: 2
       Matcher:
         type: string
+  LambdaMultiValueHeadersEnabled:
+    type: bool
   Arn:
     type: string
     configuration_disabled: true


Test patch:
diff --git a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
index f4a7ddad..ace9d8af 100755
--- a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
@@ -2,21 +2,23 @@ provider: aws
 resources:
   kubernetes:pod:eks_cluster-0/pod2:
     children:
-        - aws:iam_role:ClusterRole-eks_cluster-0
+      - aws:iam_role:ClusterRole-eks_cluster-0
     parent: eks_cluster/eks_cluster-0
     tag: big
 
   load_balancer/rest-api-4-integbcc77100:
+    children:
+      - aws:load_balancer_listener:rest-api-4-integbcc77100:rest_api_4_integration_0-pod2
     parent: vpc/vpc-0
-    tag: big
+    tag: parent
 
   load_balancer/rest-api-4-integbcc77100 -> kubernetes:pod:eks_cluster-0/pod2:
     path:
-        - aws:load_balancer_listener:rest_api_4_integration_0-pod2
-        - aws:security_group:vpc-0:eks_cluster-0-security_group
-        - aws:target_group:rest-api-4-integbcc77100
-        - kubernetes:service:eks_cluster-0:restapi4integration0-pod2
-        - kubernetes:target_group_binding:eks_cluster-0:restapi4integration0-pod2
+      - aws:load_balancer_listener:rest-api-4-integbcc77100:rest_api_4_integration_0-pod2
+      - aws:security_group:vpc-0:eks_cluster-0-security_group
+      - aws:target_group:rest-api-4-integbcc77100
+      - kubernetes:service:eks_cluster-0:restapi4integration0-pod2
+      - kubernetes:target_group_binding:eks_cluster-0:restapi4integration0-pod2
 
   aws:api_integration:rest_api_4/rest_api_4_integration_0:
     parent: rest_api/rest_api_4
@@ -24,58 +26,58 @@ resources:
 
   aws:api_integration:rest_api_4/rest_api_4_integration_0 -> load_balancer/rest-api-4-integbcc77100:
     path:
-        - aws:vpc_link:rest_api_4_integration_0-pod2
+      - aws:vpc_link:rest_api_4_integration_0-pod2
 
   kubernetes:helm_chart:eks_cluster-0/metricsserver:
     children:
-        - aws:iam_role:ClusterRole-eks_cluster-0
+      - aws:iam_role:ClusterRole-eks_cluster-0
     parent: eks_cluster/eks_cluster-0
     tag: big
 
   kubernetes:helm_chart:eks_cluster-0/aws-load-balancer-controller:
     children:
-        - aws:iam_role:ClusterRole-eks_cluster-0
+      - aws:iam_role:ClusterRole-eks_cluster-0
     parent: eks_cluster/eks_cluster-0
     tag: big
 
   vpc/vpc-0:
     children:
-        - aws:internet_gateway:vpc-0:internet_gateway-0
-        - aws:route_table:vpc-0:subnet-0-route_table
-        - aws:route_table:vpc-0:subnet-1-route_table
-        - aws:route_table:vpc-0:subnet-2-route_table
-        - aws:route_table:vpc-0:subnet-3-route_table
-        - aws:security_group:vpc-0:eks_cluster-0-security_group
-        - aws:subnet:vpc-0:subnet-0
-        - aws:subnet:vpc-0:subnet-1
-        - aws:subnet:vpc-0:subnet-2
-        - aws:subnet:vpc-0:subnet-3
+      - aws:internet_gateway:vpc-0:internet_gateway-0
+      - aws:route_table:vpc-0:subnet-0-route_table
+      - aws:route_table:vpc-0:subnet-1-route_table
+      - aws:route_table:vpc-0:subnet-2-route_table
+      - aws:route_table:vpc-0:subnet-3-route_table
+      - aws:security_group:vpc-0:eks_cluster-0-security_group
+      - aws:subnet:vpc-0:subnet-0
+      - aws:subnet:vpc-0:subnet-1
+      - aws:subnet:vpc-0:subnet-2
+      - aws:subnet:vpc-0:subnet-3
     tag: parent
 
   rest_api/rest_api_4:
     children:
-        - aws:api_deployment:rest_api_4:api_deployment-0
-        - aws:api_integration:rest_api_4:rest_api_4_integration_0
-        - aws:api_method:rest_api_4:rest_api_4_integration_0_method
-        - aws:api_resource:rest_api_4:api_resource-0
-        - aws:api_stage:rest_api_4:api_stage-0
+      - aws:api_deployment:rest_api_4:api_deployment-0
+      - aws:api_integration:rest_api_4:rest_api_4_integration_0
+      - aws:api_method:rest_api_4:rest_api_4_integration_0_method
+      - aws:api_resource:rest_api_4:api_resource-0
+      - aws:api_stage:rest_api_4:api_stage-0
     tag: parent
 
   rest_api/rest_api_4 -> aws:api_integration:rest_api_4/rest_api_4_integration_0:
     path:
-        - aws:api_method:rest_api_4:rest_api_4_integration_0_method
-        - aws:api_resource:rest_api_4:api_resource-0
+      - aws:api_method:rest_api_4:rest_api_4_integration_0_method
+      - aws:api_resource:rest_api_4:api_resource-0
 
   eks_cluster/eks_cluster-0:
     children:
-        - aws:iam_role:ClusterRole-eks_cluster-0
-        - kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller
-        - kubernetes:helm_chart:eks_cluster-0:metricsserver
-        - kubernetes:pod:eks_cluster-0:pod2
-        - kubernetes:service:eks_cluster-0:restapi4integration0-pod2
-        - kubernetes:service_account:eks_cluster-0:aws-load-balancer-controller
-        - kubernetes:service_account:eks_cluster-0:pod2
-        - kubernetes:target_group_binding:eks_cluster-0:restapi4integration0-pod2
+      - aws:iam_role:ClusterRole-eks_cluster-0
+      - kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller
+      - kubernetes:helm_chart:eks_cluster-0:metricsserver
+      - kubernetes:pod:eks_cluster-0:pod2
+      - kubernetes:service:eks_cluster-0:restapi4integration0-pod2
+      - kubernetes:service_account:eks_cluster-0:aws-load-balancer-controller
+      - kubernetes:service_account:eks_cluster-0:pod2
+      - kubernetes:target_group_binding:eks_cluster-0:restapi4integration0-pod2
     parent: vpc/vpc-0
     tag: parent
 
diff --git a/pkg/engine2/testdata/k8s_api.expect.yaml b/pkg/engine2/testdata/k8s_api.expect.yaml
index 79ca9474..dfdbfdde 100755
--- a/pkg/engine2/testdata/k8s_api.expect.yaml
+++ b/pkg/engine2/testdata/k8s_api.expect.yaml
@@ -28,29 +28,29 @@ resources:
         apiVersion: v1
         clusters:
             - cluster:
-                certificateAuthorityData: aws:eks_cluster:eks_cluster-0#CertificateAuthorityData
-                server: aws:eks_cluster:eks_cluster-0#ClusterEndpoint
+                  certificateAuthorityData: aws:eks_cluster:eks_cluster-0#CertificateAuthorityData
+                  server: aws:eks_cluster:eks_cluster-0#ClusterEndpoint
               name: aws:eks_cluster:eks_cluster-0#Name
         contexts:
             - context:
-                cluster: aws:eks_cluster:eks_cluster-0#Name
-                user: aws:eks_cluster:eks_cluster-0#Name
+                  cluster: aws:eks_cluster:eks_cluster-0#Name
+                  user: aws:eks_cluster:eks_cluster-0#Name
               name: aws:eks_cluster:eks_cluster-0#Name
         currentContext: aws:eks_cluster:eks_cluster-0#Name
         kind: Config
         users:
             - name: aws:eks_cluster:eks_cluster-0#Name
               user:
-                exec:
-                    apiVersion: client.authentication.k8s.io/v1beta1
-                    args:
-                        - eks
-                        - get-token
-                        - --cluster-name
-                        - aws:eks_cluster:eks_cluster-0#Name
-                        - --region
-                        - aws:region:region-0#Name
-                    command: aws
+                  exec:
+                      apiVersion: client.authentication.k8s.io/v1beta1
+                      args:
+                          - eks
+                          - get-token
+                          - --cluster-name
+                          - aws:eks_cluster:eks_cluster-0#Name
+                          - --region
+                          - aws:region:region-0#Name
+                      command: aws
     aws:api_deployment:rest_api_4:api_deployment-0:
         RestApi: aws:rest_api:rest_api_4
         Triggers:
@@ -60,15 +60,15 @@ resources:
         AssumeRolePolicyDoc:
             Statement:
                 - Action:
-                    - sts:AssumeRoleWithWebIdentity
+                      - sts:AssumeRoleWithWebIdentity
                   Effect: Allow
                   Principal:
-                    Federated:
-                        - aws:iam_oidc_provider:eks_cluster-0#Arn
+                      Federated:
+                          - aws:iam_oidc_provider:eks_cluster-0#Arn
             Version: "2012-10-17"
         ManagedPolicies:
-            - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess
             - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
+            - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess
     aws:rest_api:rest_api_4:
         BinaryMediaTypes:
             - application/octet-stream
@@ -105,7 +105,7 @@ resources:
             - aws:subnet:vpc-0:subnet-0
             - aws:subnet:vpc-0:subnet-1
         Type: network
-    aws:load_balancer_listener:rest_api_4_integration_0-pod2:
+    aws:load_balancer_listener:rest-api-4-integbcc77100:rest_api_4_integration_0-pod2:
         DefaultActions:
             - TargetGroup: aws:target_group:rest-api-4-integbcc77100
               Type: forward
@@ -205,20 +205,20 @@ resources:
                     - image: aws:ecr_image:pod2-ecr_image#ImageName
                       name: pod2
                       ports:
-                        - containerPort: 80
-                          hostPort: 80
-                          name: default-tcp
-                          protocol: TCP
+                          - containerPort: 80
+                            hostPort: 80
+                            name: default-tcp
+                            protocol: TCP
                 serviceAccountName: kubernetes:service_account:eks_cluster-0:pod2
     aws:iam_role:aws-load-balancer-controller:
         AssumeRolePolicyDoc:
             Statement:
                 - Action:
-                    - sts:AssumeRoleWithWebIdentity
+                      - sts:AssumeRoleWithWebIdentity
                   Effect: Allow
                   Principal:
-                    Federated:
-                        - aws:iam_oidc_provider:eks_cluster-0#Arn
+                      Federated:
+                          - aws:iam_oidc_provider:eks_cluster-0#Arn
             Version: "2012-10-17"
     aws:ecr_image:pod2-ecr_image:
         Context: .
@@ -260,174 +260,174 @@ resources:
         AssumeRolePolicyDoc:
             Statement:
                 - Action:
-                    - sts:AssumeRole
+                      - sts:AssumeRole
                   Effect: Allow
                   Principal:
-                    Service:
-                        - ec2.amazonaws.com
+                      Service:
+                          - ec2.amazonaws.com
             Version: "2012-10-17"
         ManagedPolicies:
-            - arn:aws:iam::aws:policy/AWSCloudMapFullAccess
             - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
             - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
             - 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
     aws:iam_role:pod2:
         AssumeRolePolicyDoc:
             Statement:
                 - Action:
-                    - sts:AssumeRoleWithWebIdentity
+                      - sts:AssumeRoleWithWebIdentity
                   Effect: Allow
                   Principal:
-                    Federated:
-                        - aws:iam_oidc_provider:eks_cluster-0#Arn
+                      Federated:
+                          - aws:iam_oidc_provider:eks_cluster-0#Arn
             Version: "2012-10-17"
     aws:iam_policy:iam_policy-0:
         Policy:
             Statement:
                 - Action:
-                    - ec2:DescribeAccountAttributes
-                    - ec2:DescribeAddresses
-                    - ec2:DescribeAvailabilityZones
-                    - ec2:DescribeInternetGateways
-                    - ec2:DescribeVpcs
-                    - ec2:DescribeVpcPeeringConnections
-                    - ec2:DescribeSubnets
-                    - ec2:DescribeSecurityGroups
-                    - ec2:DescribeInstances
-                    - ec2:DescribeNetworkInterfaces
-                    - ec2:DescribeTags
-                    - ec2:GetCoipPoolUsage
-                    - ec2:DescribeCoipPools
-                    - elasticloadbalancing:DescribeLoadBalancers
-                    - elasticloadbalancing:DescribeLoadBalancerAttributes
-                    - elasticloadbalancing:DescribeListeners
-                    - elasticloadbalancing:DescribeListenerCertificates
-                    - elasticloadbalancing:DescribeSSLPolicies
-                    - elasticloadbalancing:DescribeRules
-                    - elasticloadbalancing:DescribeTargetGroups
-                    - elasticloadbalancing:DescribeTargetGroupAttributes
-                    - elasticloadbalancing:DescribeTargetHealth
-                    - elasticloadbalancing:DescribeTags
-                    - elasticloadbalancing:CreateListener
-                    - elasticloadbalancing:DeleteListener
-                    - elasticloadbalancing:CreateRule
-                    - elasticloadbalancing:DeleteRule
+                      - ec2:DescribeAccountAttributes
+                      - ec2:DescribeAddresses
+                      - ec2:DescribeAvailabilityZones
+                      - ec2:DescribeInternetGateways
+                      - ec2:DescribeVpcs
+                      - ec2:DescribeVpcPeeringConnections
+                      - ec2:DescribeSubnets
+                      - ec2:DescribeSecurityGroups
+                      - ec2:DescribeInstances
+                      - ec2:DescribeNetworkInterfaces
+                      - ec2:DescribeTags
+                      - ec2:GetCoipPoolUsage
+                      - ec2:DescribeCoipPools
+                      - elasticloadbalancing:DescribeLoadBalancers
+                      - elasticloadbalancing:DescribeLoadBalancerAttributes
+                      - elasticloadbalancing:DescribeListeners
+                      - elasticloadbalancing:DescribeListenerCertificates
+                      - elasticloadbalancing:DescribeSSLPolicies
+                      - elasticloadbalancing:DescribeRules
+                      - elasticloadbalancing:DescribeTargetGroups
+                      - elasticloadbalancing:DescribeTargetGroupAttributes
+                      - elasticloadbalancing:DescribeTargetHealth
+                      - elasticloadbalancing:DescribeTags
+                      - elasticloadbalancing:CreateListener
+                      - elasticloadbalancing:DeleteListener
+                      - elasticloadbalancing:CreateRule
+                      - elasticloadbalancing:DeleteRule
                   Effect: Allow
                   Resource:
-                    - '*'
+                      - '*'
                 - Action:
-                    - cognito-idp:DescribeUserPoolClient
-                    - acm:ListCertificates
-                    - acm:DescribeCertificate
-                    - iam:ListServerCertificates
-                    - iam:GetServerCertificate
-                    - waf-regional:GetWebACL
-                    - waf-regional:GetWebACLForResource
-                    - waf-regional:AssociateWebACL
-                    - waf-regional:DisassociateWebACL
-                    - wafv2:GetWebACL
-                    - wafv2:GetWebACLForResource
-                    - wafv2:AssociateWebACL
-                    - wafv2:DisassociateWebACL
-                    - shield:GetSubscriptionState
-                    - shield:DescribeProtection
-                    - shield:CreateProtection
-                    - shield:DeleteProtection
+                      - cognito-idp:DescribeUserPoolClient
+                      - acm:ListCertificates
+                      - acm:DescribeCertificate
+                      - iam:ListServerCertificates
+                      - iam:GetServerCertificate
+                      - waf-regional:GetWebACL
+                      - waf-regional:GetWebACLForResource
+                      - waf-regional:AssociateWebACL
+                      - waf-regional:DisassociateWebACL
+                      - wafv2:GetWebACL
+                      - wafv2:GetWebACLForResource
+                      - wafv2:AssociateWebACL
+                      - wafv2:DisassociateWebACL
+                      - shield:GetSubscriptionState
+                      - shield:DescribeProtection
+                      - shield:CreateProtection
+                      - shield:DeleteProtection
                   Effect: Allow
                   Resource:
-                    - '*'
+                      - '*'
                 - Action:
-                    - iam:CreateServiceLinkedRole
+                      - iam:CreateServiceLinkedRole
                   Effect: Allow
                   Resource:
-                    - '*'
+                      - '*'
                 - Action:
-                    - ec2:AuthorizeSecurityGroupIngress
-                    - ec2:RevokeSecurityGroupIngress
+                      - ec2:AuthorizeSecurityGroupIngress
+                      - ec2:RevokeSecurityGroupIngress
                   Effect: Allow
                   Resource:
-                    - '*'
+                      - '*'
                 - Action:
-                    - ec2:CreateSecurityGroup
+                      - ec2:CreateSecurityGroup
                   Effect: Allow
                   Resource:
-                    - '*'
+                      - '*'
                 - Action:
-                    - ec2:CreateTags
+                      - ec2:CreateTags
                   Condition: {}
                   Effect: Allow
                   Resource:
-                    - arn:aws:ec2:*:*:security-group/*
+                      - arn:aws:ec2:*:*:security-group/*
                 - Action:
-                    - ec2:CreateTags
-                    - ec2:DeleteTags
+                      - ec2:CreateTags
+                      - ec2:DeleteTags
                   Condition: {}
                   Effect: Allow
                   Resource:
-                    - arn:aws:ec2:*:*:security-group/*
+                      - arn:aws:ec2:*:*:security-group/*
                 - Action:
-                    - ec2:AuthorizeSecurityGroupIngress
-                    - ec2:RevokeSecurityGroupIngress
-                    - ec2:DeleteSecurityGroup
+                      - ec2:AuthorizeSecurityGroupIngress
+                      - ec2:RevokeSecurityGroupIngress
+                      - ec2:DeleteSecurityGroup
                   Condition: {}
                   Effect: Allow
                   Resource:
-                    - arn:aws:ec2:*:*:security-group/*
+                      - arn:aws:ec2:*:*:security-group/*
                 - Action:
-                    - elasticloadbalancing:CreateLoadBalancer
-                    - elasticloadbalancing:CreateTargetGroup
+                      - elasticloadbalancing:CreateLoadBalancer
+                      - elasticloadbalancing:CreateTargetGroup
                   Condition: {}
                   Effect: Allow
                   Resource:
-                    - arn:aws:ec2:*:*:security-group/*
+                      - arn:aws:ec2:*:*:security-group/*
                 - Action:
-                    - elasticloadbalancing:AddTags
-                    - elasticloadbalancing:RemoveTags
+                      - elasticloadbalancing:AddTags
+                      - elasticloadbalancing:RemoveTags
                   Condition: {}
                   Effect: Allow
                   Resource:
-                    - arn:aws:elasticloadbalancing:*:*:targetgroup/*/*
-                    - arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*
-                    - arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*
+                      - arn:aws:elasticloadbalancing:*:*:targetgroup/*/*
+                      - arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*
+                      - arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*
                 - Action:
-                    - elasticloadbalancing:AddTags
-                    - elasticloadbalancing:RemoveTags
+                      - elasticloadbalancing:AddTags
+                      - elasticloadbalancing:RemoveTags
                   Effect: Allow
                   Resource:
-                    - arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*
-                    - arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*
-                    - arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*
-                    - arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*
+                      - arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*
+                      - arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*
+                      - arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*
+                      - arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*
                 - Action:
-                    - elasticloadbalancing:ModifyLoadBalancerAttributes
-                    - elasticloadbalancing:SetIpAddressType
-                    - elasticloadbalancing:SetSecurityGroups
-                    - elasticloadbalancing:SetSubnets
-                    - elasticloadbalancing:DeleteLoadBalancer
-                    - elasticloadbalancing:ModifyTargetGroup
-                    - elasticloadbalancing:ModifyTargetGroupAttributes
-                    - elasticloadbalancing:DeleteTargetGroup
+                      - elasticloadbalancing:ModifyLoadBalancerAttributes
+                      - elasticloadbalancing:SetIpAddressType
+                      - elasticloadbalancing:SetSecurityGroups
+                      - elasticloadbalancing:SetSubnets
+                      - elasticloadbalancing:DeleteLoadBalancer
+                      - elasticloadbalancing:ModifyTargetGroup
+                      - elasticloadbalancing:ModifyTargetGroupAttributes
+                      - elasticloadbalancing:DeleteTargetGroup
                   Condition: {}
                   Effect: Allow
                   Resource:
-                    - '*'
+                      - '*'
                 - Action:
-                    - elasticloadbalancing:RegisterTargets
-                    - elasticloadbalancing:DeregisterTargets
+                      - elasticloadbalancing:RegisterTargets
+                      - elasticloadbalancing:DeregisterTargets
                   Effect: Allow
                   Resource:
-                    - arn:aws:elasticloadbalancing:*:*:targetgroup/*/*
+                      - arn:aws:elasticloadbalancing:*:*:targetgroup/*/*
                 - Action:
-                    - elasticloadbalancing:SetWebAcl
-                    - elasticloadbalancing:ModifyListener
-                    - elasticloadbalancing:AddListenerCertificates
-                    - elasticloadbalancing:RemoveListenerCertificates
-                    - elasticloadbalancing:ModifyRule
+                      - elasticloadbalancing:SetWebAcl
+                      - elasticloadbalancing:ModifyListener
+                      - elasticloadbalancing:AddListenerCertificates
+                      - elasticloadbalancing:RemoveListenerCertificates
+                      - elasticloadbalancing:ModifyRule
                   Effect: Allow
                   Resource:
-                    - '*'
+                      - '*'
             Version: "2012-10-17"
     aws:iam_oidc_provider:eks_cluster-0:
         ClientIdLists:
@@ -441,17 +441,17 @@ resources:
         Subnets:
             - aws:subnet:vpc-0:subnet-0
             - aws:subnet:vpc-0:subnet-1
-        Version: "1.28"   
+        Version: "1.28"
         Vpc: aws:vpc:vpc-0
     aws:iam_role:ClusterRole-eks_cluster-0:
         AssumeRolePolicyDoc:
             Statement:
                 - Action:
-                    - sts:AssumeRole
+                      - sts:AssumeRole
                   Effect: Allow
                   Principal:
-                    Service:
-                        - eks.amazonaws.com
+                      Service:
+                          - eks.amazonaws.com
             Version: "2012-10-17"
         ManagedPolicies:
             - arn:aws:iam::aws:policy/AmazonEKSClusterPolicy
@@ -478,14 +478,14 @@ resources:
     aws:security_group:vpc-0:eks_cluster-0-security_group:
         EgressRules:
             - CidrBlocks:
-                - 0.0.0.0/0
+                  - 0.0.0.0/0
               Description: Allows all outbound IPv4 traffic
               FromPort: 0
               Protocol: "-1"
               ToPort: 0
         IngressRules:
             - CidrBlocks:
-                - 0.0.0.0/0
+                  - 0.0.0.0/0
               Description: Allows ingress traffic from the EKS control plane
               FromPort: 9443
               Protocol: TCP
@@ -496,13 +496,13 @@ resources:
               Self: true
               ToPort: 0
             - CidrBlocks:
-                - 10.0.128.0/18
+                  - 10.0.128.0/18
               Description: Allow ingress traffic from ip addresses within the subnet subnet-0
               FromPort: 0
               Protocol: "-1"
               ToPort: 0
             - CidrBlocks:
-                - 10.0.192.0/18
+                  - 10.0.192.0/18
               Description: Allow ingress traffic from ip addresses within the subnet subnet-1
               FromPort: 0
               Protocol: "-1"
@@ -591,20 +591,17 @@ edges:
     aws:api_method:rest_api_4:rest_api_4_integration_0_method -> aws:api_integration:rest_api_4:rest_api_4_integration_0:
     aws:api_integration:rest_api_4:rest_api_4_integration_0 -> aws:vpc_link:rest_api_4_integration_0-pod2:
     aws:vpc_link:rest_api_4_integration_0-pod2 -> aws:load_balancer:rest-api-4-integbcc77100:
-    aws:load_balancer:rest-api-4-integbcc77100 -> aws:load_balancer_listener:rest_api_4_integration_0-pod2:
+    aws:load_balancer:rest-api-4-integbcc77100 -> aws:load_balancer_listener:rest-api-4-integbcc77100:rest_api_4_integration_0-pod2:
     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:load_balancer_listener:rest-api-4-integbcc77100: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:eks_cluster-0:restapi4integration0-pod2
-    :
+    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:eks_cluster-0: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
-    :
+    kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller -> kubernetes:service_account:eks_cluster-0:aws-load-balancer-controller:
     kubernetes:service:eks_cluster-0:restapi4integration0-pod2 -> aws:eks_cluster:eks_cluster-0:
     kubernetes:service:eks_cluster-0:restapi4integration0-pod2 -> kubernetes:pod:eks_cluster-0:pod2:
     kubernetes:service_account:eks_cluster-0:aws-load-balancer-controller -> aws:eks_cluster:eks_cluster-0:
diff --git a/pkg/engine2/testdata/k8s_api.iac-viz.yaml b/pkg/engine2/testdata/k8s_api.iac-viz.yaml
index bf7669a0..5ff3256d 100755
--- a/pkg/engine2/testdata/k8s_api.iac-viz.yaml
+++ b/pkg/engine2/testdata/k8s_api.iac-viz.yaml
@@ -186,10 +186,10 @@ resources:
 
   route_table_association/subnet-0-subnet-0-route_table -> aws:route_table:vpc-0/subnet-0-route_table:
   route_table_association/subnet-0-subnet-0-route_table -> aws:subnet:vpc-0/subnet-0:
-  load_balancer_listener/rest_api_4_integration_0-pod2:
+  aws:load_balancer_listener:rest-api-4-integbcc77100/rest_api_4_integration_0-pod2:
 
-  load_balancer_listener/rest_api_4_integration_0-pod2 -> load_balancer/rest-api-4-integbcc77100:
-  load_balancer_listener/rest_api_4_integration_0-pod2 -> target_group/rest-api-4-integbcc77100:
+  aws:load_balancer_listener:rest-api-4-integbcc77100/rest_api_4_integration_0-pod2 -> load_balancer/rest-api-4-integbcc77100:
+  aws:load_balancer_listener:rest-api-4-integbcc77100/rest_api_4_integration_0-pod2 -> target_group/rest-api-4-integbcc77100:
   iam_role_policy_attachment/aws-load-balancer-controller-iam_policy-0:
 
   iam_role_policy_attachment/aws-load-balancer-controller-iam_policy-0 -> iam_policy/iam_policy-0: