Repository: klothoplatform/klotho
Pull request #921 (2024-02-01 14:58:22)

increasing op eval unit test coverage
mainly happy path tests to increase coverage on expected inputs/outputs. Now that the test base is in place we can ramp up the test coverage on those functions

```
ok  	github.com/klothoplatform/klotho/pkg/engine2/operational_eval	0.474s	coverage: 27.7% of statements
```

### 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/eval.go b/pkg/engine2/operational_eval/eval.go
index ba584f97..93230315 100644
--- a/pkg/engine2/operational_eval/eval.go
+++ b/pkg/engine2/operational_eval/eval.go
@@ -211,7 +211,7 @@ func (eval *Evaluator) RecalculateUnevaluated() error {
 	return errs
 }
 
-func (eval *Evaluator) cleanupPropertiesSubVertices(ref construct.PropertyRef, resource *construct.Resource, val any) error {
+func (eval *Evaluator) cleanupPropertiesSubVertices(ref construct.PropertyRef, resource *construct.Resource) error {
 	topo, err := graph.TopologicalSort(eval.unevaluated)
 	if err != nil {
 		return err
diff --git a/pkg/engine2/operational_eval/evaluator.go b/pkg/engine2/operational_eval/evaluator.go
index db2938d5..25130055 100644
--- a/pkg/engine2/operational_eval/evaluator.go
+++ b/pkg/engine2/operational_eval/evaluator.go
@@ -197,8 +197,10 @@ func (eval *Evaluator) isEvaluated(k Key) (bool, error) {
 	_, err = eval.unevaluated.Vertex(k)
 	if errors.Is(err, graph.ErrVertexNotFound) {
 		return true, nil
+	} else if err != nil {
+		return false, err
 	}
-	return false, err
+	return false, nil
 }
 
 func (eval *Evaluator) addEdge(source, target Key) error {
diff --git a/pkg/engine2/operational_eval/vertex_property.go b/pkg/engine2/operational_eval/vertex_property.go
index 09e6dbbb..cccd18a3 100644
--- a/pkg/engine2/operational_eval/vertex_property.go
+++ b/pkg/engine2/operational_eval/vertex_property.go
@@ -11,7 +11,6 @@ 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 (
@@ -148,15 +147,7 @@ func (v *propertyVertex) Evaluate(eval *Evaluator) error {
 			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, res, property)
+			err = eval.cleanupPropertiesSubVertices(v.Ref, res)
 			if err != nil {
 				return fmt.Errorf("could not cleanup sub vertices for %s: %w", v.Ref, err)
 			}
diff --git a/pkg/infra/iac3/templates/aws/ecs_service/factory.ts b/pkg/infra/iac3/templates/aws/ecs_service/factory.ts
index d345c07d..a7ce69bf 100644
--- a/pkg/infra/iac3/templates/aws/ecs_service/factory.ts
+++ b/pkg/infra/iac3/templates/aws/ecs_service/factory.ts
@@ -20,6 +20,7 @@ interface Args {
     Name: string
     LoadBalancers: TemplateWrapper<any[]>
     dependsOn?: pulumi.Input<pulumi.Input<pulumi.Resource>[]> | pulumi.Input<pulumi.Resource>
+    ServiceRegistries: pulumi.Input<awsInputs.ecs.ServiceServiceRegistries>
 }
 
 // noinspection JSUnusedLocalSymbols
@@ -55,6 +56,9 @@ function create(args: Args): aws.ecs.Service {
             //TMPL {{- end }}
             taskDefinition: args.TaskDefinition.arn,
             waitForSteadyState: true,
+            //TMPL {{- if .ServiceRegistries }}
+            serviceRegistries: args.ServiceRegistries,
+            //TMPL {{- end }}
         },
         { dependsOn: args.dependsOn }
     )
diff --git a/pkg/infra/iac3/templates/aws/lambda_function/factory.ts b/pkg/infra/iac3/templates/aws/lambda_function/factory.ts
index 11a63ed7..1affc31d 100644
--- a/pkg/infra/iac3/templates/aws/lambda_function/factory.ts
+++ b/pkg/infra/iac3/templates/aws/lambda_function/factory.ts
@@ -62,5 +62,6 @@ function create(args: Args): aws.lambda.Function {
 function properties(object: aws.lambda.Function, args: Args) {
     return {
         LambdaIntegrationUri: object.invokeArn,
+        Arn: object.arn,
     }
 }
diff --git a/pkg/infra/iac3/templates/aws/private_dns_namespace/factory.ts b/pkg/infra/iac3/templates/aws/private_dns_namespace/factory.ts
index d0203410..cf58d47f 100644
--- a/pkg/infra/iac3/templates/aws/private_dns_namespace/factory.ts
+++ b/pkg/infra/iac3/templates/aws/private_dns_namespace/factory.ts
@@ -11,3 +11,10 @@ function create(args: Args): aws.servicediscovery.PrivateDnsNamespace {
         vpc: args.Vpc.id,
     })
 }
+
+function properties(object: aws.servicediscovery.PrivateDnsNamespace, args: Args) {
+    return {
+        Id: object.id,
+        Name: object.name,
+    }
+}
diff --git a/pkg/infra/iac3/templates/aws/service_discovery_service/factory.ts b/pkg/infra/iac3/templates/aws/service_discovery_service/factory.ts
new file mode 100644
index 00000000..2fd63aff
--- /dev/null
+++ b/pkg/infra/iac3/templates/aws/service_discovery_service/factory.ts
@@ -0,0 +1,24 @@
+import * as aws from '@pulumi/aws'
+import * as pulumi from '@pulumi/pulumi'
+
+interface Args {
+    Name: string
+    DnsConfig: aws.servicediscovery.ServiceDnsConfig
+    HealthCheckCustomConfig: aws.servicediscovery.ServiceHealthCheckCustomConfig
+}
+
+function create(args: Args): aws.servicediscovery.Service {
+    return new aws.servicediscovery.Service(args.Name, {
+        dnsConfig: args.DnsConfig,
+        //TMPL {{- if .HealthCheckCustomConfig }}
+        healthCheckCustomConfig: args.HealthCheckCustomConfig,
+        //TMPL {{- end }}
+    })
+}
+
+function properties(object: aws.servicediscovery.Service, args: Args) {
+    return {
+        Arn: object.arn,
+        Name: object.name,
+    }
+}
diff --git a/pkg/infra/iac3/templates/aws/service_discovery_service/package.json b/pkg/infra/iac3/templates/aws/service_discovery_service/package.json
new file mode 100644
index 00000000..2352c97e
--- /dev/null
+++ b/pkg/infra/iac3/templates/aws/service_discovery_service/package.json
@@ -0,0 +1,7 @@
+{
+    "name": "service_discovery_service",
+    "dependencies": {
+        "@pulumi/pulumi": "^3.69.0",
+        "@pulumi/aws": "^5.37.0"
+    }
+}
diff --git a/pkg/knowledge_base2/properties/property.go b/pkg/knowledge_base2/properties/property.go
index ffc67dff..b1f20cc6 100644
--- a/pkg/knowledge_base2/properties/property.go
+++ b/pkg/knowledge_base2/properties/property.go
@@ -73,6 +73,18 @@ func ValidatePropertyRef(value construct.PropertyRef, propertyType string, ctx k
 	if err != nil {
 		return nil, fmt.Errorf("error getting property %s on resource %s, while validating property ref: %w", value.Property, value.Resource, err)
 	}
+
+	// recurse down in case of a nested property ref
+	for propValRef, ok := propVal.(construct.PropertyRef); ok; propValRef, ok = propVal.(construct.PropertyRef) {
+		propVal, err = ValidatePropertyRef(propValRef, propertyType, ctx)
+		if err != nil {
+			return nil, err
+		}
+		if propVal == nil {
+			return nil, nil
+		}
+	}
+
 	return propVal, nil
 }
 
diff --git a/pkg/templates/aws/edges/ecr_image-ecr_repo.yaml b/pkg/templates/aws/edges/ecr_image-ecr_repo.yaml
index b5042181..5a6cfc5b 100644
--- a/pkg/templates/aws/edges/ecr_image-ecr_repo.yaml
+++ b/pkg/templates/aws/edges/ecr_image-ecr_repo.yaml
@@ -1,2 +1,3 @@
 source: aws:ecr_image
 target: aws:ecr_repo
+unique: many-to-one
\ No newline at end of file
diff --git a/pkg/templates/aws/edges/private_dns_namespace-ecs_service.yaml b/pkg/templates/aws/edges/private_dns_namespace-ecs_service.yaml
deleted file mode 100644
index a446bf70..00000000
--- a/pkg/templates/aws/edges/private_dns_namespace-ecs_service.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-source: aws:private_dns_namespace
-target: aws:ecs_service
-
-classification:
-  - service_endpoint
diff --git a/pkg/templates/aws/edges/private_dns_namespace-service_discovery_service.yaml b/pkg/templates/aws/edges/private_dns_namespace-service_discovery_service.yaml
new file mode 100644
index 00000000..69e8539a
--- /dev/null
+++ b/pkg/templates/aws/edges/private_dns_namespace-service_discovery_service.yaml
@@ -0,0 +1,12 @@
+source: aws:private_dns_namespace
+target: aws:service_discovery_service
+deployment_order_reversed: true
+
+operational_rules:
+  - configuration_rules:
+      - resource: '{{.Target}}'
+        configuration:
+          field: DnsConfig
+          value:
+            RoutingPolicy: 'MULTIVALUE'
+            NamespaceId: '{{fieldRef "Id" .Source}}'
\ No newline at end of file
diff --git a/pkg/templates/aws/edges/service_api.yaml b/pkg/templates/aws/edges/service_api.yaml
index 1ead50fc..b1acc4e5 100644
--- a/pkg/templates/aws/edges/service_api.yaml
+++ b/pkg/templates/aws/edges/service_api.yaml
@@ -10,5 +10,6 @@ targets:
   - aws:s3_bucket
   - aws:sqs_queue
   - aws:secret
+  - aws:private_dns_namespace
 
 edge_weight_multiplier: 1.08
\ No newline at end of file
diff --git a/pkg/templates/aws/edges/service_discovery_service-ecs_service.yaml b/pkg/templates/aws/edges/service_discovery_service-ecs_service.yaml
new file mode 100644
index 00000000..8b9e1171
--- /dev/null
+++ b/pkg/templates/aws/edges/service_discovery_service-ecs_service.yaml
@@ -0,0 +1,21 @@
+source: aws:service_discovery_service
+target: aws:ecs_service
+deployment_order_reversed: true
+
+operational_rules:
+  - configuration_rules:
+      - resource: '{{.Source}}'
+        configuration:
+          field: DnsConfig.DnsRecords
+          value:
+            - Type: A
+              TTL: 60
+      - resource: '{{.Source}}'
+        configuration:
+          field: Name
+          value: '{{ .Target.Name }}'
+      - resource: '{{.Target}}'
+        configuration:
+          field: ServiceRegistries
+          value:
+            RegistryArn: '{{ fieldRef "Arn" .Source }}'
diff --git a/pkg/templates/aws/resources/ecr_image.yaml b/pkg/templates/aws/resources/ecr_image.yaml
index 156ce7bb..9e595b5b 100644
--- a/pkg/templates/aws/resources/ecr_image.yaml
+++ b/pkg/templates/aws/resources/ecr_image.yaml
@@ -16,6 +16,7 @@ properties:
         direction: downstream
         resources:
           - aws:ecr_repo
+        unique: true
     description: A reference to the AWS ECR repository resource where the image will
       be stored
   Context:
diff --git a/pkg/templates/aws/resources/ecs_service.yaml b/pkg/templates/aws/resources/ecs_service.yaml
index 0566e634..c3ae43d2 100644
--- a/pkg/templates/aws/resources/ecs_service.yaml
+++ b/pkg/templates/aws/resources/ecs_service.yaml
@@ -76,6 +76,13 @@ properties:
 
     description: The family and revision (family:revision) or full Amazon Resource
       Name (ARN) of the task definition to run in the service
+  ServiceRegistries:
+    type: map
+    properties:
+      RegistryArn:
+        type: string
+        description: ARN of the Service Registry. The currently supported service registry is Amazon Route 53 Auto Naming Service
+
 consumption:
   consumed:
     - model: EnvironmentVariables
diff --git a/pkg/templates/aws/resources/ecs_task_definition.yaml b/pkg/templates/aws/resources/ecs_task_definition.yaml
index 63d8ee90..a24d62de 100644
--- a/pkg/templates/aws/resources/ecs_task_definition.yaml
+++ b/pkg/templates/aws/resources/ecs_task_definition.yaml
@@ -123,6 +123,7 @@ properties:
         properties:
           ContainerPath:
             type: string
+            important: true
             description: The path on the container to mount the volume at
           SourceVolume:
             type: string
diff --git a/pkg/templates/aws/resources/private_dns_namespace.yaml b/pkg/templates/aws/resources/private_dns_namespace.yaml
index e29a12d9..b21b94cb 100644
--- a/pkg/templates/aws/resources/private_dns_namespace.yaml
+++ b/pkg/templates/aws/resources/private_dns_namespace.yaml
@@ -9,6 +9,17 @@ properties:
         direction: downstream
         resources:
           - aws:vpc
+  Name:
+    type: string
+    description: The name of the namespace
+  Id:
+    type: string
+    deploy_time: true
+    configuration_disabled: true
+
+path_satisfaction:
+  as_target:
+    - permissions
 
 classification:
   is:
diff --git a/pkg/templates/aws/resources/service_discovery_service.yaml b/pkg/templates/aws/resources/service_discovery_service.yaml
new file mode 100644
index 00000000..ad7dc78f
--- /dev/null
+++ b/pkg/templates/aws/resources/service_discovery_service.yaml
@@ -0,0 +1,61 @@
+qualified_type_name: aws:service_discovery_service
+display_name: Service Discovery Service
+
+properties:
+  DnsConfig:
+    type: map
+    description: The DNS records required to verify the domain
+    properties:
+      DnsRecords:
+        type: list
+        description: An array that contains one DnsRecord object for each resource record set.
+        properties:
+          TTL:
+            type: int
+            description: The amount of time, in seconds, that you want DNS resolvers to cache
+              the value specified in the Value element.
+            min_value: 1
+            max_value: 2147483647
+          Type:
+            type: string
+            description: The type of the resource, which indicates the value that Amazon Route 53 returns in response to DNS queries
+            allowed_values:
+              - A
+              - AAAA
+              - SRV
+              - CNAME
+      NamespaceId:
+        type: string
+        description: The ID of the namespace to use for DNS configuration
+        operational_rule:
+          step:
+            direction: upstream
+            resources:
+              - aws:private_dns_namespace
+            use_property_ref: Id
+    RoutingPolicy:
+      type: string
+      description: The routing policy that you want to apply to all records that Route 53 creates when you register an instance and specify this service
+      allowed_values:
+        - MULTIVALUE
+        - WEIGHTED
+  Name:
+    type: string
+    description: The name of the service
+  HealthCheckCustomConfig:
+    type: map
+    description: A complex type that contains information about an optional custom health check.
+    properties:
+      FailureThreshold:
+        type: int
+        description: The number of 30-second intervals that you want service discovery to wait before it changes the health status of a service instance. Maximum value of 10.
+  Arn:
+    type: string
+    deploy_time: true
+    configuration_disabled: true
+
+
+classification:
+  is:
+    - service_discovery
+


Test patch:
diff --git a/pkg/engine2/operational_eval/eval_test.go b/pkg/engine2/operational_eval/eval_test.go
new file mode 100644
index 00000000..6c98188f
--- /dev/null
+++ b/pkg/engine2/operational_eval/eval_test.go
@@ -0,0 +1,116 @@
+package operational_eval
+
+import (
+	"testing"
+
+	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEvaluator_cleanupPropertiesSubVertices(t *testing.T) {
+
+	tests := []struct {
+		name         string
+		initialState []Vertex
+		ref          construct.PropertyRef
+		resource     *construct.Resource
+		want         []Key
+		wantErr      bool
+	}{
+		{
+			name: "simple resource",
+			initialState: []Vertex{
+				&propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: construct.ResourceId{Name: "a"},
+						Property: "prop1[0]",
+					},
+				},
+				&propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: construct.ResourceId{Name: "a"},
+						Property: "prop1[0].prop2",
+					},
+				},
+			},
+			ref: construct.PropertyRef{
+				Resource: construct.ResourceId{Name: "a"},
+				Property: "prop1[0]",
+			},
+			resource: &construct.Resource{
+				ID: construct.ResourceId{Name: "a"},
+				Properties: map[string]any{
+					"prop1": "value1",
+				},
+			},
+			want: []Key{},
+		},
+		{
+			name: "doesnt remove if path parent 2 back exists",
+			initialState: []Vertex{
+				&propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: construct.ResourceId{Name: "a"},
+						Property: "prop1[0]",
+					},
+				},
+				&propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: construct.ResourceId{Name: "a"},
+						Property: "prop1[0].prop2",
+					},
+				},
+			},
+			ref: construct.PropertyRef{
+				Resource: construct.ResourceId{Name: "a"},
+				Property: "prop1[0].prop2",
+			},
+			resource: &construct.Resource{
+				ID: construct.ResourceId{Name: "a"},
+				Properties: map[string]any{
+					"prop1": "value1",
+				},
+			},
+			want: []Key{
+				{Ref: construct.PropertyRef{
+					Resource: construct.ResourceId{Name: "a"},
+					Property: "prop1[0]",
+				}},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			testSol := enginetesting.NewTestSolution()
+			eval := NewEvaluator(testSol)
+			for _, v := range tt.initialState {
+				err := eval.graph.AddVertex(v)
+				assert.NoError(err)
+				err = eval.unevaluated.AddVertex(v)
+				assert.NoError(err)
+			}
+			err := eval.cleanupPropertiesSubVertices(tt.ref, tt.resource)
+			if tt.wantErr {
+				assert.Error(err)
+				return
+			}
+			assert.NoError(err)
+			adj, err := eval.graph.AdjacencyMap()
+			assert.NoError(err)
+			assert.Equal(len(adj), len(tt.want))
+			for key := range adj {
+				assert.Contains(tt.want, key)
+			}
+
+			// check that the unevaluated graph is also cleaned up
+			adj, err = eval.unevaluated.AdjacencyMap()
+			assert.NoError(err)
+			assert.Equal(len(adj), len(tt.want))
+			for key := range adj {
+				assert.Contains(tt.want, key)
+			}
+		})
+	}
+}
diff --git a/pkg/engine2/operational_eval/evaluator_test.go b/pkg/engine2/operational_eval/evaluator_test.go
new file mode 100644
index 00000000..8c836c17
--- /dev/null
+++ b/pkg/engine2/operational_eval/evaluator_test.go
@@ -0,0 +1,306 @@
+package operational_eval
+
+import (
+	"testing"
+
+	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/construct2/graphtest"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
+	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
+	"github.com/klothoplatform/klotho/pkg/set"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestEvaluator_isEvaluated(t *testing.T) {
+	tests := []struct {
+		name          string
+		vertex        Vertex
+		inGraph       bool
+		inUnevaluated bool
+		want          bool
+		wantErr       bool
+	}{
+		{
+			name: "simple evaluated vertex",
+			vertex: &propertyVertex{
+				Ref: construct.PropertyRef{
+					Resource: graphtest.ParseId(t, "a:a:a"),
+					Property: "prop1",
+				},
+			},
+			inGraph: true,
+			want:    true,
+		},
+		{
+			name: "simple unevaluated vertex",
+			vertex: &propertyVertex{
+				Ref: construct.PropertyRef{
+					Resource: graphtest.ParseId(t, "a:a:a"),
+					Property: "prop1",
+				},
+			},
+			inGraph:       true,
+			inUnevaluated: true,
+			want:          false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			testSol := enginetesting.NewTestSolution()
+			eval := NewEvaluator(testSol)
+			if tt.inGraph {
+				err := eval.graph.AddVertex(tt.vertex)
+				assert.NoError(err)
+			}
+			if tt.inUnevaluated {
+				err := eval.unevaluated.AddVertex(tt.vertex)
+				assert.NoError(err)
+			}
+			got, err := eval.isEvaluated(tt.vertex.Key())
+			if tt.wantErr {
+				assert.Error(err)
+				return
+			}
+			assert.NoError(err)
+			assert.Equal(tt.want, got)
+		})
+	}
+}
+
+func TestEvaluator_addEdge(t *testing.T) {
+	tests := []struct {
+		name         string
+		initialState []Vertex
+		source       Key
+		target       Key
+		wantErr      bool
+	}{
+		{
+			name: "simple add edge",
+			initialState: []Vertex{
+				&propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "a:a:a"),
+						Property: "prop1",
+					},
+				},
+				&propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "a:a:a"),
+						Property: "prop2",
+					},
+				},
+			},
+			source: Key{Ref: construct.PropertyRef{
+				Resource: graphtest.ParseId(t, "a:a:a"),
+				Property: "prop1",
+			}},
+			target: Key{Ref: construct.PropertyRef{
+				Resource: graphtest.ParseId(t, "a:a:a"),
+				Property: "prop2",
+			}},
+		},
+		{
+			name: "add edge with missing source",
+			initialState: []Vertex{
+				&propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "a:a:a"),
+						Property: "prop2",
+					},
+				},
+			},
+			source: Key{Ref: construct.PropertyRef{
+				Resource: graphtest.ParseId(t, "a:a:a"),
+				Property: "prop1",
+			}},
+			target: Key{Ref: construct.PropertyRef{
+				Resource: graphtest.ParseId(t, "a:a:a"),
+				Property: "prop2",
+			}},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			testSol := enginetesting.NewTestSolution()
+			eval := NewEvaluator(testSol)
+			for _, v := range tt.initialState {
+				err := eval.graph.AddVertex(v)
+				assert.NoError(err)
+				err = eval.unevaluated.AddVertex(v)
+				assert.NoError(err)
+			}
+			err := eval.addEdge(tt.source, tt.target)
+			if tt.wantErr {
+				assert.Error(err)
+				return
+			}
+			assert.NoError(err)
+			_, err = eval.graph.Edge(tt.source, tt.target)
+			assert.NoError(err)
+			_, err = eval.unevaluated.Edge(tt.source, tt.target)
+			assert.NoError(err)
+		})
+	}
+}
+
+func TestEvaluator_enqueue(t *testing.T) {
+	tests := []struct {
+		name         string
+		changes      graphChanges
+		initialState []Vertex
+		want         map[Key][]Key
+		wantErr      bool
+	}{
+		{
+			name: "simple enqueue",
+			initialState: []Vertex{
+				&propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "a:a:a"),
+						Property: "prop2",
+					},
+				},
+			},
+			changes: graphChanges{
+				nodes: map[Key]Vertex{
+					{Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "a:a:a"),
+						Property: "prop1",
+					}}: &propertyVertex{
+						Ref: construct.PropertyRef{
+							Resource: graphtest.ParseId(t, "a:a:a"),
+							Property: "prop1",
+						},
+					},
+				},
+				edges: map[Key]set.Set[Key]{
+
+					{Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "a:a:a"),
+						Property: "prop1",
+					}}: set.SetOf[Key](
+						Key{Ref: construct.PropertyRef{
+							Resource: graphtest.ParseId(t, "a:a:a"),
+							Property: "prop2",
+						}},
+					),
+				},
+			},
+			want: map[Key][]Key{
+				{Ref: construct.PropertyRef{
+					Resource: graphtest.ParseId(t, "a:a:a"),
+					Property: "prop1",
+				}}: []Key{
+					{Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "a:a:a"),
+						Property: "prop2",
+					}},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			testSol := enginetesting.NewTestSolution()
+			eval := NewEvaluator(testSol)
+			for _, v := range tt.initialState {
+				err := eval.graph.AddVertex(v)
+				assert.NoError(err)
+			}
+			err := eval.enqueue(tt.changes)
+			if tt.wantErr {
+				assert.Error(err)
+				return
+			}
+			assert.NoError(err)
+			adj, err := eval.graph.AdjacencyMap()
+			assert.NoError(err)
+			for k, v := range tt.want {
+				p := adj[k]
+				for _, dep := range v {
+					assert.Contains(p, dep)
+				}
+			}
+		})
+	}
+}
+
+func TestEvaluator_UpdateId(t *testing.T) {
+
+	tests := []struct {
+		name         string
+		initialGraph []any
+		initialState []Vertex
+		oldId        construct.ResourceId
+		newId        construct.ResourceId
+		want         map[Key]Vertex
+		wantGraph    []any
+		wantErr      bool
+	}{
+		{
+			name:         "simple update with property vertex",
+			initialGraph: []any{"a:a:a"},
+			initialState: []Vertex{
+				&propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "a:a:a"),
+						Property: "prop1",
+					},
+				},
+			},
+			oldId: graphtest.ParseId(t, "a:a:a"),
+			newId: graphtest.ParseId(t, "b:b:b"),
+			want: map[Key]Vertex{
+				{Ref: construct.PropertyRef{
+					Resource: graphtest.ParseId(t, "b:b:b"),
+					Property: "prop1",
+				}}: &propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "b:b:b"),
+						Property: "prop1",
+					},
+				},
+			},
+			wantGraph: []any{"b:b:b"},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			testSol := enginetesting.NewTestSolution()
+			testSol.KB.On("GetResourceTemplate", mock.Anything).Return(&knowledgebase.ResourceTemplate{}, nil)
+			newG := graphtest.MakeGraph(t, construct.NewGraph(), tt.initialGraph...)
+			err := testSol.RawView().AddVerticesFrom(newG)
+			assert.NoError(err)
+			err = testSol.RawView().AddVerticesFrom(newG)
+			assert.NoError(err)
+			eval := NewEvaluator(testSol)
+			for _, v := range tt.initialState {
+				err := eval.graph.AddVertex(v)
+				assert.NoError(err)
+			}
+			err = eval.UpdateId(tt.oldId, tt.newId)
+			if tt.wantErr {
+				assert.Error(err)
+				return
+			}
+			assert.NoError(err)
+			adj, err := eval.graph.AdjacencyMap()
+			assert.NoError(err)
+			assert.Equal(len(adj), len(tt.want))
+			for k, v := range tt.want {
+				actual, err := eval.graph.Vertex(k)
+				assert.NoError(err)
+				assert.Equal(v, actual)
+			}
+			wantGraph := graphtest.MakeGraph(t, construct.NewGraph(), tt.wantGraph...)
+			graphtest.AssertGraphEqual(t, testSol.RawView(), wantGraph, "graph")
+		})
+	}
+}
diff --git a/pkg/engine2/operational_eval/graph_test.go b/pkg/engine2/operational_eval/graph_test.go
new file mode 100644
index 00000000..3d8015aa
--- /dev/null
+++ b/pkg/engine2/operational_eval/graph_test.go
@@ -0,0 +1,178 @@
+package operational_eval
+
+import (
+	"testing"
+
+	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/construct2/graphtest"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
+	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
+	"github.com/klothoplatform/klotho/pkg/knowledge_base2/properties"
+	"github.com/klothoplatform/klotho/pkg/set"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEvaluator_resourceVertices(t *testing.T) {
+	tests := []struct {
+		name    string
+		res     *construct.Resource
+		tmpl    *knowledgebase.ResourceTemplate
+		want    graphChanges
+		wantErr bool
+	}{
+		{
+			name: "simple resource",
+			res: &construct.Resource{
+				ID: graphtest.ParseId(t, "a:a:a"),
+				Properties: map[string]any{
+					"prop1": "value1",
+				},
+			},
+			tmpl: &knowledgebase.ResourceTemplate{
+				Properties: map[string]knowledgebase.Property{
+					"prop1": &properties.StringProperty{
+						PropertyDetails: knowledgebase.PropertyDetails{
+							Path: "prop1",
+						},
+					},
+				},
+			},
+			want: graphChanges{
+				nodes: map[Key]Vertex{
+					{Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "a:a:a"),
+						Property: "prop1",
+					}}: &propertyVertex{
+						Ref: construct.PropertyRef{
+							Resource: graphtest.ParseId(t, "a:a:a"),
+							Property: "prop1",
+						},
+						Template: &properties.StringProperty{
+							PropertyDetails: knowledgebase.PropertyDetails{
+								Path: "prop1",
+							},
+						},
+						EdgeRules: map[construct.SimpleEdge][]knowledgebase.OperationalRule{},
+					},
+				},
+				edges: map[Key]set.Set[Key]{},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			testSol := enginetesting.NewTestSolution()
+			err := testSol.DataflowGraph().AddVertex(tt.res)
+			assert.NoError(err)
+			eval := NewEvaluator(testSol)
+			actual, err := eval.resourceVertices(tt.res, tt.tmpl)
+			if tt.wantErr {
+				assert.Error(err)
+				return
+			}
+			assert.NoError(err)
+			assert.Equal(tt.want, actual)
+		})
+	}
+}
+
+func TestEvaluator_RemoveEdge(t *testing.T) {
+	tests := []struct {
+		name         string
+		initialState []Vertex
+		source       construct.ResourceId
+		target       construct.ResourceId
+		want         map[Key]Vertex
+		wantErr      bool
+	}{
+		{
+			name: "remove edge",
+			initialState: []Vertex{
+				&edgeVertex{
+					Edge: construct.SimpleEdge{
+						Source: graphtest.ParseId(t, "a:a:a"),
+						Target: graphtest.ParseId(t, "a:a:b"),
+					},
+				},
+			},
+			source: graphtest.ParseId(t, "a:a:a"),
+			target: graphtest.ParseId(t, "a:a:b"),
+			want:   map[Key]Vertex{},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			testSol := enginetesting.NewTestSolution()
+			eval := NewEvaluator(testSol)
+			for _, v := range tt.initialState {
+				err := eval.graph.AddVertex(v)
+				assert.NoError(err)
+			}
+			err := eval.RemoveEdge(tt.source, tt.target)
+			if tt.wantErr {
+				assert.Error(err)
+				return
+			}
+			assert.NoError(err)
+			adj, err := eval.graph.AdjacencyMap()
+			assert.NoError(err)
+			assert.Equal(len(adj), len(tt.want))
+			for k, v := range tt.want {
+				actual, err := eval.graph.Vertex(k)
+				assert.NoError(err)
+				assert.Equal(v, actual)
+			}
+		})
+	}
+}
+
+func TestEvaluator_RemoveResource(t *testing.T) {
+	tests := []struct {
+		name         string
+		initialState []Vertex
+		id           construct.ResourceId
+		want         map[Key]Vertex
+		wantErr      bool
+	}{
+		{
+			name: "remove resource property vertex",
+			initialState: []Vertex{
+				&propertyVertex{
+					Ref: construct.PropertyRef{
+						Resource: graphtest.ParseId(t, "a:a:a"),
+						Property: "prop1",
+					},
+				},
+			},
+			id:   graphtest.ParseId(t, "a:a:a"),
+			want: map[Key]Vertex{},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			testSol := enginetesting.NewTestSolution()
+			eval := NewEvaluator(testSol)
+			for _, v := range tt.initialState {
+				err := eval.graph.AddVertex(v)
+				assert.NoError(err)
+			}
+			err := eval.RemoveResource(tt.id)
+			if tt.wantErr {
+				assert.Error(err)
+				return
+			}
+			assert.NoError(err)
+			adj, err := eval.graph.AdjacencyMap()
+			assert.NoError(err)
+			assert.Equal(len(adj), len(tt.want))
+			for k, v := range tt.want {
+				actual, err := eval.graph.Vertex(k)
+				assert.NoError(err)
+				assert.Equal(v, actual)
+			}
+		})
+	}
+}
diff --git a/pkg/engine2/path_selection/candidate_validity_test.go b/pkg/engine2/path_selection/candidate_validity_test.go
new file mode 100644
index 00000000..252c0052
--- /dev/null
+++ b/pkg/engine2/path_selection/candidate_validity_test.go
@@ -0,0 +1,419 @@
+package path_selection
+
+import (
+	"testing"
+
+	"github.com/klothoplatform/klotho/pkg/construct2/graphtest"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
+	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_checkAsTargetValidity(t *testing.T) {
+	type testResource struct {
+		id    string
+		props map[string]string
+	}
+	type args struct {
+		graph          []any
+		kb             func(t *testing.T, kb *enginetesting.MockKB)
+		resource       testResource
+		source         string
+		classification string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    bool
+		wantErr bool
+	}{
+		{
+			name: "no path satisfaction rules",
+			args: args{
+				graph: []any{"p:a:a -> p:c:c"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:c:c"},
+				source:         "p:a:a",
+				classification: "network",
+			},
+			want: true,
+		},
+		{
+			name: "simple as target matches",
+			args: args{
+				graph: []any{"p:a:a -> p:c:c"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{
+						PathSatisfaction: knowledgebase.PathSatisfaction{
+							AsTarget: []knowledgebase.PathSatisfactionRoute{
+								{
+									Classification: "network",
+									Validity:       knowledgebase.DownstreamOperation,
+								},
+							},
+						},
+					}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:c:c"},
+				source:         "p:a:a",
+				classification: "network",
+			},
+			want: true,
+		},
+		{
+			name: "target does not match",
+			args: args{
+				graph: []any{"p:a:a", "p:c:c"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{
+						PathSatisfaction: knowledgebase.PathSatisfaction{
+							AsTarget: []knowledgebase.PathSatisfactionRoute{
+								{
+									Classification: "network",
+									Validity:       knowledgebase.DownstreamOperation,
+								},
+							},
+						},
+					}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:c:c"},
+				source:         "p:a:a",
+				classification: "network",
+			},
+			want: false,
+		},
+		{
+			name: "property ref no value",
+			args: args{
+				graph: []any{"p:a:a -> p:c:c"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{
+						PathSatisfaction: knowledgebase.PathSatisfaction{
+							AsTarget: []knowledgebase.PathSatisfactionRoute{
+								{
+									Classification:    "network",
+									PropertyReference: "X",
+									Validity:          knowledgebase.DownstreamOperation,
+								},
+							},
+						},
+					}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:c:c"},
+				source:         "p:a:a",
+				classification: "network",
+			},
+			want: true,
+		},
+		{
+			name: "property ref valid value",
+			args: args{
+				graph: []any{"p:a:a -> p:c:c", "p:a:a -> p:x:x"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{
+						PathSatisfaction: knowledgebase.PathSatisfaction{
+							AsTarget: []knowledgebase.PathSatisfactionRoute{
+								{
+									Classification:    "network",
+									PropertyReference: "X",
+									Validity:          knowledgebase.DownstreamOperation,
+								},
+							},
+						},
+					}, nil)
+					kb.On("GetResourceTemplate", matches("p:x")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:x")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:c:c", props: map[string]string{"X": "p:x:x"}},
+				source:         "p:a:a",
+				classification: "network",
+			},
+			want: true,
+		},
+		{
+			name: "property ref invalid value",
+			args: args{
+				graph: []any{"p:a:a -> p:c:c", "p:x:x"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{
+						PathSatisfaction: knowledgebase.PathSatisfaction{
+							AsTarget: []knowledgebase.PathSatisfactionRoute{
+								{
+									Classification:    "network",
+									PropertyReference: "X",
+									Validity:          knowledgebase.DownstreamOperation,
+								},
+							},
+						},
+					}, nil)
+					kb.On("GetResourceTemplate", matches("p:x")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:c:c", props: map[string]string{"X": "p:x:x"}},
+				source:         "p:a:a",
+				classification: "network",
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			sol := enginetesting.NewTestSolution()
+			sol.KB.Test(t)
+			tt.args.kb(t, &sol.KB)
+			sol.LoadState(t, tt.args.graph...)
+
+			res, err := sol.RawView().Vertex(graphtest.ParseId(t, tt.args.resource.id))
+			require.NoError(t, err)
+			for k, v := range tt.args.resource.props {
+				require.NoError(t, res.SetProperty(k, graphtest.ParseId(t, v)))
+			}
+
+			src := graphtest.ParseId(t, tt.args.source)
+
+			got, err := checkAsTargetValidity(sol, res, src, tt.args.classification)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func Test_checkAsSourceValidity(t *testing.T) {
+	type testResource struct {
+		id    string
+		props map[string]string
+	}
+	type args struct {
+		graph          []any
+		kb             func(t *testing.T, kb *enginetesting.MockKB)
+		resource       testResource
+		source         string
+		classification string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    bool
+		wantErr bool
+	}{
+		{
+			name: "no path satisfaction rules",
+			args: args{
+				graph: []any{"p:a:a -> p:c:c"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:c:c"},
+				source:         "p:a:a",
+				classification: "network",
+			},
+			want: true,
+		},
+		{
+			name: "simple as source matches",
+			args: args{
+				graph: []any{"p:a:a -> p:c:c"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{
+						PathSatisfaction: knowledgebase.PathSatisfaction{
+							AsSource: []knowledgebase.PathSatisfactionRoute{
+								{
+									Classification: "network",
+									Validity:       knowledgebase.DownstreamOperation,
+								},
+							},
+						},
+					}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:a:a"},
+				source:         "p:c:c",
+				classification: "network",
+			},
+			want: true,
+		},
+		{
+			name: "source does not match",
+			args: args{
+				graph: []any{"p:a:a", "p:c:c"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{
+						PathSatisfaction: knowledgebase.PathSatisfaction{
+							AsSource: []knowledgebase.PathSatisfactionRoute{
+								{
+									Classification: "network",
+									Validity:       knowledgebase.DownstreamOperation,
+								},
+							},
+						},
+					}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:a:a"},
+				source:         "p:c:c",
+				classification: "network",
+			},
+			want: false,
+		},
+		{
+			name: "property ref no value",
+			args: args{
+				graph: []any{"p:a:a -> p:c:c"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{
+						PathSatisfaction: knowledgebase.PathSatisfaction{
+							AsSource: []knowledgebase.PathSatisfactionRoute{
+								{
+									Classification:    "network",
+									PropertyReference: "X",
+									Validity:          knowledgebase.DownstreamOperation,
+								},
+							},
+						},
+					}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:a:a"},
+				source:         "p:c:c",
+				classification: "network",
+			},
+			want: true,
+		},
+		{
+			name: "property ref valid value",
+			args: args{
+				graph: []any{"p:a:a -> p:c:c", "p:x:x -> p:c:c"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{
+						PathSatisfaction: knowledgebase.PathSatisfaction{
+							AsSource: []knowledgebase.PathSatisfactionRoute{
+								{
+									Classification:    "network",
+									PropertyReference: "X",
+									Validity:          knowledgebase.DownstreamOperation,
+								},
+							},
+						},
+					}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetResourceTemplate", matches("p:x")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+					kb.On("GetEdgeTemplate", matches("p:x"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:a:a", props: map[string]string{"X": "p:x:x"}},
+				source:         "p:c:c",
+				classification: "network",
+			},
+			want: true,
+		},
+		{
+			name: "property ref invalid value",
+			args: args{
+				graph: []any{"p:a:a -> p:c:c", "p:x:x"},
+				kb: func(t *testing.T, kb *enginetesting.MockKB) {
+					matches := func(id string) any {
+						return mock.MatchedBy(graphtest.ParseId(t, id).Matches)
+					}
+					kb.On("GetResourceTemplate", matches("p:a")).Return(&knowledgebase.ResourceTemplate{
+						PathSatisfaction: knowledgebase.PathSatisfaction{
+							AsSource: []knowledgebase.PathSatisfactionRoute{
+								{
+									Classification:    "network",
+									PropertyReference: "X",
+									Validity:          knowledgebase.DownstreamOperation,
+								},
+							},
+						},
+					}, nil)
+					kb.On("GetResourceTemplate", matches("p:c")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetResourceTemplate", matches("p:x")).Return(&knowledgebase.ResourceTemplate{}, nil)
+					kb.On("GetEdgeTemplate", matches("p:a"), matches("p:c")).Return(&knowledgebase.EdgeTemplate{})
+				},
+				resource:       testResource{id: "p:a:a", props: map[string]string{"X": "p:x:x"}},
+				source:         "p:c:c",
+				classification: "network",
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			sol := enginetesting.NewTestSolution()
+			sol.KB.Test(t)
+			tt.args.kb(t, &sol.KB)
+			sol.LoadState(t, tt.args.graph...)
+
+			res, err := sol.RawView().Vertex(graphtest.ParseId(t, tt.args.resource.id))
+			require.NoError(t, err)
+			for k, v := range tt.args.resource.props {
+				require.NoError(t, res.SetProperty(k, graphtest.ParseId(t, v)))
+			}
+
+			src := graphtest.ParseId(t, tt.args.source)
+
+			got, err := checkAsSourceValidity(sol, res, src, tt.args.classification)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/pkg/engine2/path_selection/candidate_weight_test.go b/pkg/engine2/path_selection/candidate_weight_test.go
index eb87353a..4d1bd3ac 100644
--- a/pkg/engine2/path_selection/candidate_weight_test.go
+++ b/pkg/engine2/path_selection/candidate_weight_test.go
@@ -43,7 +43,7 @@ func Test_determineCandidateWeight(t *testing.T) {
 			wantWeight:  11,
 		},
 		{
-			name:       "downstream direct",
+			name:       "downstream direct / upstream indirect",
 			graph:      []any{"p:compute:a -> p:glue:b -> p:glue:c -> p:compute:d"},
 			src:        "p:compute:a",
 			target:     "p:compute:d",
@@ -58,6 +58,14 @@ func Test_determineCandidateWeight(t *testing.T) {
 			id:         "p:glue:c",
 			wantWeight: 15,
 		},
+		{
+			name:       "upstream direct",
+			graph:      []any{"p:compute:a -> p:glue:b -> p:glue:c -> p:compute:d"},
+			src:        "p:compute:a",
+			target:     "p:compute:d",
+			id:         "p:glue:c",
+			wantWeight: 20,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/engine2/testdata/2_routes.dataflow-viz.yaml b/pkg/engine2/testdata/2_routes.dataflow-viz.yaml
index c36389dc..32224f81 100755
--- a/pkg/engine2/testdata/2_routes.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/2_routes.dataflow-viz.yaml
@@ -3,14 +3,14 @@ resources:
   lambda_function/lambda_function_1:
     children:
         - aws:ecr_image:lambda_function_1-image
-        - aws:ecr_repo:ecr_repo-0
+        - aws:ecr_repo:lambda_function_1-image-ecr_repo
         - aws:iam_role:lambda_function_1-ExecutionRole
     tag: big
 
   lambda_function/lambda_function_0:
     children:
         - aws:ecr_image:lambda_function_0-image
-        - aws:ecr_repo:ecr_repo-0
+        - aws:ecr_repo:lambda_function_0-image-ecr_repo
         - aws:iam_role:lambda_function_0-ExecutionRole
     tag: big
 
diff --git a/pkg/engine2/testdata/2_routes.expect.yaml b/pkg/engine2/testdata/2_routes.expect.yaml
index 34411a19..74573c74 100755
--- a/pkg/engine2/testdata/2_routes.expect.yaml
+++ b/pkg/engine2/testdata/2_routes.expect.yaml
@@ -89,7 +89,7 @@ resources:
     aws:ecr_image:lambda_function_0-image:
         Context: .
         Dockerfile: lambda_function_0-image.Dockerfile
-        Repo: aws:ecr_repo:ecr_repo-0
+        Repo: aws:ecr_repo:lambda_function_0-image-ecr_repo
     aws:iam_role:lambda_function_0-ExecutionRole:
         AssumeRolePolicyDoc:
             Statement:
@@ -106,7 +106,7 @@ resources:
     aws:ecr_image:lambda_function_1-image:
         Context: .
         Dockerfile: lambda_function_1-image.Dockerfile
-        Repo: aws:ecr_repo:ecr_repo-0
+        Repo: aws:ecr_repo:lambda_function_1-image-ecr_repo
     aws:iam_role:lambda_function_1-ExecutionRole:
         AssumeRolePolicyDoc:
             Statement:
@@ -119,10 +119,12 @@ resources:
             Version: "2012-10-17"
         ManagedPolicies:
             - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+    aws:ecr_repo:lambda_function_0-image-ecr_repo:
+        ForceDelete: true
     aws:log_group:lambda_function_0-log-group:
         LogGroupName: /aws/lambda/lambda_function_0
         RetentionInDays: 5
-    aws:ecr_repo:ecr_repo-0:
+    aws:ecr_repo:lambda_function_1-image-ecr_repo:
         ForceDelete: true
     aws:log_group:lambda_function_1-log-group:
         LogGroupName: /aws/lambda/lambda_function_1
@@ -161,9 +163,9 @@ edges:
     aws:lambda_function:lambda_function_1 -> aws:SERVICE_API:lambda_function_0-lambda_function_0-log-group:
     aws:lambda_function:lambda_function_1 -> aws:ecr_image:lambda_function_1-image:
     aws:lambda_function:lambda_function_1 -> aws:iam_role:lambda_function_1-ExecutionRole:
-    aws:ecr_image:lambda_function_0-image -> aws:ecr_repo:ecr_repo-0:
+    aws:ecr_image:lambda_function_0-image -> aws:ecr_repo:lambda_function_0-image-ecr_repo:
     aws:iam_role:lambda_function_0-ExecutionRole -> aws:log_group:lambda_function_0-log-group:
     aws:SERVICE_API:lambda_function_0-lambda_function_0-log-group -> aws:log_group:lambda_function_0-log-group:
     aws:SERVICE_API:lambda_function_0-lambda_function_0-log-group -> aws:log_group:lambda_function_1-log-group:
-    aws:ecr_image:lambda_function_1-image -> aws:ecr_repo:ecr_repo-0:
+    aws:ecr_image:lambda_function_1-image -> aws:ecr_repo:lambda_function_1-image-ecr_repo:
     aws:iam_role:lambda_function_1-ExecutionRole -> aws:log_group:lambda_function_1-log-group:
diff --git a/pkg/engine2/testdata/2_routes.iac-viz.yaml b/pkg/engine2/testdata/2_routes.iac-viz.yaml
index 8a387a88..8a1a0925 100755
--- a/pkg/engine2/testdata/2_routes.iac-viz.yaml
+++ b/pkg/engine2/testdata/2_routes.iac-viz.yaml
@@ -2,10 +2,12 @@ provider: aws
 resources:
   rest_api/rest_api_1:
 
-  ecr_repo/ecr_repo-0:
+  ecr_repo/lambda_function_0-image-ecr_repo:
 
   log_group/lambda_function_0-log-group:
 
+  ecr_repo/lambda_function_1-image-ecr_repo:
+
   log_group/lambda_function_1-log-group:
 
   aws:api_resource:rest_api_1/lambda0:
@@ -13,7 +15,7 @@ resources:
   aws:api_resource:rest_api_1/lambda0 -> rest_api/rest_api_1:
   ecr_image/lambda_function_0-image:
 
-  ecr_image/lambda_function_0-image -> ecr_repo/ecr_repo-0:
+  ecr_image/lambda_function_0-image -> ecr_repo/lambda_function_0-image-ecr_repo:
   iam_role/lambda_function_0-executionrole:
 
   iam_role/lambda_function_0-executionrole -> log_group/lambda_function_0-log-group:
@@ -22,7 +24,7 @@ resources:
   aws:api_resource:rest_api_1/lambda1 -> rest_api/rest_api_1:
   ecr_image/lambda_function_1-image:
 
-  ecr_image/lambda_function_1-image -> ecr_repo/ecr_repo-0:
+  ecr_image/lambda_function_1-image -> ecr_repo/lambda_function_1-image-ecr_repo:
   iam_role/lambda_function_1-executionrole:
 
   iam_role/lambda_function_1-executionrole -> log_group/lambda_function_1-log-group:
diff --git a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
index 2e18e294..a0fe5394 100755
--- a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
@@ -24,7 +24,7 @@ resources:
   ecs_service/ecs_service_0:
     children:
         - aws:ecr_image:ecs_service_0-ecs_service_0
-        - aws:ecr_repo:ecr_repo-0
+        - aws:ecr_repo:ecs_service_0-ecs_service_0-ecr_repo
         - aws:ecs_task_definition:ecs_service_0
         - aws:iam_role:ecs_service_0-execution-role
         - aws:log_group:ecs_service_0-log-group
diff --git a/pkg/engine2/testdata/ecs_rds.expect.yaml b/pkg/engine2/testdata/ecs_rds.expect.yaml
index c0123f6b..83d6e2d6 100755
--- a/pkg/engine2/testdata/ecs_rds.expect.yaml
+++ b/pkg/engine2/testdata/ecs_rds.expect.yaml
@@ -63,7 +63,7 @@ resources:
     aws:ecr_image:ecs_service_0-ecs_service_0:
         Context: .
         Dockerfile: ecs_service_0-ecs_service_0.Dockerfile
-        Repo: aws:ecr_repo:ecr_repo-0
+        Repo: aws:ecr_repo:ecs_service_0-ecs_service_0-ecr_repo
     aws:iam_role:ecs_service_0-execution-role:
         AssumeRolePolicyDoc:
             Statement:
@@ -89,7 +89,7 @@ resources:
     aws:log_group:ecs_service_0-log-group:
         LogGroupName: /aws/ecs/ecs_service_0
         RetentionInDays: 5
-    aws:ecr_repo:ecr_repo-0:
+    aws:ecr_repo:ecs_service_0-ecs_service_0-ecr_repo:
         ForceDelete: true
     aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip:
     aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip:
@@ -224,7 +224,7 @@ edges:
     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:ecr_image:ecs_service_0-ecs_service_0 -> aws:ecr_repo:ecr_repo-0:
+    aws:ecr_image:ecs_service_0-ecs_service_0 -> aws:ecr_repo:ecs_service_0-ecs_service_0-ecr_repo:
     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 df8250f5..415536c1 100755
--- a/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
+++ b/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
@@ -39,7 +39,7 @@ resources:
 
   aws:subnet:vpc-0/subnet-2 -> aws:availability_zone:region-0/availability_zone-0:
   aws:subnet:vpc-0/subnet-2 -> vpc/vpc-0:
-  ecr_repo/ecr_repo-0:
+  ecr_repo/ecs_service_0-ecs_service_0-ecr_repo:
 
   rds_instance/rds-instance-2:
 
@@ -58,7 +58,7 @@ resources:
   aws:nat_gateway:subnet-2/subnet-0-route_table-nat_gateway -> aws:subnet:vpc-0/subnet-2:
   ecr_image/ecs_service_0-ecs_service_0:
 
-  ecr_image/ecs_service_0-ecs_service_0 -> ecr_repo/ecr_repo-0:
+  ecr_image/ecs_service_0-ecs_service_0 -> ecr_repo/ecs_service_0-ecs_service_0-ecr_repo:
   iam_role/ecs_service_0-execution-role:
 
   iam_role/ecs_service_0-execution-role -> rds_instance/rds-instance-2:
diff --git a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
index a4b4ad68..5847e315 100755
--- a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
@@ -3,7 +3,7 @@ resources:
   kubernetes:pod:eks_cluster-0/pod2:
     children:
         - aws:ecr_image:pod2-ecr_image
-        - aws:ecr_repo:ecr_repo-0
+        - aws:ecr_repo:pod2-ecr_image-ecr_repo
         - aws:iam_role:ClusterRole-eks_cluster-0
         - kubernetes:service_account:eks_cluster-0:pod2
     parent: eks_cluster/eks_cluster-0
diff --git a/pkg/engine2/testdata/k8s_api.expect.yaml b/pkg/engine2/testdata/k8s_api.expect.yaml
index 5e5e0ba3..13904dd3 100755
--- a/pkg/engine2/testdata/k8s_api.expect.yaml
+++ b/pkg/engine2/testdata/k8s_api.expect.yaml
@@ -223,7 +223,7 @@ resources:
     aws:ecr_image:pod2-ecr_image:
         Context: .
         Dockerfile: pod2-ecr_image.Dockerfile
-        Repo: aws:ecr_repo:ecr_repo-0
+        Repo: aws:ecr_repo:pod2-ecr_image-ecr_repo
     aws:eks_node_group:eks_node_group-0:
         AmiType: AL2_x86_64
         Cluster: aws:eks_cluster:eks_cluster-0
@@ -254,7 +254,7 @@ resources:
     aws:iam_role_policy_attachment:aws-load-balancer-controller-iam_policy-0:
         Policy: aws:iam_policy:iam_policy-0
         Role: aws:iam_role:aws-load-balancer-controller
-    aws:ecr_repo:ecr_repo-0:
+    aws:ecr_repo:pod2-ecr_image-ecr_repo:
         ForceDelete: true
     aws:iam_role:eks_node_group-0-iam_role:
         AssumeRolePolicyDoc:
@@ -484,12 +484,6 @@ resources:
               Protocol: "-1"
               ToPort: 0
         IngressRules:
-            - CidrBlocks:
-                - 10.0.192.0/18
-              Description: Allow ingress traffic from ip addresses within the subnet subnet-1
-              FromPort: 0
-              Protocol: "-1"
-              ToPort: 0
             - CidrBlocks:
                 - 0.0.0.0/0
               Description: Allows ingress traffic from the EKS control plane
@@ -507,6 +501,12 @@ resources:
               FromPort: 0
               Protocol: "-1"
               ToPort: 0
+            - CidrBlocks:
+                - 10.0.192.0/18
+              Description: Allow ingress traffic from ip addresses within the subnet subnet-1
+              FromPort: 0
+              Protocol: "-1"
+              ToPort: 0
         Vpc: aws:vpc:vpc-0
     aws:route_table:vpc-0:subnet-0-route_table:
         Routes:
@@ -615,7 +615,7 @@ edges:
     kubernetes:pod:eks_cluster-0:pod2 -> kubernetes:service_account:eks_cluster-0:pod2:
     aws:iam_role:aws-load-balancer-controller -> aws:iam_oidc_provider:eks_cluster-0:
     aws:iam_role:aws-load-balancer-controller -> aws:iam_role_policy_attachment:aws-load-balancer-controller-iam_policy-0:
-    aws:ecr_image:pod2-ecr_image -> aws:ecr_repo:ecr_repo-0:
+    aws:ecr_image:pod2-ecr_image -> aws:ecr_repo:pod2-ecr_image-ecr_repo:
     aws:eks_node_group:eks_node_group-0 -> aws:eks_cluster:eks_cluster-0:
     aws:eks_node_group:eks_node_group-0 -> aws:iam_role:eks_node_group-0-iam_role:
     aws:eks_node_group:eks_node_group-0 -> aws:subnet:vpc-0:subnet-0:
diff --git a/pkg/engine2/testdata/k8s_api.iac-viz.yaml b/pkg/engine2/testdata/k8s_api.iac-viz.yaml
index 5ff3256d..38d95a66 100755
--- a/pkg/engine2/testdata/k8s_api.iac-viz.yaml
+++ b/pkg/engine2/testdata/k8s_api.iac-viz.yaml
@@ -38,7 +38,7 @@ resources:
   iam_oidc_provider/eks_cluster-0 -> region/region-0:
   rest_api/rest_api_4:
 
-  ecr_repo/ecr_repo-0:
+  ecr_repo/pod2-ecr_image-ecr_repo:
 
   iam_role/eks_node_group-0-iam_role:
 
@@ -57,7 +57,7 @@ resources:
   iam_role/aws-load-balancer-controller -> iam_oidc_provider/eks_cluster-0:
   ecr_image/pod2-ecr_image:
 
-  ecr_image/pod2-ecr_image -> ecr_repo/ecr_repo-0:
+  ecr_image/pod2-ecr_image -> ecr_repo/pod2-ecr_image-ecr_repo:
   eks_node_group/eks_node_group-0:
 
   eks_node_group/eks_node_group-0 -> eks_cluster/eks_cluster-0:
diff --git a/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml b/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml
index 1180a050..cbabee5e 100755
--- a/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml
@@ -26,7 +26,7 @@ resources:
   lambda_function/lambda_test_app:
     children:
         - aws:ecr_image:lambda_test_app-image
-        - aws:ecr_repo:ecr_repo-0
+        - aws:ecr_repo:lambda_test_app-image-ecr_repo
         - aws:iam_role:lambda_test_app-ExecutionRole
     parent: vpc/vpc-0
     tag: big
diff --git a/pkg/engine2/testdata/lambda_efs.expect.yaml b/pkg/engine2/testdata/lambda_efs.expect.yaml
index 5f816909..ed8cacb0 100755
--- a/pkg/engine2/testdata/lambda_efs.expect.yaml
+++ b/pkg/engine2/testdata/lambda_efs.expect.yaml
@@ -29,7 +29,7 @@ resources:
     aws:ecr_image:lambda_test_app-image:
         Context: .
         Dockerfile: lambda_test_app-image.Dockerfile
-        Repo: aws:ecr_repo:ecr_repo-0
+        Repo: aws:ecr_repo:lambda_test_app-image-ecr_repo
     aws:efs_access_point:test-efs-fs:lambda_test_app-test-efs-fs:
         FileSystem: aws:efs_file_system:test-efs-fs
         PosixUser:
@@ -62,9 +62,9 @@ resources:
                         - aws:efs_file_system:test-efs-fs#Arn
                 Version: "2012-10-17"
         ManagedPolicies:
-            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
             - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
-    aws:ecr_repo:ecr_repo-0:
+            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+    aws:ecr_repo:lambda_test_app-image-ecr_repo:
         ForceDelete: true
     aws:efs_mount_target:test-efs-fs:lambda_test_app-test-efs-fs:
         FileSystem: aws:efs_file_system:test-efs-fs
@@ -145,12 +145,6 @@ resources:
               Protocol: "-1"
               ToPort: 0
         IngressRules:
-            - CidrBlocks:
-                - 10.0.128.0/18
-              Description: Allow ingress traffic from ip addresses within the subnet subnet-1
-              FromPort: 0
-              Protocol: "-1"
-              ToPort: 0
             - CidrBlocks:
                 - 10.0.192.0/18
               Description: Allow ingress traffic from ip addresses within the subnet lambda_test_app-test-efs-fs
@@ -162,6 +156,12 @@ resources:
               Protocol: "-1"
               Self: true
               ToPort: 0
+            - CidrBlocks:
+                - 10.0.128.0/18
+              Description: Allow ingress traffic from ip addresses within the subnet subnet-1
+              FromPort: 0
+              Protocol: "-1"
+              ToPort: 0
         Vpc: aws:vpc:vpc-0
     aws:security_group:vpc-0:subnet-1-test-efs-fs:
         EgressRules:
@@ -223,7 +223,7 @@ edges:
     aws:lambda_function:lambda_test_app -> aws:iam_role:lambda_test_app-ExecutionRole:
     aws:lambda_function:lambda_test_app -> aws:subnet:vpc-0:lambda_test_app-test-efs-fs:
     aws:lambda_function:lambda_test_app -> aws:subnet:vpc-0:subnet-1:
-    aws:ecr_image:lambda_test_app-image -> aws:ecr_repo:ecr_repo-0:
+    aws:ecr_image:lambda_test_app-image -> aws:ecr_repo:lambda_test_app-image-ecr_repo:
     aws:efs_access_point:test-efs-fs:lambda_test_app-test-efs-fs -> aws:efs_file_system:test-efs-fs:
     aws:iam_role:lambda_test_app-ExecutionRole -> aws:efs_file_system:test-efs-fs:
     aws:iam_role:lambda_test_app-ExecutionRole -> aws:log_group:lambda_test_app-log-group:
diff --git a/pkg/engine2/testdata/lambda_efs.iac-viz.yaml b/pkg/engine2/testdata/lambda_efs.iac-viz.yaml
index 62e6bd8b..e289462a 100755
--- a/pkg/engine2/testdata/lambda_efs.iac-viz.yaml
+++ b/pkg/engine2/testdata/lambda_efs.iac-viz.yaml
@@ -39,7 +39,7 @@ resources:
 
   aws:nat_gateway:subnet-2/lambda_test_app-test-efs-fs-route_table-nat_gateway -> elastic_ip/lambda_test_app-test-efs-fs-route_table-nat_gateway-elastic_ip:
   aws:nat_gateway:subnet-2/lambda_test_app-test-efs-fs-route_table-nat_gateway -> aws:subnet:vpc-0/subnet-2:
-  ecr_repo/ecr_repo-0:
+  ecr_repo/lambda_test_app-image-ecr_repo:
 
   efs_file_system/test-efs-fs:
 
@@ -75,7 +75,7 @@ resources:
   aws:subnet:vpc-0/lambda_test_app-test-efs-fs -> vpc/vpc-0:
   ecr_image/lambda_test_app-image:
 
-  ecr_image/lambda_test_app-image -> ecr_repo/ecr_repo-0:
+  ecr_image/lambda_test_app-image -> ecr_repo/lambda_test_app-image-ecr_repo:
   aws:efs_access_point:test-efs-fs/lambda_test_app-test-efs-fs:
 
   aws:efs_access_point:test-efs-fs/lambda_test_app-test-efs-fs -> efs_file_system/test-efs-fs:
diff --git a/pkg/engine2/testdata/namespace_pathselect.dataflow-viz.yaml b/pkg/engine2/testdata/namespace_pathselect.dataflow-viz.yaml
index de979fd9..65602898 100755
--- a/pkg/engine2/testdata/namespace_pathselect.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/namespace_pathselect.dataflow-viz.yaml
@@ -17,7 +17,7 @@ resources:
   lambda_function/lambda_function_2:
     children:
         - aws:ecr_image:lambda_function_2-image
-        - aws:ecr_repo:ecr_repo-0
+        - aws:ecr_repo:lambda_function_2-image-ecr_repo
         - aws:iam_role:lambda_function_2-ExecutionRole
     parent: vpc/vpc_1
     tag: big
diff --git a/pkg/engine2/testdata/namespace_pathselect.expect.yaml b/pkg/engine2/testdata/namespace_pathselect.expect.yaml
index d86c6736..da226662 100755
--- a/pkg/engine2/testdata/namespace_pathselect.expect.yaml
+++ b/pkg/engine2/testdata/namespace_pathselect.expect.yaml
@@ -47,10 +47,12 @@ resources:
             - aws:subnet:vpc_1:lambda_function_2-vpc_1
             - aws:subnet:vpc_1:subnet-1
         Timeout: 180
+    aws:ecr_repo:ecr_repo-0:
+        ForceDelete: true
     aws:ecr_image:lambda_function_2-image:
         Context: .
         Dockerfile: lambda_function_2-image.Dockerfile
-        Repo: aws:ecr_repo:ecr_repo-0
+        Repo: aws:ecr_repo:lambda_function_2-image-ecr_repo
     aws:iam_role:lambda_function_2-ExecutionRole:
         AssumeRolePolicyDoc:
             Statement:
@@ -78,7 +80,7 @@ resources:
         RouteTable: aws:route_table:vpc_1:subnet-1-route_table
         Type: private
         Vpc: aws:vpc:vpc_1
-    aws:ecr_repo:ecr_repo-0:
+    aws:ecr_repo:lambda_function_2-image-ecr_repo:
         ForceDelete: true
     aws:route_table_association:lambda_function_2-vpc_1-lambda_function_2-vpc_1-route_table:
         RouteTable: aws:route_table:vpc_1:lambda_function_2-vpc_1-route_table
@@ -167,7 +169,7 @@ edges:
     aws:lambda_function:lambda_function_2 -> aws:iam_role:lambda_function_2-ExecutionRole:
     aws:lambda_function:lambda_function_2 -> aws:subnet:vpc_1:lambda_function_2-vpc_1:
     aws:lambda_function:lambda_function_2 -> aws:subnet:vpc_1:subnet-1:
-    aws:ecr_image:lambda_function_2-image -> aws:ecr_repo:ecr_repo-0:
+    aws:ecr_image:lambda_function_2-image -> aws:ecr_repo:lambda_function_2-image-ecr_repo:
     aws:iam_role:lambda_function_2-ExecutionRole -> aws:log_group:lambda_function_2-log-group:
     aws:subnet:vpc_1:lambda_function_2-vpc_1 -> aws:SERVICE_API:lambda_function_0-lambda_function_0-log-group:
     aws:subnet:vpc_1:lambda_function_2-vpc_1 -> aws:availability_zone:region-0:availability_zone-0:
diff --git a/pkg/engine2/testdata/namespace_pathselect.iac-viz.yaml b/pkg/engine2/testdata/namespace_pathselect.iac-viz.yaml
index 44aff518..660c6150 100755
--- a/pkg/engine2/testdata/namespace_pathselect.iac-viz.yaml
+++ b/pkg/engine2/testdata/namespace_pathselect.iac-viz.yaml
@@ -33,10 +33,12 @@ resources:
 
   aws:nat_gateway:subnet-2/lambda_function_2-vpc_1-route_table-nat_gateway -> elastic_ip/lambda_function_2-vpc_1-route_table-nat_gateway-elastic_ip:
   aws:nat_gateway:subnet-2/lambda_function_2-vpc_1-route_table-nat_gateway -> aws:subnet:vpc_1/subnet-2:
-  ecr_repo/ecr_repo-0:
+  ecr_repo/lambda_function_2-image-ecr_repo:
 
   log_group/lambda_function_2-log-group:
 
+  ecr_repo/ecr_repo-0:
+
   log_group/lambda_function_0-log-group:
 
   aws:route_table:vpc_1/subnet-3-route_table:
@@ -65,7 +67,7 @@ resources:
   aws:subnet:vpc_1/lambda_function_2-vpc_1 -> vpc/vpc_1:
   ecr_image/lambda_function_2-image:
 
-  ecr_image/lambda_function_2-image -> ecr_repo/ecr_repo-0:
+  ecr_image/lambda_function_2-image -> ecr_repo/lambda_function_2-image-ecr_repo:
   iam_role/lambda_function_2-executionrole:
 
   iam_role/lambda_function_2-executionrole -> log_group/lambda_function_2-log-group:
diff --git a/pkg/engine2/testdata/single_lambda.dataflow-viz.yaml b/pkg/engine2/testdata/single_lambda.dataflow-viz.yaml
index f96c54cf..98ab075c 100755
--- a/pkg/engine2/testdata/single_lambda.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/single_lambda.dataflow-viz.yaml
@@ -3,7 +3,7 @@ resources:
   lambda_function/lambda_function_0:
     children:
         - aws:ecr_image:lambda_function_0-image
-        - aws:ecr_repo:ecr_repo-0
+        - aws:ecr_repo:lambda_function_0-image-ecr_repo
         - aws:iam_role:lambda_function_0-ExecutionRole
     tag: big
 
diff --git a/pkg/engine2/testdata/single_lambda.expect.yaml b/pkg/engine2/testdata/single_lambda.expect.yaml
index 703ad43e..d6c36740 100755
--- a/pkg/engine2/testdata/single_lambda.expect.yaml
+++ b/pkg/engine2/testdata/single_lambda.expect.yaml
@@ -9,7 +9,7 @@ resources:
     aws:ecr_image:lambda_function_0-image:
         Context: .
         Dockerfile: lambda_function_0-image.Dockerfile
-        Repo: aws:ecr_repo:ecr_repo-0
+        Repo: aws:ecr_repo:lambda_function_0-image-ecr_repo
     aws:iam_role:lambda_function_0-ExecutionRole:
         AssumeRolePolicyDoc:
             Statement:
@@ -22,7 +22,7 @@ resources:
             Version: "2012-10-17"
         ManagedPolicies:
             - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
-    aws:ecr_repo:ecr_repo-0:
+    aws:ecr_repo:lambda_function_0-image-ecr_repo:
         ForceDelete: true
     aws:log_group:lambda_function_0-log-group:
         LogGroupName: /aws/lambda/lambda_function_0
@@ -32,5 +32,5 @@ edges:
     aws:lambda_function:lambda_function_0 -> aws:ecr_image:lambda_function_0-image:
     aws:lambda_function:lambda_function_0 -> aws:iam_role:lambda_function_0-ExecutionRole:
     aws:SERVICE_API:lambda_function_0-lambda_function_0-log-group -> aws:log_group:lambda_function_0-log-group:
-    aws:ecr_image:lambda_function_0-image -> aws:ecr_repo:ecr_repo-0:
+    aws:ecr_image:lambda_function_0-image -> aws:ecr_repo:lambda_function_0-image-ecr_repo:
     aws:iam_role:lambda_function_0-ExecutionRole -> aws:log_group:lambda_function_0-log-group:
diff --git a/pkg/engine2/testdata/single_lambda.iac-viz.yaml b/pkg/engine2/testdata/single_lambda.iac-viz.yaml
index 777d6f5f..3370986e 100755
--- a/pkg/engine2/testdata/single_lambda.iac-viz.yaml
+++ b/pkg/engine2/testdata/single_lambda.iac-viz.yaml
@@ -1,12 +1,12 @@
 provider: aws
 resources:
-  ecr_repo/ecr_repo-0:
+  ecr_repo/lambda_function_0-image-ecr_repo:
 
   log_group/lambda_function_0-log-group:
 
   ecr_image/lambda_function_0-image:
 
-  ecr_image/lambda_function_0-image -> ecr_repo/ecr_repo-0:
+  ecr_image/lambda_function_0-image -> ecr_repo/lambda_function_0-image-ecr_repo:
   iam_role/lambda_function_0-executionrole:
 
   iam_role/lambda_function_0-executionrole -> log_group/lambda_function_0-log-group:
diff --git a/pkg/engine2/testdata/vpc_import_to_lambda.dataflow-viz.yaml b/pkg/engine2/testdata/vpc_import_to_lambda.dataflow-viz.yaml
index 853f4a5d..a7641402 100644
--- a/pkg/engine2/testdata/vpc_import_to_lambda.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/vpc_import_to_lambda.dataflow-viz.yaml
@@ -10,7 +10,7 @@ resources:
   lambda_function/lambda_function:
     children:
         - aws:ecr_image:lambda_function-image
-        - aws:ecr_repo:ecr_repo-0
+        - aws:ecr_repo:lambda_function-image-ecr_repo
         - aws:iam_role:lambda_function-ExecutionRole
     parent: vpc/vpc
     tag: big
diff --git a/pkg/engine2/testdata/vpc_import_to_lambda.expect.yaml b/pkg/engine2/testdata/vpc_import_to_lambda.expect.yaml
index e97d9b53..a90f1dac 100644
--- a/pkg/engine2/testdata/vpc_import_to_lambda.expect.yaml
+++ b/pkg/engine2/testdata/vpc_import_to_lambda.expect.yaml
@@ -36,7 +36,7 @@ resources:
     aws:ecr_image:lambda_function-image:
         Context: .
         Dockerfile: lambda_function-image.Dockerfile
-        Repo: aws:ecr_repo:ecr_repo-0
+        Repo: aws:ecr_repo:lambda_function-image-ecr_repo
     aws:iam_role:lambda_function-ExecutionRole:
         AssumeRolePolicyDoc:
             Statement:
@@ -48,8 +48,8 @@ resources:
                         - lambda.amazonaws.com
             Version: "2012-10-17"
         ManagedPolicies:
-            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
             - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
+            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
     aws:subnet:vpc:subnet1:
         Type: private
         Vpc: aws:vpc:vpc
@@ -58,7 +58,7 @@ resources:
         Type: private
         Vpc: aws:vpc:vpc
         imported: true
-    aws:ecr_repo:ecr_repo-0:
+    aws:ecr_repo:lambda_function-image-ecr_repo:
         ForceDelete: true
     aws:SERVICE_API:lambda_function-lambda_function-log-group:
     aws:vpc:vpc:
@@ -76,7 +76,7 @@ edges:
     aws:lambda_function:lambda_function -> aws:iam_role:lambda_function-ExecutionRole:
     aws:lambda_function:lambda_function -> aws:subnet:vpc:subnet1:
     aws:lambda_function:lambda_function -> aws:subnet:vpc:subnet2:
-    aws:ecr_image:lambda_function-image -> aws:ecr_repo:ecr_repo-0:
+    aws:ecr_image:lambda_function-image -> aws:ecr_repo:lambda_function-image-ecr_repo:
     aws:iam_role:lambda_function-ExecutionRole -> aws:log_group:lambda_function-log-group:
     aws:subnet:vpc:subnet1 -> aws:SERVICE_API:lambda_function-lambda_function-log-group:
     aws:subnet:vpc:subnet1 -> aws:vpc:vpc:
diff --git a/pkg/engine2/testdata/vpc_import_to_lambda.iac-viz.yaml b/pkg/engine2/testdata/vpc_import_to_lambda.iac-viz.yaml
index 05d8ec1e..2f776270 100644
--- a/pkg/engine2/testdata/vpc_import_to_lambda.iac-viz.yaml
+++ b/pkg/engine2/testdata/vpc_import_to_lambda.iac-viz.yaml
@@ -1,6 +1,6 @@
 provider: aws
 resources:
-  ecr_repo/ecr_repo-0:
+  ecr_repo/lambda_function-image-ecr_repo:
 
   log_group/lambda_function-log-group:
 
@@ -8,7 +8,7 @@ resources:
 
   ecr_image/lambda_function-image:
 
-  ecr_image/lambda_function-image -> ecr_repo/ecr_repo-0:
+  ecr_image/lambda_function-image -> ecr_repo/lambda_function-image-ecr_repo:
   iam_role/lambda_function-executionrole:
 
   iam_role/lambda_function-executionrole -> log_group/lambda_function-log-group:
diff --git a/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.dataflow-viz.yaml b/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.dataflow-viz.yaml
index ae4f1887..5faf932a 100644
--- a/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.dataflow-viz.yaml
@@ -17,7 +17,7 @@ resources:
   lambda_function/lambda_function:
     children:
         - aws:ecr_image:lambda_function-image
-        - aws:ecr_repo:ecr_repo-0
+        - aws:ecr_repo:lambda_function-image-ecr_repo
         - aws:iam_role:lambda_function-ExecutionRole
     parent: vpc/vpc
     tag: big
diff --git a/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.expect.yaml b/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.expect.yaml
index f90b59f7..054dfeb2 100644
--- a/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.expect.yaml
+++ b/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.expect.yaml
@@ -28,7 +28,7 @@ resources:
     aws:ecr_image:lambda_function-image:
         Context: .
         Dockerfile: lambda_function-image.Dockerfile
-        Repo: aws:ecr_repo:ecr_repo-0
+        Repo: aws:ecr_repo:lambda_function-image-ecr_repo
     aws:iam_role:lambda_function-ExecutionRole:
         AssumeRolePolicyDoc:
             Statement:
@@ -40,8 +40,8 @@ resources:
                         - lambda.amazonaws.com
             Version: "2012-10-17"
         ManagedPolicies:
-            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
             - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
+            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
     aws:subnet:vpc:lambda_function-vpc:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-0
         CidrBlock: 10.0.128.0/18
@@ -56,7 +56,7 @@ resources:
         RouteTable: aws:route_table:vpc:subnet-1-route_table
         Type: private
         Vpc: aws:vpc:vpc
-    aws:ecr_repo:ecr_repo-0:
+    aws:ecr_repo:lambda_function-image-ecr_repo:
         ForceDelete: true
     aws:route_table_association:lambda_function-vpc-lambda_function-vpc-route_table:
         RouteTable: aws:route_table:vpc:lambda_function-vpc-route_table
@@ -135,7 +135,7 @@ edges:
     aws:lambda_function:lambda_function -> aws:iam_role:lambda_function-ExecutionRole:
     aws:lambda_function:lambda_function -> aws:subnet:vpc:lambda_function-vpc:
     aws:lambda_function:lambda_function -> aws:subnet:vpc:subnet-1:
-    aws:ecr_image:lambda_function-image -> aws:ecr_repo:ecr_repo-0:
+    aws:ecr_image:lambda_function-image -> aws:ecr_repo:lambda_function-image-ecr_repo:
     aws:iam_role:lambda_function-ExecutionRole -> aws:log_group:lambda_function-log-group:
     aws:subnet:vpc:lambda_function-vpc -> aws:SERVICE_API:lambda_function-lambda_function-log-group:
     aws:subnet:vpc:lambda_function-vpc -> aws:availability_zone:region-0:availability_zone-0:
diff --git a/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.iac-viz.yaml b/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.iac-viz.yaml
index 40d83508..2e90ccbb 100644
--- a/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.iac-viz.yaml
+++ b/pkg/engine2/testdata/vpc_import_wo_subnets_to_lambda.iac-viz.yaml
@@ -33,7 +33,7 @@ resources:
 
   aws:nat_gateway:subnet-2/lambda_function-vpc-route_table-nat_gateway -> elastic_ip/lambda_function-vpc-route_table-nat_gateway-elastic_ip:
   aws:nat_gateway:subnet-2/lambda_function-vpc-route_table-nat_gateway -> aws:subnet:vpc/subnet-2:
-  ecr_repo/ecr_repo-0:
+  ecr_repo/lambda_function-image-ecr_repo:
 
   log_group/lambda_function-log-group:
 
@@ -63,7 +63,7 @@ resources:
   aws:subnet:vpc/lambda_function-vpc -> vpc/vpc:
   ecr_image/lambda_function-image:
 
-  ecr_image/lambda_function-image -> ecr_repo/ecr_repo-0:
+  ecr_image/lambda_function-image -> ecr_repo/lambda_function-image-ecr_repo:
   iam_role/lambda_function-executionrole:
 
   iam_role/lambda_function-executionrole -> log_group/lambda_function-log-group:
diff --git a/pkg/knowledge_base2/properties/property_test.go b/pkg/knowledge_base2/properties/property_test.go
index d737d76e..df14e551 100644
--- a/pkg/knowledge_base2/properties/property_test.go
+++ b/pkg/knowledge_base2/properties/property_test.go
@@ -87,6 +87,60 @@ func Test_ValidatePropertyRef(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "nested property ref",
+			ref: construct.PropertyRef{
+				Resource: construct.ResourceId{Name: "test"},
+				Property: "test",
+			},
+			propertyType: "string",
+			testResources: []*construct.Resource{
+				{
+					ID: construct.ResourceId{Name: "test"},
+					Properties: map[string]any{
+						"test": construct.PropertyRef{
+							Resource: construct.ResourceId{Name: "test2"},
+							Property: "test2",
+						},
+					},
+				},
+				{
+					ID: construct.ResourceId{Name: "test2"},
+					Properties: map[string]any{
+						"test2": "testval",
+					},
+				},
+			},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetResourceTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Name: "test"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.ResourceTemplate{
+							Properties: knowledgebase.Properties{
+								"test": &StringProperty{PropertyDetails: knowledgebase.PropertyDetails{Path: "test", Name: "test"}},
+							},
+						}, nil,
+					},
+				},
+				{
+					Method: "GetResourceTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Name: "test2"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.ResourceTemplate{
+							Properties: knowledgebase.Properties{
+								"test2": &StringProperty{PropertyDetails: knowledgebase.PropertyDetails{Path: "test2", Name: "test2"}},
+							},
+						}, nil,
+					},
+				},
+			},
+			expect: "testval",
+		},
 		{
 			name:         "no resource, throws err",
 			propertyType: "string",