Repository: klothoplatform/klotho
Pull request #847 (2024-01-05 06:51:11)

Fix Local layer to only look at operational resource paths
Since it's a little hard to parse the test data, here's the test yaml I was using

```yaml
constraints:
  - node: aws:ecs_service:ecs_service_0
    operator: add
    scope: application
  - node: aws:load_balancer:load-balancer-2
    operator: add
    scope: application
  - operator: must_exist
    scope: edge
    target:
      source: aws:load_balancer:load-balancer-2
      target: aws:ecs_service:ecs_service_0
```

Which results in (annotated):

```yaml
provider: aws
resources:
  ecs_service/ecs_service_0:
    children:
      # AZs added because ecs -> subnet -> az is all within the operational side effect (glue) boundary
      - aws:availability_zone:region-0:availability_zone-0
      - aws:availability_zone:region-0:availability_zone-1
      # ECR things via the task definition
      - aws:ecr_image:ecs_service_0-image
      - aws:ecr_repo:ecr_repo-0
      - aws:ecs_cluster:ecs_cluster-0
      - aws:ecs_task_definition:ecs_service_0
      - aws:iam_role:ecs_service_0-execution-role
      - aws:log_group:ecs_service_0-log-group
      - aws:region:region-0
      - aws:subnet:vpc-0:subnet-0
      - aws:subnet:vpc-0:subnet-1
      - aws:vpc:vpc-0
    parent: vpc/vpc-0
    tag: big

  vpc/vpc-0:
    children:
      # VPC includes resource for which it is the namespace
      - 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:ecs_service_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

  load_balancer/load-balancer-2:
    children:
      - aws:availability_zone:region-0:availability_zone-0
      - aws:availability_zone:region-0:availability_zone-1
      - aws:region:region-0
      - aws:subnet:vpc-0:subnet-0
      - aws:subnet:vpc-0:subnet-1
      - aws:vpc:vpc-0
    parent: vpc/vpc-0
    tag: big

  load_balancer/load-balancer-2 -> ecs_service/ecs_service_0:
    path:
      # resources included in any path from load_balancer to ecs_service
      - aws:load_balancer_listener:load-balancer-2-ecs_service_0
      - aws:security_group:vpc-0:ecs_service_0-security_group
      - aws:subnet:vpc-0:subnet-0
      - aws:subnet:vpc-0:subnet-1
      - aws:target_group:load-balancer-2-6c8c4811
```

![image](https://github.com/klothoplatform/klotho/assets/90645437/49dc6311-404f-4387-b62a-1229471021ca)
![image](https://github.com/klothoplatform/klotho/assets/90645437/3bde68ac-10c2-4f73-8ae7-89c57ea5abe7)
![image](https://github.com/klothoplatform/klotho/assets/90645437/fa0a98be-2bc9-47eb-82af-ea7be7bded7c)



gordon-klotho: > in which case i think we need to be able to separate these search patterns because this will definitely affect path selection.

I intentionally changed the glue layer logic instead of implementing it just for the visualizer because I believe the old glue layer logic was incorrect and the new logic is better for what it was intended to be.

> could we add some unit tests around the glue layer search? i realize our functional tests do cover it so not blocking but would be great to have isolated tests

Yeah, I was going to add some but decided to post this as soon as I had it working to get an early review. Standby for some tests

gordon-klotho: ![image](https://github.com/klothoplatform/klotho/assets/90645437/d025c62c-09a2-4288-934e-9695a132ecba)


Patch:
diff --git a/pkg/engine2/cli.go b/pkg/engine2/cli.go
index 64b773ac..9fc9105d 100644
--- a/pkg/engine2/cli.go
+++ b/pkg/engine2/cli.go
@@ -412,14 +412,14 @@ func writeDebugGraphs(sol solution_context.SolutionContext) {
 	wg.Add(2)
 	go func() {
 		defer wg.Done()
-		err := construct.GraphToSVG(sol.DataflowGraph(), "dataflow")
+		err := GraphToSVG(sol.KnowledgeBase(), sol.DataflowGraph(), "dataflow")
 		if err != nil {
 			zap.S().Errorf("failed to write dataflow graph: %s", err.Error())
 		}
 	}()
 	go func() {
 		defer wg.Done()
-		err := construct.GraphToSVG(sol.DeploymentGraph(), "iac")
+		err := GraphToSVG(sol.KnowledgeBase(), sol.DeploymentGraph(), "iac")
 		if err != nil {
 			zap.S().Errorf("failed to write iac graph: %s", err.Error())
 		}
diff --git a/pkg/engine2/dot.go b/pkg/engine2/dot.go
new file mode 100644
index 00000000..da81474e
--- /dev/null
+++ b/pkg/engine2/dot.go
@@ -0,0 +1,129 @@
+package engine2
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"sort"
+
+	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/dot"
+	knowledgebase2 "github.com/klothoplatform/klotho/pkg/knowledge_base2"
+)
+
+func dotAttributes(kb knowledgebase2.TemplateKB, r *construct.Resource) map[string]string {
+	a := make(map[string]string)
+	a["label"] = r.ID.String()
+	a["shape"] = "box"
+	tmpl, _ := kb.GetResourceTemplate(r.ID)
+	if tmpl != nil {
+		a["label"] += fmt.Sprintf("\n%v", tmpl.Classification.Is)
+	}
+	return a
+}
+
+func dotEdgeAttributes(e construct.ResourceEdge) map[string]string {
+	a := make(map[string]string)
+	_ = e.Source.WalkProperties(func(path construct.PropertyPath, nerr error) error {
+		v := path.Get()
+		if v == e.Target.ID {
+			a["label"] = path.String()
+			return construct.StopWalk
+		}
+		return nil
+	})
+	if e.Properties.Weight > 0 {
+		if a["label"] == "" {
+			a["label"] = fmt.Sprintf("%d", e.Properties.Weight)
+		} else {
+			a["label"] = fmt.Sprintf("%s\n%d", a["label"], e.Properties.Weight)
+		}
+	}
+	return a
+}
+
+func GraphToDOT(kb knowledgebase2.TemplateKB, g construct.Graph, out io.Writer) error {
+	ids, err := construct.TopologicalSort(g)
+	if err != nil {
+		return err
+	}
+	nodes, err := construct.ResolveIds(g, ids)
+	if err != nil {
+		return err
+	}
+	var errs error
+	printf := func(s string, args ...any) {
+		_, err := fmt.Fprintf(out, s, args...)
+		errs = errors.Join(errs, err)
+	}
+	printf(`digraph {
+  rankdir = TB
+`)
+	for _, n := range nodes {
+		printf("  %q%s\n", n.ID, dot.AttributesToString(dotAttributes(kb, n)))
+	}
+
+	topoIndex := func(id construct.ResourceId) int {
+		for i, id2 := range ids {
+			if id2 == id {
+				return i
+			}
+		}
+		return -1
+	}
+	edges, err := g.Edges()
+	if err != nil {
+		return err
+	}
+	sort.Slice(edges, func(i, j int) bool {
+		ti, tj := topoIndex(edges[i].Source), topoIndex(edges[j].Source)
+		if ti != tj {
+			return ti < tj
+		}
+		ti, tj = topoIndex(edges[i].Target), topoIndex(edges[j].Target)
+		return ti < tj
+	})
+	for _, e := range edges {
+		edge, err := g.Edge(e.Source, e.Target)
+		if err != nil {
+			errs = errors.Join(errs, err)
+			continue
+		}
+		printf("  %q -> %q%s\n", e.Source, e.Target, dot.AttributesToString(dotEdgeAttributes(edge)))
+	}
+	printf("}\n")
+	return errs
+}
+
+func GraphToSVG(kb knowledgebase2.TemplateKB, g construct.Graph, prefix string) error {
+	if debugDir := os.Getenv("KLOTHO_DEBUG_DIR"); debugDir != "" {
+		prefix = filepath.Join(debugDir, prefix)
+	}
+	f, err := os.Create(prefix + ".gv")
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	dotContent := new(bytes.Buffer)
+	err = GraphToDOT(kb, g, io.MultiWriter(f, dotContent))
+	if err != nil {
+		return fmt.Errorf("could not render graph to file %s: %v", prefix+".gv", err)
+	}
+
+	svgContent, err := dot.ExecPan(bytes.NewReader(dotContent.Bytes()))
+	if err != nil {
+		return fmt.Errorf("could not run 'dot' for %s: %v", prefix+".gv", err)
+	}
+
+	svgFile, err := os.Create(prefix + ".gv.svg")
+	if err != nil {
+		return fmt.Errorf("could not create file %s: %v", prefix+".gv.svg", err)
+	}
+	defer svgFile.Close()
+	_, err = fmt.Fprint(svgFile, svgContent)
+	return err
+}
diff --git a/pkg/engine2/path_selection/paths.go b/pkg/engine2/path_selection/paths.go
index 20596817..0eaea619 100644
--- a/pkg/engine2/path_selection/paths.go
+++ b/pkg/engine2/path_selection/paths.go
@@ -13,7 +13,7 @@ import (
 func GetPaths(
 	sol solution_context.SolutionContext,
 	source, target construct.ResourceId,
-	pathValidityChecks func(source, target construct.ResourceId, path []construct.ResourceId) bool,
+	pathValidityChecks func(source, target construct.ResourceId, path construct.Path) bool,
 	hasPathCheck bool,
 ) ([]construct.Path, error) {
 	var errs error
diff --git a/pkg/engine2/reconciler/remove_edge.go b/pkg/engine2/reconciler/remove_edge.go
index 1609fdb2..0a601905 100644
--- a/pkg/engine2/reconciler/remove_edge.go
+++ b/pkg/engine2/reconciler/remove_edge.go
@@ -155,7 +155,7 @@ func findEdgesUsedInOtherPathSelection(
 			continue
 		}
 		paths, err := path_selection.GetPaths(ctx, upstream, target,
-			func(source, target construct.ResourceId, path []construct.ResourceId) bool { return true }, false)
+			func(source, target construct.ResourceId, path construct.Path) bool { return true }, false)
 		if err != nil {
 			errs = errors.Join(errs, err)
 			continue
@@ -190,7 +190,7 @@ func findEdgesUsedInOtherPathSelection(
 		}
 
 		paths, err := path_selection.GetPaths(ctx, source, downstream,
-			func(source, target construct.ResourceId, path []construct.ResourceId) bool { return true }, false)
+			func(source, target construct.ResourceId, path construct.Path) bool { return true }, false)
 		if err != nil {
 			errs = errors.Join(errs, err)
 			continue
diff --git a/pkg/engine2/visualizer.go b/pkg/engine2/visualizer.go
index 93b1cc2d..c09dc861 100644
--- a/pkg/engine2/visualizer.go
+++ b/pkg/engine2/visualizer.go
@@ -64,24 +64,24 @@ func GetResourceVizTag(kb knowledgebase.TemplateKB, view View, resource construc
 
 func (e *Engine) GetViewsDag(view View, sol solution_context.SolutionContext) (visualizer.VisGraph, error) {
 	viewDag := visualizer.NewVisGraph()
-	var graph construct.Graph
+	var resGraph construct.Graph
 	if view == IACView {
-		graph = sol.DeploymentGraph()
+		resGraph = sol.DeploymentGraph()
 	} else {
-		graph = sol.DataflowGraph()
+		resGraph = sol.DataflowGraph()
 	}
 
 	undirected := construct.NewGraphWithOptions()
-	err := undirected.AddVerticesFrom(graph)
+	err := undirected.AddVerticesFrom(resGraph)
 	if err != nil {
 		return nil, fmt.Errorf("could not copy vertices for undirected: %w", err)
 	}
-	err = undirected.AddEdgesFrom(graph)
+	err = undirected.AddEdgesFrom(resGraph)
 	if err != nil {
 		return nil, fmt.Errorf("could not copy edges for undirected: %w", err)
 	}
 
-	ids, err := construct.ReverseTopologicalSort(graph)
+	ids, err := construct.ReverseTopologicalSort(resGraph)
 	if err != nil {
 		return nil, err
 	}
@@ -113,17 +113,17 @@ func (e *Engine) GetViewsDag(view View, sol solution_context.SolutionContext) (v
 		case NoRenderTag:
 			continue
 		case ParentIconTag:
-			continue
+			err := e.handleParentIcon(sol, view, viewDag, id)
+			if err != nil {
+				errs = errors.Join(errs, fmt.Errorf("failed to handle parent icon %s: %w", id, err))
+			}
 		case BigIconTag:
 			err := e.handleBigIcon(sol, view, undirected, viewDag, id)
 			if err != nil {
 				errs = errors.Join(errs, fmt.Errorf("failed to handle big icon %s: %w", id, err))
 			}
 		case SmallIconTag:
-			err := e.handleSmallIcon(view, undirected, viewDag, id)
-			if err != nil {
-				errs = errors.Join(errs, fmt.Errorf("failed to handle small icon %s: %w", id, err))
-			}
+			// Small icons don't need special handling, handleBigIcon will look for any relevant small icons to include
 		default:
 			errs = errors.Join(errs, fmt.Errorf("unknown tag %s", tag))
 		}
@@ -132,70 +132,27 @@ func (e *Engine) GetViewsDag(view View, sol solution_context.SolutionContext) (v
 	return viewDag, errs
 }
 
-// handleSmallIcon finds big icons to attach this resource to. It always adds to big icons for which it is
-// in the glue layer. It also adds to the big icon that is closest to the resource (if there is one).
-func (e *Engine) handleSmallIcon(
+func (e *Engine) handleParentIcon(
+	sol solution_context.SolutionContext,
 	view View,
-	g construct.Graph,
 	viewDag visualizer.VisGraph,
 	id construct.ResourceId,
 ) error {
-	ids, err := construct.TopologicalSort(viewDag)
+	this, err := viewDag.Vertex(id)
 	if err != nil {
 		return err
 	}
-	pather, err := construct.ShortestPaths(g, id, construct.DontSkipEdges)
+
+	parent, err := e.findParent(view, sol, viewDag, id)
 	if err != nil {
 		return err
 	}
-	glueIds, err := knowledgebase.Downstream(g, e.Kb, id, knowledgebase.ResourceGlueLayer)
-	if err != nil {
+	this.Parent = parent
+
+	if err := e.setChildren(sol, view, this); err != nil {
 		return err
 	}
-	glue := set.SetOf(glueIds...)
-	var errs error
-	var bestParent *visualizer.VisResource
-	bestParentWeight := math.MaxInt32
-	for _, candidate := range ids {
-		// If the resource is in the glue layer, add it
-		if glue.Contains(candidate) {
-			parent, err := viewDag.Vertex(candidate)
-			if err != nil {
-				errs = errors.Join(errs, err)
-				continue
-			}
-			parent.Children.Add(id)
-		}
 
-		// Even if it was in the glue layer, continue calculating the best parent so we don't accidentally
-		// attribute it to a worse parent.
-		path, err := pather.ShortestPath(candidate)
-		if errors.Is(err, graph.ErrTargetNotReachable) {
-			continue
-		} else if err != nil {
-			errs = errors.Join(errs, err)
-			continue
-		}
-		weight, err := graph_addons.PathWeight(g, graph_addons.Path[construct.ResourceId](path))
-		if err != nil {
-			errs = errors.Join(errs, err)
-			continue
-		}
-		if weight < bestParentWeight {
-			bestParentWeight = weight
-			bestParent, err = viewDag.Vertex(candidate)
-			if err != nil {
-				errs = errors.Join(errs, err)
-				continue
-			}
-		}
-	}
-	if errs != nil {
-		return errs
-	}
-	if bestParent != nil {
-		bestParent.Children.Add(id)
-	}
 	return nil
 }
 
@@ -208,15 +165,16 @@ func (e *Engine) handleBigIcon(
 	viewDag visualizer.VisGraph,
 	id construct.ResourceId,
 ) error {
-	source, err := viewDag.Vertex(id)
+	this, err := viewDag.Vertex(id)
 	if err != nil {
 		return err
 	}
-	parent, err := e.findParent(view, undirected, viewDag, id)
+
+	parent, err := e.findParent(view, sol, viewDag, id)
 	if err != nil {
 		return err
 	}
-	source.Parent = parent
+	this.Parent = parent
 
 	targets, err := construct.TopologicalSort(viewDag)
 	if err != nil {
@@ -232,40 +190,146 @@ func (e *Engine) handleBigIcon(
 			continue
 		}
 
-		hasPath, err := hasVisPath(sol, view, id, target)
+		paths, err := visPaths(sol, view, id, target)
 		if err != nil {
 			errs = errors.Join(errs, err)
 			continue
 		}
-		if hasPath {
-			errs = errors.Join(errs, viewDag.AddEdge(id, target))
+		if len(paths) > 0 {
+			allPathResources := make(set.Set[construct.ResourceId])
+			for _, path := range paths {
+				for _, pathRes := range path[1 : len(path)-1] {
+					allPathResources.Add(pathRes)
+				}
+			}
+			errs = errors.Join(errs, viewDag.AddEdge(id, target, graph.EdgeData(visualizer.VisEdgeData{
+				PathResources: allPathResources,
+			})))
 		}
 	}
-	return err
+	if errs != nil {
+		return errs
+	}
+
+	if err := e.setChildren(sol, view, this); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (e *Engine) setChildren(sol solution_context.SolutionContext, view View, v *visualizer.VisResource) error {
+	glue, err := knowledgebase.Downstream(
+		sol.DataflowGraph(),
+		sol.KnowledgeBase(),
+		v.ID,
+		knowledgebase.ResourceLocalLayer,
+	)
+	if err != nil {
+		return fmt.Errorf("failed to get glue layer for %s: %w", v.ID, err)
+	}
+	for _, glueElem := range glue {
+		if GetResourceVizTag(e.Kb, view, glueElem) == SmallIconTag {
+			v.Children.Add(glueElem)
+		}
+	}
+
+	// After glue, also include any resources whose namespace is this resource
+	ids, err := construct.TopologicalSort(sol.DataflowGraph())
+	if err != nil {
+		return err
+	}
+
+	var errs error
+	for _, id := range ids {
+		if id.Namespace == "" {
+			continue
+		}
+		tmpl, err := sol.KnowledgeBase().GetResourceTemplate(id)
+		if err != nil {
+			errs = errors.Join(errs, err)
+			continue
+		}
+		for _, p := range tmpl.Properties {
+			if p.Details().Namespace {
+				pres, err := sol.RawView().Vertex(id)
+				if err != nil {
+					errs = errors.Join(errs, err)
+					break
+				}
+				val, err := pres.GetProperty(p.Details().Path)
+				if err != nil {
+					errs = errors.Join(errs, err)
+					break
+				}
+				if val == v.ID {
+					v.Children.Add(id)
+					break
+				}
+			}
+		}
+	}
+
+	return nil
 }
 
 func (e *Engine) findParent(
 	view View,
-	undirected construct.Graph,
+	sol solution_context.SolutionContext,
 	viewDag visualizer.VisGraph,
 	id construct.ResourceId,
 ) (bestParent construct.ResourceId, err error) {
-	ids, err := construct.TopologicalSort(viewDag)
+	if id.Namespace != "" {
+		// namespaced resources' parents is always their namespace resource
+		tmpl, err := sol.KnowledgeBase().GetResourceTemplate(id)
+		if err != nil {
+			return bestParent, err
+		}
+		thisRes, err := sol.RawView().Vertex(id)
+		if err != nil {
+			return bestParent, err
+		}
+		for _, p := range tmpl.Properties {
+			if !p.Details().Namespace {
+				continue
+			}
+			v, err := thisRes.GetProperty(p.Details().Path)
+			if err != nil {
+				return bestParent, fmt.Errorf("failed to get namespace property %s: %w", p.Details().Path, err)
+			}
+			if propId, ok := v.(construct.ResourceId); ok {
+				if GetResourceVizTag(e.Kb, view, propId) == ParentIconTag {
+					return propId, nil
+				}
+				// the property isn't shown as a parent (eg. Subnet or ALB Listener), so roll it up to the next parent
+				return e.findParent(view, sol, viewDag, propId)
+			} else {
+				return bestParent, fmt.Errorf("namespace property %s is not a resource id (was: %T)", p.Details().Path, v)
+			}
+		}
+	}
+
+	glue, err := knowledgebase.Downstream(
+		sol.DataflowGraph(),
+		sol.KnowledgeBase(),
+		id,
+		knowledgebase.ResourceLocalLayer,
+	)
 	if err != nil {
 		return
 	}
-	pather, err := construct.ShortestPaths(undirected, id, construct.DontSkipEdges)
+	pather, err := construct.ShortestPaths(sol.DataflowGraph(), id, construct.DontSkipEdges)
 	if err != nil {
 		return
 	}
 	bestParentWeight := math.MaxInt32
 	var errs error
 candidateLoop:
-	for _, id := range ids {
-		if GetResourceVizTag(e.Kb, view, id) != ParentIconTag {
+	for _, candidate := range glue {
+		if GetResourceVizTag(e.Kb, view, candidate) != ParentIconTag {
 			continue
 		}
-		path, err := pather.ShortestPath(id)
+		path, err := pather.ShortestPath(candidate)
 		if errors.Is(err, graph.ErrTargetNotReachable) {
 			continue
 		} else if err != nil {
@@ -283,39 +347,39 @@ candidateLoop:
 				continue candidateLoop
 			}
 		}
-		weight, err := graph_addons.PathWeight(undirected, graph_addons.Path[construct.ResourceId](path))
+		weight, err := graph_addons.PathWeight(sol.DataflowGraph(), graph_addons.Path[construct.ResourceId](path))
 		if err != nil {
 			errs = errors.Join(errs, err)
 			continue
 		}
 		if weight < bestParentWeight {
 			bestParentWeight = weight
-			bestParent = id
+			bestParent = candidate
 		}
 	}
 	err = errs
 	return
 }
 
-func hasVisPath(sol solution_context.SolutionContext, view View, source, target construct.ResourceId) (bool, error) {
+func visPaths(sol solution_context.SolutionContext, view View, source, target construct.ResourceId) ([]construct.Path, error) {
 	srcTemplate, err := sol.KnowledgeBase().GetResourceTemplate(source)
 	if err != nil || srcTemplate == nil {
-		return false, fmt.Errorf("has path could not find source resource %s: %w", source, err)
+		return nil, fmt.Errorf("has path could not find source resource %s: %w", source, err)
 	}
 	targetTemplate, err := sol.KnowledgeBase().GetResourceTemplate(target)
 	if err != nil || targetTemplate == nil {
-		return false, fmt.Errorf("has path could not find target resource %s: %w", target, err)
+		return nil, fmt.Errorf("has path could not find target resource %s: %w", target, err)
 	}
 	if len(targetTemplate.PathSatisfaction.AsTarget) == 0 || len(srcTemplate.PathSatisfaction.AsSource) == 0 {
-		return false, nil
+		return nil, nil
 	}
 	sourceRes, err := sol.RawView().Vertex(source)
 	if err != nil {
-		return false, fmt.Errorf("has path could not find source resource %s: %w", source, err)
+		return nil, fmt.Errorf("has path could not find source resource %s: %w", source, err)
 	}
 	targetRes, err := sol.RawView().Vertex(target)
 	if err != nil {
-		return false, fmt.Errorf("has path could not find target resource %s: %w", target, err)
+		return nil, fmt.Errorf("has path could not find target resource %s: %w", target, err)
 	}
 
 	consumed, err := knowledgebase.HasConsumedFromResource(
@@ -324,20 +388,20 @@ func hasVisPath(sol solution_context.SolutionContext, view View, source, target
 		solution_context.DynamicCtx(sol),
 	)
 	if err != nil {
-		return false, err
+		return nil, err
 	}
 	if !consumed {
-		return false, nil
+		return nil, nil
 	}
 	return checkPaths(sol, view, source, target)
 }
 
-func checkPaths(sol solution_context.SolutionContext, view View, source, target construct.ResourceId) (bool, error) {
+func checkPaths(sol solution_context.SolutionContext, view View, source, target construct.ResourceId) ([]construct.Path, error) {
 	paths, err := path_selection.GetPaths(
 		sol,
 		source,
 		target,
-		func(source, target construct.ResourceId, path []construct.ResourceId) bool {
+		func(source, target construct.ResourceId, path construct.Path) bool {
 			for _, res := range path[1 : len(path)-1] {
 				switch GetResourceVizTag(sol.KnowledgeBase(), view, res) {
 				case BigIconTag, ParentIconTag:
@@ -347,7 +411,7 @@ func checkPaths(sol solution_context.SolutionContext, view View, source, target
 			}
 			return true
 		},
-		true,
+		false,
 	)
-	return len(paths) > 0, err
+	return paths, err
 }
diff --git a/pkg/graph_addons/path.go b/pkg/graph_addons/path.go
index 131a56fe..b62a60c9 100644
--- a/pkg/graph_addons/path.go
+++ b/pkg/graph_addons/path.go
@@ -23,3 +23,12 @@ func PathWeight[K comparable, V any](g graph.Graph[K, V], path Path[K]) (weight
 	}
 	return
 }
+
+func (p Path[K]) Contains(k K) bool {
+	for _, elem := range p {
+		if elem == k {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/graph_addons/walk.go b/pkg/graph_addons/walk.go
index f2db9bea..92c93692 100644
--- a/pkg/graph_addons/walk.go
+++ b/pkg/graph_addons/walk.go
@@ -4,10 +4,9 @@ import (
 	"errors"
 
 	"github.com/dominikbraun/graph"
-	"github.com/klothoplatform/klotho/pkg/set"
 )
 
-type WalkGraphFunc[K comparable] func(k K, nerr error) error
+type WalkGraphFunc[K comparable] func(p Path[K], nerr error) error
 
 var (
 	StopWalk = errors.New("stop walk")
@@ -38,19 +37,34 @@ func walk[K comparable, T any](
 	f WalkGraphFunc[K],
 	deps map[K]map[K]graph.Edge[K],
 ) error {
-	visited := make(set.Set[K])
-	var queue []K
+	var queue []Path[K]
+	enqueue := func(current Path[K], next K) {
+		if current.Contains(next) {
+			// Prevent loops
+			return
+		}
+		// make a new slice because `append` won't copy if there's capacity
+		// which causes the latest `append` to overwrite the last element of any previous appends
+		// (as happens when appending in a loop as we do below).
+		//   x := make([]int, 2, 3); x[0] = 1; x[1] = 2
+		//   y := append(x, 3)
+		//   z := append(x, 4)
+		//   fmt.Println(y) // [1 2 4] !!
+		nextPath := make(Path[K], len(current)+1)
+		copy(nextPath, current)
+		nextPath[len(nextPath)-1] = next
+		queue = append(queue, nextPath)
+	}
 
+	startPath := Path[K]{start}
 	for d := range deps[start] {
-		queue = append(queue, d)
+		enqueue(startPath, d)
 	}
-	visited.Add(start)
 
 	var err error
-	var current K
+	var current Path[K]
 	for len(queue) > 0 {
 		current, queue = queue[0], queue[1:]
-		visited.Add(current)
 
 		nerr := f(current, err)
 		if errors.Is(nerr, StopWalk) {
@@ -61,11 +75,9 @@ func walk[K comparable, T any](
 		}
 		err = nerr
 
-		for d := range deps[current] {
-			if visited.Contains(d) {
-				continue
-			}
-			queue = append(queue, d)
+		last := current[len(current)-1]
+		for d := range deps[last] {
+			enqueue(current, d)
 		}
 	}
 	return err
diff --git a/pkg/knowledge_base2/dynamic_value.go b/pkg/knowledge_base2/dynamic_value.go
index 3ef559fa..e088da60 100644
--- a/pkg/knowledge_base2/dynamic_value.go
+++ b/pkg/knowledge_base2/dynamic_value.go
@@ -289,7 +289,8 @@ func (ctx DynamicValueContext) upstream(selector any, resource construct.Resourc
 	}
 
 	var match construct.ResourceId
-	err = graph_addons.WalkUp(ctx.Graph, resource, func(id construct.ResourceId, nerr error) error {
+	err = graph_addons.WalkUp(ctx.Graph, resource, func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		id := path[len(path)-1]
 		if selId.Matches(id) {
 			match = id
 			return graph_addons.StopWalk
@@ -341,12 +342,13 @@ func (ctx DynamicValueContext) LayeredUpstream(
 		return construct.ResourceId{}, err
 	}
 	result := construct.ResourceId{}
-	wrapper := func(id construct.ResourceId, nerr error) error {
+	wrapper := func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		id := path[len(path)-1]
 		if selId.Matches(id) {
 			result = id
 			return graph_addons.StopWalk
 		}
-		return f(id, nerr)
+		return f(path, nerr)
 	}
 	err = graph_addons.WalkUp(ctx.Graph, resource, wrapper)
 	if err != nil {
@@ -382,7 +384,8 @@ func (ctx DynamicValueContext) downstream(selector any, resource construct.Resou
 	}
 
 	var match construct.ResourceId
-	err = graph_addons.WalkDown(ctx.Graph, resource, func(id construct.ResourceId, nerr error) error {
+	err = graph_addons.WalkDown(ctx.Graph, resource, func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		id := path[len(path)-1]
 		if selId.Matches(id) {
 			match = id
 			return graph_addons.StopWalk
@@ -413,12 +416,13 @@ func (ctx DynamicValueContext) LayeredDownstream(
 		return construct.ResourceId{}, err
 	}
 	result := construct.ResourceId{}
-	wrapper := func(id construct.ResourceId, nerr error) error {
+	wrapper := func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		id := path[len(path)-1]
 		if selId.Matches(id) {
 			result = id
 			return graph_addons.StopWalk
 		}
-		return f(id, nerr)
+		return f(path, nerr)
 	}
 	err = graph_addons.WalkDown(ctx.Graph, resource, wrapper)
 	if err != nil {
diff --git a/pkg/knowledge_base2/graph.go b/pkg/knowledge_base2/graph.go
index 190d83c4..841d064c 100644
--- a/pkg/knowledge_base2/graph.go
+++ b/pkg/knowledge_base2/graph.go
@@ -12,9 +12,10 @@ import (
 
 type (
 	// DependencyLayer represents how far away a resource to return for the [Upstream]/[Downstream] methods.
-	// 1. ResourceLocalLayer (layer 1) represents any unique resources the target resource needs to be operational
+	// 1. ResourceLocalLayer (layer 1) represents any unique resources the target resource needs to be operational,
+	//    transitively.
 	// 2. ResourceGlueLayer (layer 2) represents all upstream/downstream resources that represent glue.
-	//  This will not include any other functional resources and will stop searching paths
+	//  This will not include any other functional resources and will stopsearching paths
 	//  once a functional resource is reached.
 	// 3. FirstFunctionalLayer (layer 3) represents all upstream/downstream resources that represent glue and
 	//  the first functional resource in other paths from the target resource.
@@ -41,18 +42,24 @@ func resourceLocal(
 	rid construct.ResourceId,
 	ids *[]construct.ResourceId,
 ) graph_addons.WalkGraphFunc[construct.ResourceId] {
-	return func(id construct.ResourceId, nerr error) error {
-		isSideEffect, err := IsOperationalResourceSideEffect(dag, kb, rid, id)
+	return func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		if len(path) <= 1 {
+			// skip source, this shouldn't happen but just in case
+			return nil
+		}
+		// Since we're skipping the path if it doesn't match, we only need to check the most recently added (ie, the last)
+		// resource in the path.
+		last := path[len(path)-1]
+		prevLast := path[len(path)-2]
+		sideEffect, err := IsOperationalResourceSideEffect(dag, kb, prevLast, last)
 		if err != nil {
 			return errors.Join(nerr, err)
 		}
-		if isSideEffect {
-			if ids != nil {
-				(*ids) = append(*ids, id)
-			}
-			return nil
+		if !sideEffect {
+			return graph_addons.SkipPath
 		}
-		return graph_addons.SkipPath
+		(*ids) = append(*ids, last)
+		return nil
 	}
 }
 
@@ -60,7 +67,8 @@ func resourceDirect(
 	dag construct.Graph,
 	ids *[]construct.ResourceId,
 ) graph_addons.WalkGraphFunc[construct.ResourceId] {
-	return func(id construct.ResourceId, nerr error) error {
+	return func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		id := path[len(path)-1]
 		if ids != nil {
 			(*ids) = append(*ids, id)
 		}
@@ -72,7 +80,8 @@ func resourceGlue(
 	kb TemplateKB,
 	ids *[]construct.ResourceId,
 ) graph_addons.WalkGraphFunc[construct.ResourceId] {
-	return func(id construct.ResourceId, nerr error) error {
+	return func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		id := path[len(path)-1]
 		if GetFunctionality(kb, id) == Unknown {
 			if ids != nil {
 				(*ids) = append(*ids, id)
@@ -87,7 +96,8 @@ func firstFunctional(
 	kb TemplateKB,
 	ids *[]construct.ResourceId,
 ) graph_addons.WalkGraphFunc[construct.ResourceId] {
-	return func(id construct.ResourceId, nerr error) error {
+	return func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		id := path[len(path)-1]
 		if ids != nil {
 			(*ids) = append(*ids, id)
 		}
@@ -101,7 +111,8 @@ func firstFunctional(
 func allDeps(
 	ids *[]construct.ResourceId,
 ) graph_addons.WalkGraphFunc[construct.ResourceId] {
-	return func(id construct.ResourceId, nerr error) error {
+	return func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		id := path[len(path)-1]
 		if ids != nil {
 			(*ids) = append(*ids, id)
 		}
@@ -157,7 +168,18 @@ func Downstream(dag construct.Graph, kb TemplateKB, rid construct.ResourceId, la
 	case ResourceLocalLayer:
 		f = resourceLocal(dag, kb, rid, &result)
 	case ResourceDirectLayer:
-		f = resourceDirect(dag, &result)
+		// use a more performant implementation for direct since we can use the edges directly.
+		edges, err := dag.Edges()
+		if err != nil {
+			return nil, err
+		}
+		var ids []construct.ResourceId
+		for _, edge := range edges {
+			if edge.Source == rid {
+				ids = append(ids, edge.Target)
+			}
+		}
+		return ids, nil
 	case ResourceGlueLayer:
 		f = resourceGlue(kb, &result)
 	case FirstFunctionalLayer:
@@ -173,7 +195,8 @@ func Downstream(dag construct.Graph, kb TemplateKB, rid construct.ResourceId, la
 
 func DownstreamFunctional(dag construct.Graph, kb TemplateKB, resource construct.ResourceId) ([]construct.ResourceId, error) {
 	var result []construct.ResourceId
-	err := graph_addons.WalkDown(dag, resource, func(id construct.ResourceId, nerr error) error {
+	err := graph_addons.WalkDown(dag, resource, func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		id := path[len(path)-1]
 		if GetFunctionality(kb, id) != Unknown {
 			result = append(result, id)
 			return graph_addons.SkipPath
@@ -190,7 +213,18 @@ func Upstream(dag construct.Graph, kb TemplateKB, rid construct.ResourceId, laye
 	case ResourceLocalLayer:
 		f = resourceLocal(dag, kb, rid, &result)
 	case ResourceDirectLayer:
-		f = resourceDirect(dag, &result)
+		// use a more performant implementation for direct since we can use the edges directly.
+		edges, err := dag.Edges()
+		if err != nil {
+			return nil, err
+		}
+		var ids []construct.ResourceId
+		for _, edge := range edges {
+			if edge.Target == rid {
+				ids = append(ids, edge.Source)
+			}
+		}
+		return ids, nil
 	case ResourceGlueLayer:
 		f = resourceGlue(kb, &result)
 	case FirstFunctionalLayer:
@@ -229,7 +263,8 @@ func layerWalkFunc(
 
 func UpstreamFunctional(dag construct.Graph, kb TemplateKB, resource construct.ResourceId) ([]construct.ResourceId, error) {
 	var result []construct.ResourceId
-	err := graph_addons.WalkUp(dag, resource, func(id construct.ResourceId, nerr error) error {
+	err := graph_addons.WalkUp(dag, resource, func(path graph_addons.Path[construct.ResourceId], nerr error) error {
+		id := path[len(path)-1]
 		if GetFunctionality(kb, id) != Unknown {
 			result = append(result, id)
 			return graph_addons.SkipPath
@@ -277,41 +312,43 @@ func IsOperationalResourceSideEffect(dag construct.Graph, kb TemplateKB, rid, si
 			}
 		}
 
+		if !ruleSatisfied {
+			continue
+		}
+
 		// If the side effect resource fits the rule we then perform 2 more checks
 		// 1. is there a path in the direction of the rule
 		// 2. Is the property set with the resource that we are checking for
-		if ruleSatisfied {
-			if step.Direction == DirectionUpstream {
-				resources, err := graph.ShortestPathStable(dag, sideEffect, rid, construct.ResourceIdLess)
-				if len(resources) == 0 || err != nil {
-					continue
-				}
-			} else {
-				resources, err := graph.ShortestPathStable(dag, rid, sideEffect, construct.ResourceIdLess)
-				if len(resources) == 0 || err != nil {
-					continue
-				}
+		if step.Direction == DirectionUpstream {
+			resources, err := graph.ShortestPathStable(dag, sideEffect, rid, construct.ResourceIdLess)
+			if len(resources) == 0 || err != nil {
+				continue
 			}
-
-			propertyVal, err := resource.GetProperty(property.Details().Path)
-			if err != nil || propertyVal == nil {
+		} else {
+			resources, err := graph.ShortestPathStable(dag, rid, sideEffect, construct.ResourceIdLess)
+			if len(resources) == 0 || err != nil {
 				continue
 			}
-			val := reflect.ValueOf(propertyVal)
-			if val.Kind() == reflect.Array || val.Kind() == reflect.Slice {
-				for i := 0; i < val.Len(); i++ {
-					if arrId, ok := val.Index(i).Interface().(construct.ResourceId); ok && arrId == sideEffect {
-						return true, nil
-					}
-				}
-			} else {
-				if val.IsZero() {
-					continue
-				}
-				if valId, ok := val.Interface().(construct.ResourceId); ok && valId == sideEffect {
+		}
+
+		propertyVal, err := resource.GetProperty(property.Details().Path)
+		if err != nil || propertyVal == nil {
+			continue
+		}
+		val := reflect.ValueOf(propertyVal)
+		if val.Kind() == reflect.Array || val.Kind() == reflect.Slice {
+			for i := 0; i < val.Len(); i++ {
+				if arrId, ok := val.Index(i).Interface().(construct.ResourceId); ok && arrId == sideEffect {
 					return true, nil
 				}
 			}
+		} else {
+			if val.IsZero() {
+				continue
+			}
+			if valId, ok := val.Interface().(construct.ResourceId); ok && valId == sideEffect {
+				return true, nil
+			}
 		}
 
 	}
diff --git a/pkg/visualizer2/file.go b/pkg/visualizer2/file.go
index fcc49af3..80ec084a 100644
--- a/pkg/visualizer2/file.go
+++ b/pkg/visualizer2/file.go
@@ -64,12 +64,8 @@ func (f *File) WriteTo(w io.Writer) (n int64, err error) {
 			props["parent"] = f.KeyFor(res.Parent)
 		}
 		if len(res.Children) > 0 {
-			childrenIds := res.Children.ToSlice()
-			sort.Sort(construct.SortedIds(childrenIds))
-			children := make([]string, len(childrenIds))
-			for i, child := range childrenIds {
-				children[i] = child.String()
-			}
+			children := res.Children.ToSlice()
+			sort.Sort(construct.SortedIds(children))
 			props["children"] = children
 		}
 
diff --git a/pkg/visualizer2/vis_graph.go b/pkg/visualizer2/vis_graph.go
index 9eb7fca1..c4abe6eb 100644
--- a/pkg/visualizer2/vis_graph.go
+++ b/pkg/visualizer2/vis_graph.go
@@ -2,6 +2,7 @@ package visualizer
 
 import (
 	"errors"
+	"sort"
 
 	"github.com/dominikbraun/graph"
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
@@ -18,6 +19,10 @@ type (
 		Children set.Set[construct.ResourceId]
 	}
 
+	VisEdgeData struct {
+		PathResources set.Set[construct.ResourceId]
+	}
+
 	VisGraph graph.Graph[construct.ResourceId, *VisResource]
 )
 
@@ -51,3 +56,15 @@ func ConstructToVis(g construct.Graph) (VisGraph, error) {
 	}
 	return vis, errs
 }
+
+func (d VisEdgeData) MarshalYAML() (interface{}, error) {
+	res := d.PathResources.ToSlice()
+	sort.Sort(construct.SortedIds(res))
+	return map[string]any{
+		// TODO infacopilot frontend currently just uses 'path' as the colledction of
+		// additional resources to show in the graph for that edge. We have more information
+		// we could give, but for compatibility until more is added to the frontend, just flatten
+		// everything and call it 'path'.
+		"path": res,
+	}, nil
+}


Test patch:
diff --git a/pkg/engine2/testdata/2_routes.dataflow-viz.yaml b/pkg/engine2/testdata/2_routes.dataflow-viz.yaml
index 7130dda8..c36389dc 100755
--- a/pkg/engine2/testdata/2_routes.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/2_routes.dataflow-viz.yaml
@@ -3,8 +3,8 @@ resources:
   lambda_function/lambda_function_1:
     children:
         - aws:ecr_image:lambda_function_1-image
+        - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function_1-ExecutionRole
-    parent: rest_api/rest_api_1
     tag: big
 
   lambda_function/lambda_function_0:
@@ -12,21 +12,13 @@ resources:
         - aws:ecr_image:lambda_function_0-image
         - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function_0-ExecutionRole
-        - aws:log_group:lambda_function_0-log-group
-        - aws:log_group:lambda_function_1-log-group
-    parent: rest_api/rest_api_1
     tag: big
 
   rest_api/rest_api_1:
-    children:
-        - aws:api_resource:rest_api_1:lambda0
-        - aws:api_resource:rest_api_1:lambda1
-        - aws:api_stage:rest_api_1:api_stage-0
-    tag: parent
-
-  aws:api_integration:rest_api_1/integ1:
     children:
         - aws:api_deployment:rest_api_1:api_deployment-0
+        - aws:api_integration:rest_api_1:integ0
+        - aws:api_integration:rest_api_1:integ1
         - aws:api_method:rest_api_1:integ0-api_method
         - aws:api_method:rest_api_1:integ1-api_method
         - aws:api_resource:rest_api_1:api_resource-0
@@ -34,25 +26,21 @@ resources:
         - aws:api_resource:rest_api_1:lambda0
         - aws:api_resource:rest_api_1:lambda1
         - aws:api_stage:rest_api_1:api_stage-0
-        - aws:lambda_permission:integ0-lambda_function_0
-        - aws:lambda_permission:integ1-lambda_function_1
+    tag: parent
+
+  aws:api_integration:rest_api_1/integ1:
     parent: rest_api/rest_api_1
     tag: big
 
   aws:api_integration:rest_api_1/integ1 -> lambda_function/lambda_function_1:
-  aws:api_integration:rest_api_1/integ0:
-    children:
-        - aws:api_deployment:rest_api_1:api_deployment-0
-        - aws:api_method:rest_api_1:integ0-api_method
-        - aws:api_method:rest_api_1:integ1-api_method
-        - aws:api_resource:rest_api_1:api_resource-0
-        - aws:api_resource:rest_api_1:api_resource-1
-        - aws:api_resource:rest_api_1:lambda0
-        - aws:api_resource:rest_api_1:lambda1
-        - aws:api_stage:rest_api_1:api_stage-0
-        - aws:lambda_permission:integ0-lambda_function_0
+    path:
         - aws:lambda_permission:integ1-lambda_function_1
+
+  aws:api_integration:rest_api_1/integ0:
     parent: rest_api/rest_api_1
     tag: big
 
   aws:api_integration:rest_api_1/integ0 -> lambda_function/lambda_function_0:
+    path:
+        - aws:lambda_permission:integ0-lambda_function_0
+
diff --git a/pkg/engine2/testdata/delete_namespace_resource.dataflow-viz.yaml b/pkg/engine2/testdata/delete_namespace_resource.dataflow-viz.yaml
index 6989b166..48b9543d 100644
--- a/pkg/engine2/testdata/delete_namespace_resource.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/delete_namespace_resource.dataflow-viz.yaml
@@ -5,6 +5,5 @@ resources:
         - aws:ecr_image:lambda_function_2-image
         - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function_2-ExecutionRole
-        - aws:log_group:lambda_function_2-log-group
     tag: big
 
diff --git a/pkg/engine2/testdata/delete_resource_and_iacdeps.dataflow-viz.yaml b/pkg/engine2/testdata/delete_resource_and_iacdeps.dataflow-viz.yaml
index c1861776..c3a3924f 100644
--- a/pkg/engine2/testdata/delete_resource_and_iacdeps.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/delete_resource_and_iacdeps.dataflow-viz.yaml
@@ -1,16 +1,15 @@
 provider: aws
 resources:
   rest_api/rest_api_0:
-    children:
-        - aws:api_stage:rest_api_0:api_stage-0
-    tag: parent
-
-  aws:api_integration:rest_api_0/rest_api_0_integration_0:
     children:
         - aws:api_deployment:rest_api_0:api_deployment-0
+        - aws:api_integration:rest_api_0:rest_api_0_integration_0
         - aws:api_method:rest_api_0:rest_api_0_integration_0_method
         - aws:api_resource:rest_api_0:api_resource-0
         - aws:api_stage:rest_api_0:api_stage-0
+    tag: parent
+
+  aws:api_integration:rest_api_0/rest_api_0_integration_0:
     parent: rest_api/rest_api_0
     tag: big
 
diff --git a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
index 405aa42e..9f728f2c 100755
--- a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
@@ -2,31 +2,23 @@ provider: aws
 resources:
   rds_instance/rds-instance-2:
     children:
-        - aws:iam_role:ecs_service_0-execution-role
         - aws:rds_subnet_group:rds_subnet_group-0
-        - aws:security_group:vpc-0:rds-instance-2-security_group
     parent: vpc/vpc-0
     tag: big
 
   vpc/vpc-0:
     children:
-        - aws:ecr_image:ecs_service_0-image
-        - aws:ecr_repo:ecr_repo-0
-        - aws:ecs_task_definition:ecs_service_0
-        - aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip
-        - aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
-        - aws:iam_role:ecs_service_0-execution-role
         - aws:internet_gateway:vpc-0:internet_gateway-0
-        - aws:log_group:ecs_service_0-log-group
-        - aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway
-        - aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
-        - aws:rds_subnet_group:rds_subnet_group-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:ecs_service_0-security_group
         - aws:security_group:vpc-0:rds-instance-2-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
 
   ecs_service/ecs_service_0:
@@ -34,9 +26,14 @@ resources:
         - aws:ecr_image:ecs_service_0-image
         - aws:ecr_repo:ecr_repo-0
         - aws:ecs_task_definition:ecs_service_0
+        - aws:iam_role:ecs_service_0-execution-role
         - aws:log_group:ecs_service_0-log-group
-        - aws:security_group:vpc-0:ecs_service_0-security_group
     parent: vpc/vpc-0
     tag: big
 
   ecs_service/ecs_service_0 -> rds_instance/rds-instance-2:
+    path:
+        - aws:ecs_task_definition:ecs_service_0
+        - aws:iam_role:ecs_service_0-execution-role
+        - aws:security_group:vpc-0:rds-instance-2-security_group
+
diff --git a/pkg/engine2/testdata/extend_graph.dataflow-viz.yaml b/pkg/engine2/testdata/extend_graph.dataflow-viz.yaml
index 2042458e..a43ab65d 100755
--- a/pkg/engine2/testdata/extend_graph.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/extend_graph.dataflow-viz.yaml
@@ -5,12 +5,14 @@ resources:
         - aws:ecr_image:lambda_function_0-image
         - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function_0-ExecutionRole
-        - aws:log_group:lambda_function_0-log-group
-    parent: rest_api/rest_api_1
     tag: big
 
   rest_api/rest_api_1:
     children:
+        - aws:api_deployment:rest_api_1:api_deployment-0
+        - aws:api_integration:rest_api_1:rest_api_1_integration_0
+        - aws:api_method:rest_api_1:rest_api_1_integration_0_method
+        - aws:api_resource:rest_api_1:api_resource-0
         - aws:api_stage:rest_api_1:api_stage-0
     tag: parent
 
@@ -18,13 +20,10 @@ resources:
     tag: big
 
   aws:api_integration:rest_api_1/rest_api_1_integration_0:
-    children:
-        - aws:api_deployment:rest_api_1:api_deployment-0
-        - aws:api_method:rest_api_1:rest_api_1_integration_0_method
-        - aws:api_resource:rest_api_1:api_resource-0
-        - aws:api_stage:rest_api_1:api_stage-0
-        - aws:lambda_permission:rest_api_1_integration_0_lambda_function_0
     parent: rest_api/rest_api_1
     tag: big
 
   aws:api_integration:rest_api_1/rest_api_1_integration_0 -> lambda_function/lambda_function_0:
+    path:
+        - aws:lambda_permission:rest_api_1_integration_0_lambda_function_0
+
diff --git a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
index 50a2f40a..f1792347 100755
--- a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
@@ -2,204 +2,75 @@ provider: aws
 resources:
   kubernetes:pod:eks_cluster-0/pod2:
     children:
-        - aws:ecr_image:pod2-ecr_image
-        - aws:ecr_repo:ecr_repo-0
+        - aws:iam_role:ClusterRole-eks_cluster-0
     parent: eks_cluster/eks_cluster-0
     tag: big
 
   load_balancer/rest-api-4-integbcc77100:
-    children:
-        - aws:api_deployment:rest_api_4:api_deployment-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:eks_add_on:amazon-cloudwatch-observability
-        - aws:eks_node_group:eks_node_group-0
-        - aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip
-        - aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
-        - aws:iam_oidc_provider:eks_cluster-0
-        - aws:iam_policy:iam_policy-0
-        - aws:iam_role:amazon-cloudwatch-observability-iam_role
-        - aws:iam_role:aws-load-balancer-controller
-        - aws:iam_role:eks_node_group-0-iam_role
-        - aws:iam_role:pod2
-        - aws:internet_gateway:vpc-0:internet_gateway-0
-        - aws:load_balancer_listener:rest_api_4_integration_0-pod2
-        - aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway
-        - aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
-        - 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:target_group:rest-api-4-integbcc77100
-        - aws:vpc_link:rest_api_4_integration_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: eks_cluster/eks_cluster-0
+    parent: vpc/vpc-0
     tag: big
 
   load_balancer/rest-api-4-integbcc77100 -> kubernetes:pod:eks_cluster-0/pod2:
-  kubernetes:helm_chart:eks_cluster-0/metricsserver:
-    children:
-        - aws:api_deployment:rest_api_4:api_deployment-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:eks_add_on:amazon-cloudwatch-observability
-        - aws:eks_node_group:eks_node_group-0
-        - aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip
-        - aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
-        - aws:iam_oidc_provider:eks_cluster-0
-        - aws:iam_policy:iam_policy-0
-        - aws:iam_role:amazon-cloudwatch-observability-iam_role
-        - aws:iam_role:aws-load-balancer-controller
-        - aws:iam_role:eks_node_group-0-iam_role
-        - aws:iam_role:pod2
-        - aws:internet_gateway:vpc-0:internet_gateway-0
+    path:
         - aws:load_balancer_listener:rest_api_4_integration_0-pod2
-        - aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway
-        - aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
-        - 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:target_group:rest-api-4-integbcc77100
-        - aws:vpc_link:rest_api_4_integration_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
+
+  kubernetes:helm_chart:eks_cluster-0/metricsserver:
+    children:
+        - 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:api_deployment:rest_api_4:api_deployment-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:eks_add_on:amazon-cloudwatch-observability
-        - aws:eks_node_group:eks_node_group-0
-        - aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip
-        - aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
-        - aws:iam_oidc_provider:eks_cluster-0
-        - aws:iam_policy:iam_policy-0
-        - aws:iam_role:amazon-cloudwatch-observability-iam_role
-        - aws:iam_role:aws-load-balancer-controller
-        - aws:iam_role:eks_node_group-0-iam_role
-        - aws:iam_role:pod2
-        - aws:internet_gateway:vpc-0:internet_gateway-0
-        - aws:load_balancer_listener:rest_api_4_integration_0-pod2
-        - aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway
-        - aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
-        - 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:target_group:rest-api-4-integbcc77100
-        - aws:vpc_link:rest_api_4_integration_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
     parent: eks_cluster/eks_cluster-0
     tag: big
 
   vpc/vpc-0:
     children:
-        - aws:api_deployment:rest_api_4:api_deployment-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:eks_add_on:amazon-cloudwatch-observability
-        - aws:eks_node_group:eks_node_group-0
-        - aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip
-        - aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
-        - aws:iam_oidc_provider:eks_cluster-0
-        - aws:iam_policy:iam_policy-0
-        - aws:iam_role:amazon-cloudwatch-observability-iam_role
-        - aws:iam_role:aws-load-balancer-controller
-        - aws:iam_role:eks_node_group-0-iam_role
-        - aws:iam_role:pod2
         - aws:internet_gateway:vpc-0:internet_gateway-0
-        - aws:load_balancer_listener:rest_api_4_integration_0-pod2
-        - aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway
-        - aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
         - 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:target_group:rest-api-4-integbcc77100
-        - aws:vpc_link:rest_api_4_integration_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: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
     tag: parent
 
   eks_cluster/eks_cluster-0:
     children:
-        - aws:eks_add_on:amazon-cloudwatch-observability
-        - aws:eks_add_on:vpc-cni
-        - aws:eks_node_group:eks_node_group-0
-        - aws:iam_oidc_provider:eks_cluster-0
-        - aws:iam_policy:iam_policy-0
         - aws:iam_role:ClusterRole-eks_cluster-0
-        - aws:iam_role:amazon-cloudwatch-observability-iam_role
-        - aws:iam_role:aws-load-balancer-controller
-        - aws:iam_role:eks_node_group-0-iam_role
-        - aws:iam_role:pod2
-        - aws:security_group:vpc-0:eks_cluster-0-security_group
-        - aws:target_group:rest-api-4-integbcc77100
+        - 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
 
   aws:api_integration:rest_api_4/rest_api_4_integration_0:
-    children:
-        - aws:api_deployment:rest_api_4:api_deployment-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:eks_add_on:amazon-cloudwatch-observability
-        - aws:eks_node_group:eks_node_group-0
-        - aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip
-        - aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
-        - aws:iam_oidc_provider:eks_cluster-0
-        - aws:iam_policy:iam_policy-0
-        - aws:iam_role:amazon-cloudwatch-observability-iam_role
-        - aws:iam_role:aws-load-balancer-controller
-        - aws:iam_role:eks_node_group-0-iam_role
-        - aws:iam_role:pod2
-        - aws:internet_gateway:vpc-0:internet_gateway-0
-        - aws:load_balancer_listener:rest_api_4_integration_0-pod2
-        - aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway
-        - aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
-        - 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:target_group:rest-api-4-integbcc77100
-        - aws:vpc_link:rest_api_4_integration_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: rest_api/rest_api_4
     tag: big
 
   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
+
diff --git a/pkg/engine2/testdata/k8s_api.expect.yaml b/pkg/engine2/testdata/k8s_api.expect.yaml
index d4ae3b2a..93670516 100755
--- a/pkg/engine2/testdata/k8s_api.expect.yaml
+++ b/pkg/engine2/testdata/k8s_api.expect.yaml
@@ -67,8 +67,8 @@ resources:
                         - aws:iam_oidc_provider:eks_cluster-0#Arn
             Version: "2012-10-17"
         ManagedPolicies:
-            - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
             - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess
+            - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
     aws:rest_api:rest_api_4:
         BinaryMediaTypes:
             - application/octet-stream
@@ -267,12 +267,12 @@ resources:
                         - ec2.amazonaws.com
             Version: "2012-10-17"
         ManagedPolicies:
-            - 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
+            - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
+            - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
+            - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
     aws:iam_role:pod2:
         AssumeRolePolicyDoc:
             Statement:
@@ -483,12 +483,6 @@ resources:
               Protocol: "-1"
               ToPort: 0
         IngressRules:
-            - CidrBlocks:
-                - 0.0.0.0/0
-              Description: Allows ingress traffic from the EKS control plane
-              FromPort: 9443
-              Protocol: TCP
-              ToPort: 9443
             - Description: Allow ingress traffic from within the same security group
               FromPort: 0
               Protocol: "-1"
@@ -506,6 +500,12 @@ resources:
               FromPort: 0
               Protocol: "-1"
               ToPort: 0
+            - CidrBlocks:
+                - 0.0.0.0/0
+              Description: Allows ingress traffic from the EKS control plane
+              FromPort: 9443
+              Protocol: TCP
+              ToPort: 9443
         Vpc: aws:vpc:vpc-0
     aws:route_table:vpc-0:subnet-0-route_table:
         Routes:
diff --git a/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml b/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml
index 90e7b643..1180a050 100755
--- a/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml
@@ -5,22 +5,11 @@ resources:
         - aws:efs_access_point:test-efs-fs:lambda_test_app-test-efs-fs
         - aws:efs_mount_target:test-efs-fs:lambda_test_app-test-efs-fs
         - aws:efs_mount_target:test-efs-fs:subnet-1-test-efs-fs
-        - aws:iam_role:lambda_test_app-ExecutionRole
-        - aws:log_group:lambda_test_app-log-group
-    parent: vpc/vpc-0
     tag: big
 
   vpc/vpc-0:
     children:
-        - aws:efs_mount_target:test-efs-fs:lambda_test_app-test-efs-fs
-        - aws:efs_mount_target:test-efs-fs:subnet-1-test-efs-fs
-        - aws:elastic_ip:lambda_test_app-test-efs-fs-route_table-nat_gateway-elastic_ip
-        - aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
-        - aws:iam_role:lambda_test_app-ExecutionRole
         - aws:internet_gateway:vpc-0:internet_gateway-0
-        - aws:log_group:lambda_test_app-log-group
-        - aws:nat_gateway:subnet-2:lambda_test_app-test-efs-fs-route_table-nat_gateway
-        - aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
         - aws:route_table:vpc-0:lambda_test_app-test-efs-fs-route_table
         - aws:route_table:vpc-0:subnet-1-route_table
         - aws:route_table:vpc-0:subnet-2-route_table
@@ -28,14 +17,28 @@ resources:
         - aws:security_group:vpc-0:lambda_test_app-security_group
         - aws:security_group:vpc-0:lambda_test_app-test-efs-fs
         - aws:security_group:vpc-0:subnet-1-test-efs-fs
+        - aws:subnet:vpc-0:lambda_test_app-test-efs-fs
+        - aws:subnet:vpc-0:subnet-1
+        - aws:subnet:vpc-0:subnet-2
+        - aws:subnet:vpc-0:subnet-3
     tag: parent
 
   lambda_function/lambda_test_app:
     children:
         - aws:ecr_image:lambda_test_app-image
         - aws:ecr_repo:ecr_repo-0
-        - aws:security_group:vpc-0:lambda_test_app-security_group
+        - aws:iam_role:lambda_test_app-ExecutionRole
     parent: vpc/vpc-0
     tag: big
 
   lambda_function/lambda_test_app -> efs_file_system/test-efs-fs:
+    path:
+        - aws:efs_access_point:test-efs-fs:lambda_test_app-test-efs-fs
+        - aws:efs_mount_target:test-efs-fs:lambda_test_app-test-efs-fs
+        - aws:efs_mount_target:test-efs-fs:subnet-1-test-efs-fs
+        - aws:iam_role:lambda_test_app-ExecutionRole
+        - aws:security_group:vpc-0:lambda_test_app-test-efs-fs
+        - aws:security_group:vpc-0:subnet-1-test-efs-fs
+        - aws:subnet:vpc-0:lambda_test_app-test-efs-fs
+        - aws:subnet:vpc-0:subnet-1
+
diff --git a/pkg/engine2/testdata/lambda_efs.expect.yaml b/pkg/engine2/testdata/lambda_efs.expect.yaml
index 7245f91b..dba34bea 100755
--- a/pkg/engine2/testdata/lambda_efs.expect.yaml
+++ b/pkg/engine2/testdata/lambda_efs.expect.yaml
@@ -172,16 +172,16 @@ resources:
               Protocol: "-1"
               ToPort: 0
         IngressRules:
-            - CidrBlocks:
-                - 10.0.128.0/18
-              Description: Allow ingress traffic from ip addresses within the subnet subnet-1
+            - Description: Allow ingress traffic from within the same security group
               FromPort: 0
               Protocol: "-1"
+              Self: true
               ToPort: 0
-            - Description: Allow ingress traffic from within the same security group
+            - CidrBlocks:
+                - 10.0.128.0/18
+              Description: Allow ingress traffic from ip addresses within the subnet subnet-1
               FromPort: 0
               Protocol: "-1"
-              Self: true
               ToPort: 0
         Vpc: aws:vpc:vpc-0
     aws:route_table:vpc-0:subnet-1-route_table:
diff --git a/pkg/engine2/testdata/namespace_pathselect.dataflow-viz.yaml b/pkg/engine2/testdata/namespace_pathselect.dataflow-viz.yaml
index 3d372719..de979fd9 100755
--- a/pkg/engine2/testdata/namespace_pathselect.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/namespace_pathselect.dataflow-viz.yaml
@@ -2,27 +2,23 @@ provider: aws
 resources:
   vpc/vpc_1:
     children:
-        - aws:elastic_ip:lambda_function_2-vpc_1-route_table-nat_gateway-elastic_ip
-        - aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
-        - aws:iam_role:lambda_function_0-ExecutionRole
-        - aws:iam_role:lambda_function_2-ExecutionRole
         - aws:internet_gateway:vpc_1:internet_gateway-0
-        - aws:log_group:lambda_function_0-log-group
-        - aws:log_group:lambda_function_2-log-group
-        - aws:nat_gateway:subnet-2:lambda_function_2-vpc_1-route_table-nat_gateway
-        - aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
         - aws:route_table:vpc_1:lambda_function_2-vpc_1-route_table
         - aws:route_table:vpc_1:subnet-1-route_table
         - aws:route_table:vpc_1:subnet-2-route_table
         - aws:route_table:vpc_1:subnet-3-route_table
         - aws:security_group:vpc_1:lambda_function_2-security_group
+        - aws:subnet:vpc_1:lambda_function_2-vpc_1
+        - aws:subnet:vpc_1:subnet-1
+        - aws:subnet:vpc_1:subnet-2
+        - aws:subnet:vpc_1:subnet-3
     tag: parent
 
   lambda_function/lambda_function_2:
     children:
         - aws:ecr_image:lambda_function_2-image
+        - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function_2-ExecutionRole
-        - aws:security_group:vpc_1:lambda_function_2-security_group
     parent: vpc/vpc_1
     tag: big
 
@@ -31,8 +27,5 @@ resources:
         - aws:ecr_image:lambda_function_0-image
         - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function_0-ExecutionRole
-        - aws:log_group:lambda_function_0-log-group
-        - aws:log_group:lambda_function_2-log-group
-    parent: vpc/vpc_1
     tag: big
 
diff --git a/pkg/engine2/testdata/remove_path.dataflow-viz.yaml b/pkg/engine2/testdata/remove_path.dataflow-viz.yaml
index 83e7a6de..7e8af91c 100644
--- a/pkg/engine2/testdata/remove_path.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/remove_path.dataflow-viz.yaml
@@ -3,22 +3,12 @@ resources:
   rds_instance/rds-instance-1:
     children:
         - aws:rds_subnet_group:rds_subnet_group-0
-        - aws:security_group:vpc-0:rds-instance-1-security_group
     parent: vpc/vpc-0
     tag: big
 
   vpc/vpc-0:
     children:
-        - aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip
-        - aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
-        - aws:iam_role:lambda_function_0-ExecutionRole
-        - aws:iam_role:lambda_function_3-ExecutionRole
         - aws:internet_gateway:vpc-0:internet_gateway-0
-        - aws:log_group:lambda_function_0-log-group
-        - aws:log_group:lambda_function_3-log-group
-        - aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway
-        - aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
-        - aws:rds_subnet_group:rds_subnet_group-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
@@ -26,13 +16,17 @@ resources:
         - aws:security_group:vpc-0:lambda_function_0-security_group
         - aws:security_group:vpc-0:lambda_function_3-security_group
         - aws:security_group:vpc-0:rds-instance-1-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
 
   lambda_function/lambda_function_3:
     children:
         - aws:ecr_image:lambda_function_3-image
+        - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function_3-ExecutionRole
-        - aws:security_group:vpc-0:lambda_function_3-security_group
     parent: vpc/vpc-0
     tag: big
 
@@ -41,10 +35,13 @@ resources:
         - aws:ecr_image:lambda_function_0-image
         - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function_0-ExecutionRole
-        - aws:log_group:lambda_function_0-log-group
-        - aws:log_group:lambda_function_3-log-group
-        - aws:security_group:vpc-0:lambda_function_0-security_group
     parent: vpc/vpc-0
     tag: big
 
   lambda_function/lambda_function_0 -> rds_instance/rds-instance-1:
+    path:
+        - aws:iam_role:lambda_function_0-ExecutionRole
+        - aws:security_group:vpc-0:rds-instance-1-security_group
+        - aws:subnet:vpc-0:subnet-0
+        - aws:subnet:vpc-0:subnet-1
+
diff --git a/pkg/engine2/testdata/remove_path.expect.yaml b/pkg/engine2/testdata/remove_path.expect.yaml
index 9d4b30dc..71afa2c3 100644
--- a/pkg/engine2/testdata/remove_path.expect.yaml
+++ b/pkg/engine2/testdata/remove_path.expect.yaml
@@ -215,6 +215,11 @@ resources:
               Protocol: "-1"
               ToPort: 0
         IngressRules:
+            - Description: Allow ingress traffic from within the same security group
+              FromPort: 0
+              Protocol: "-1"
+              Self: true
+              ToPort: 0
             - CidrBlocks:
                 - 10.0.128.0/18
               Description: Allow ingress traffic from ip addresses within the subnet subnet-0
@@ -227,11 +232,6 @@ resources:
               FromPort: 0
               Protocol: "-1"
               ToPort: 0
-            - Description: Allow ingress traffic from within the same security group
-              FromPort: 0
-              Protocol: "-1"
-              Self: true
-              ToPort: 0
         Vpc: aws:vpc:vpc-0
     aws:route_table:vpc-0:subnet-0-route_table:
         Routes:
diff --git a/pkg/engine2/testdata/rename.dataflow-viz.yaml b/pkg/engine2/testdata/rename.dataflow-viz.yaml
index 8342b915..54ce3b18 100644
--- a/pkg/engine2/testdata/rename.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/rename.dataflow-viz.yaml
@@ -1,16 +1,17 @@
 provider: aws
 resources:
   dynamodb_table/dynamodb_table_1:
-    children:
-        - aws:iam_role:lambda_function_0-ExecutionRole
-        - aws:log_group:lambda_function_0-log-group
-        - aws:log_group:new_lambda_name-log-group
     tag: big
 
   lambda_function/new_lambda_name:
     children:
         - aws:ecr_image:lambda_function_0-image
         - aws:ecr_repo:ecr_repo-0
+        - aws:iam_role:lambda_function_0-ExecutionRole
     tag: big
 
   lambda_function/new_lambda_name -> dynamodb_table/dynamodb_table_1:
+    path:
+        - aws:SERVICE_API:lambda_function_0-lambda_function_0-log-group
+        - aws:iam_role:lambda_function_0-ExecutionRole
+
diff --git a/pkg/engine2/testdata/single_lambda.dataflow-viz.yaml b/pkg/engine2/testdata/single_lambda.dataflow-viz.yaml
index ae0fd618..f96c54cf 100755
--- a/pkg/engine2/testdata/single_lambda.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/single_lambda.dataflow-viz.yaml
@@ -5,6 +5,5 @@ resources:
         - aws:ecr_image:lambda_function_0-image
         - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function_0-ExecutionRole
-        - aws:log_group:lambda_function_0-log-group
     tag: big
 
diff --git a/pkg/engine2/testdata/static_site.dataflow-viz.yaml b/pkg/engine2/testdata/static_site.dataflow-viz.yaml
index 45f661cf..af6a0210 100755
--- a/pkg/engine2/testdata/static_site.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/static_site.dataflow-viz.yaml
@@ -1,13 +1,13 @@
 provider: aws
 resources:
   s3_bucket/s3-bucket-0:
-    children:
-        - aws:s3_bucket_policy:s3_bucket_policy-0
     tag: big
 
   cloudfront_distribution/cloudfront_distribution_1:
-    children:
-        - aws:s3_bucket_policy:s3_bucket_policy-0
     tag: big
 
   cloudfront_distribution/cloudfront_distribution_1 -> s3_bucket/s3-bucket-0:
+    path:
+        - aws:cloudfront_origin_access_identity:cloudfront_origin_access_identity-0
+        - aws:s3_bucket_policy:s3_bucket_policy-0
+
diff --git a/pkg/engine2/testdata/vpc_import.dataflow-viz.yaml b/pkg/engine2/testdata/vpc_import.dataflow-viz.yaml
index d9450d94..ef77d5e5 100644
--- a/pkg/engine2/testdata/vpc_import.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/vpc_import.dataflow-viz.yaml
@@ -1,5 +1,10 @@
 provider: aws
 resources:
   vpc/vpc:
+    children:
+        - aws:subnet:vpc:subnet1
+        - aws:subnet:vpc:subnet2
+        - aws:subnet:vpc:subnet3
+        - aws:subnet:vpc:subnet4
     tag: parent
 
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 fe032689..4f5085f5 100644
--- a/pkg/engine2/testdata/vpc_import_to_lambda.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/vpc_import_to_lambda.dataflow-viz.yaml
@@ -2,9 +2,8 @@ provider: aws
 resources:
   vpc/vpc:
     children:
-        - aws:iam_role:lambda_function-ExecutionRole
-        - aws:log_group:lambda_function-log-group
         - aws:security_group:vpc:lambda_function-security_group
+        - aws:subnet:vpc:subnet1
     tag: parent
 
   lambda_function/lambda_function:
@@ -12,8 +11,6 @@ resources:
         - aws:ecr_image:lambda_function-image
         - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function-ExecutionRole
-        - aws:log_group:lambda_function-log-group
-        - aws:security_group:vpc:lambda_function-security_group
     parent: vpc/vpc
     tag: big
 
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 a44a958a..ae4f1887 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
@@ -2,18 +2,16 @@ provider: aws
 resources:
   vpc/vpc:
     children:
-        - aws:elastic_ip:lambda_function-vpc-route_table-nat_gateway-elastic_ip
-        - aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
-        - aws:iam_role:lambda_function-ExecutionRole
         - aws:internet_gateway:vpc:internet_gateway-0
-        - aws:log_group:lambda_function-log-group
-        - aws:nat_gateway:subnet-2:lambda_function-vpc-route_table-nat_gateway
-        - aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
         - aws:route_table:vpc:lambda_function-vpc-route_table
         - aws:route_table:vpc:subnet-1-route_table
         - aws:route_table:vpc:subnet-2-route_table
         - aws:route_table:vpc:subnet-3-route_table
         - aws:security_group:vpc:lambda_function-security_group
+        - aws:subnet:vpc:lambda_function-vpc
+        - aws:subnet:vpc:subnet-1
+        - aws:subnet:vpc:subnet-2
+        - aws:subnet:vpc:subnet-3
     tag: parent
 
   lambda_function/lambda_function:
@@ -21,8 +19,6 @@ resources:
         - aws:ecr_image:lambda_function-image
         - aws:ecr_repo:ecr_repo-0
         - aws:iam_role:lambda_function-ExecutionRole
-        - aws:log_group:lambda_function-log-group
-        - aws:security_group:vpc:lambda_function-security_group
     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 a906cf73..f90b59f7 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
@@ -40,8 +40,8 @@ resources:
                         - lambda.amazonaws.com
             Version: "2012-10-17"
         ManagedPolicies:
-            - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
             - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+            - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
     aws:subnet:vpc:lambda_function-vpc:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-0
         CidrBlock: 10.0.128.0/18
diff --git a/pkg/knowledge_base2/graph_test.go b/pkg/knowledge_base2/graph_test.go
deleted file mode 100644
index 57ffdb71..00000000
--- a/pkg/knowledge_base2/graph_test.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package knowledgebase2
-
-// import (
-// 	"testing"
-
-// 	construct "github.com/klothoplatform/klotho/pkg/construct2"
-// )
-
-// func Test_IsOperationalResourceSideEffect(t *testing.T) {
-// 	type args struct {
-// 		dag       construct.Graph
-// 		templates []*ResourceTemplate
-// 		rid       construct.ResourceId
-// 		id        construct.ResourceId
-// 	}
-// 	tests := []struct {
-// 		name    string
-// 		args    args
-// 		want    bool
-// 		wantErr bool
-// 	}{
-// 		{},
-// 	}
-// 	for _, tt := range tests {
-// 		t.Run(tt.name, func(t *testing.T) {
-// 			kb := &KnowledgeBase{}
-// 			for _, template := range tt.args.templates {
-// 				kb.AddResourceTemplate(template)
-// 			}
-
-// 			got, err := IsOperationalResourceSideEffect(tt.args.dag, tt.args.kb, tt.args.rid, tt.args.id)
-// 			if (err != nil) != tt.wantErr {
-// 				t.Errorf("IsOperationalResourceSideEffect() error = %v, wantErr %v", err, tt.wantErr)
-// 				return
-// 			}
-// 			if got != tt.want {
-// 				t.Errorf(
-// 					"IsOperationalResourceSideEffect() got = %v, want %v",
-// 					got, tt.want,
-// 				)
-// 			}
-// 		})
-// 	}
-// }
diff --git a/pkg/knowledge_base2/kbtesting/graph_test.go b/pkg/knowledge_base2/kbtesting/graph_test.go
new file mode 100644
index 00000000..2e3bf691
--- /dev/null
+++ b/pkg/knowledge_base2/kbtesting/graph_test.go
@@ -0,0 +1,168 @@
+package kbtesting
+
+import (
+	"fmt"
+	"testing"
+
+	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/construct2/graphtest"
+	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
+	"github.com/klothoplatform/klotho/pkg/knowledge_base2/properties"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestDownstream(t *testing.T) {
+	// The following variables are provided as a convenience for writing tests that all can use the same
+	// basic structure / parameters
+	defaultGraph := graphtest.MakeGraph(t, construct.NewGraph(),
+		"p:t:A -> p:t:B -> p:t:C", // A -> B -> C all within the same operational boundary
+		"p:t:B -> p:t:D",          // B -> D crosses operational boundaries
+		"p:t:B -> p:t:X -> p:t:Y", // X is a functional boundary
+	)
+	defaultResource := graphtest.ParseId(t, "p:t:A")
+	defaultKB := func(t *testing.T, kb *MockKB) {
+		named := func(name string) func(construct.ResourceId) bool {
+			return func(id construct.ResourceId) bool {
+				return id.Name == name
+			}
+		}
+		makeOpProperty := func(name string, namespace bool) knowledgebase.Property {
+			p := &properties.ResourceProperty{}
+			p.Name = name
+			p.Path = name
+			p.Namespace = namespace
+			p.OperationalRule = &knowledgebase.PropertyRule{
+				Step: knowledgebase.OperationalStep{
+					Resources: []knowledgebase.ResourceSelector{{Selector: "p:t:" + name}},
+				},
+			}
+			return p
+		}
+
+		kb.On("GetResourceTemplate", mock.MatchedBy(named("A"))).Return(&knowledgebase.ResourceTemplate{
+			Classification: knowledgebase.Classification{Is: []string{"compute"}},
+			Properties: knowledgebase.Properties{
+				"B": makeOpProperty("B", false),
+			},
+		}, nil)
+		kb.On("GetResourceTemplate", mock.MatchedBy(named("B"))).Return(&knowledgebase.ResourceTemplate{
+			Properties: knowledgebase.Properties{
+				"C": makeOpProperty("C", false),
+				"D": &properties.ResourceProperty{}, // not an operational property
+			},
+		}, nil)
+		kb.On("GetResourceTemplate", mock.MatchedBy(named("X"))).Return(&knowledgebase.ResourceTemplate{
+			Classification: knowledgebase.Classification{Is: []string{"compute"}},
+		}, nil)
+		kb.On("GetResourceTemplate", mock.Anything).Return(&knowledgebase.ResourceTemplate{}, nil)
+	}
+	setProp := func(idStr string, prop string, resValueStr string) {
+		id := graphtest.ParseId(t, idStr)
+		resValue := graphtest.ParseId(t, resValueStr)
+		r, err := defaultGraph.Vertex(id)
+		if err != nil {
+			t.Fatal(fmt.Errorf("error setting property %s.%s = %s: %w", idStr, prop, resValueStr, err))
+		}
+		err = r.SetProperty(prop, resValue)
+		if err != nil {
+			t.Fatal(fmt.Errorf("error setting property %s.%s = %s: %w", idStr, prop, resValueStr, err))
+		}
+	}
+	setProp("p:t:A", "B", "p:t:B")
+	setProp("p:t:B", "C", "p:t:C")
+	setProp("p:t:B", "D", "p:t:D")
+
+	tests := []struct {
+		name     string
+		graph    []any
+		resource string
+		layer    knowledgebase.DependencyLayer
+		kbmock   func(*testing.T, *MockKB)
+		want     []string
+		wantErr  bool
+	}{
+		// Local
+		{
+			name:  "local",
+			layer: knowledgebase.ResourceLocalLayer,
+			want:  []string{"p:t:B", "p:t:C"},
+		},
+		// Direct
+		{
+			name:     "no downstream",
+			resource: "p:t:C",
+			layer:    knowledgebase.ResourceDirectLayer,
+		},
+		{
+			name:  "direct downstream",
+			layer: knowledgebase.ResourceDirectLayer,
+			want:  []string{"p:t:B"},
+		},
+		{
+			name:     "direct downstream B",
+			layer:    knowledgebase.ResourceDirectLayer,
+			resource: "p:t:B",
+			want:     []string{"p:t:C", "p:t:D", "p:t:X"},
+		},
+		// Glue
+		{
+			name:  "glue",
+			layer: knowledgebase.ResourceGlueLayer,
+			want:  []string{"p:t:B", "p:t:C", "p:t:D"},
+		},
+		// Functional
+		{
+			name:  "functional",
+			layer: knowledgebase.FirstFunctionalLayer,
+			want:  []string{"p:t:B", "p:t:C", "p:t:D", "p:t:X"},
+		},
+		// All
+		{
+			name:  "all",
+			layer: knowledgebase.AllDepsLayer,
+			want:  []string{"p:t:B", "p:t:C", "p:t:D", "p:t:X", "p:t:Y"},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+
+			var rid construct.ResourceId
+			if tt.resource != "" {
+				rid = graphtest.ParseId(t, tt.resource)
+			} else {
+				rid = defaultResource
+			}
+
+			var g construct.Graph
+			if tt.graph != nil {
+				g = graphtest.MakeGraph(t, construct.NewGraph(), tt.graph...)
+			} else {
+				g = defaultGraph
+			}
+
+			kb := &MockKB{}
+			if tt.kbmock != nil {
+				tt.kbmock(t, kb)
+			} else {
+				defaultKB(t, kb)
+			}
+			gotIds, err := knowledgebase.Downstream(g, kb, rid, tt.layer)
+			if tt.wantErr {
+				assert.Error(err)
+				return
+			} else if !assert.NoError(err) {
+				return
+			}
+			var got []string
+			if gotIds != nil {
+				got = make([]string, len(gotIds))
+				for i, w := range gotIds {
+					got[i] = w.String()
+				}
+			}
+			assert.ElementsMatch(tt.want, got)
+		})
+	}
+}
diff --git a/pkg/knowledge_base2/kbtesting/mock_kb.go b/pkg/knowledge_base2/kbtesting/mock_kb.go
index 33ca7873..6f7b9274 100644
--- a/pkg/knowledge_base2/kbtesting/mock_kb.go
+++ b/pkg/knowledge_base2/kbtesting/mock_kb.go
@@ -1,4 +1,4 @@
-package enginetesting
+package kbtesting
 
 import (
 	"github.com/dominikbraun/graph"
@@ -16,6 +16,11 @@ func (m *MockKB) ListResources() []*knowledgebase.ResourceTemplate {
 	return args.Get(0).([]*knowledgebase.ResourceTemplate)
 }
 
+func (m *MockKB) GetModel(model string) *knowledgebase.Model {
+	args := m.Called(model)
+	return args.Get(0).(*knowledgebase.Model)
+}
+
 func (m *MockKB) Edges() ([]graph.Edge[*knowledgebase.ResourceTemplate], error) {
 	args := m.Called()
 	return args.Get(0).([]graph.Edge[*knowledgebase.ResourceTemplate]), args.Error(1)
@@ -64,11 +69,18 @@ func (m *MockKB) GetClassification(id construct.ResourceId) knowledgebase.Classi
 	args := m.Called(id)
 	return args.Get(0).(knowledgebase.Classification)
 }
-func (m *MockKB) GetResourcesNamespaceResource(resource *construct.Resource) construct.ResourceId {
+func (m *MockKB) GetResourcesNamespaceResource(resource *construct.Resource) (construct.ResourceId, error) {
 	args := m.Called(resource)
-	return args.Get(0).(construct.ResourceId)
+	return args.Get(0).(construct.ResourceId), args.Error(1)
 }
 func (m *MockKB) GetResourcePropertyType(resource construct.ResourceId, propertyName string) string {
 	args := m.Called(resource, propertyName)
 	return args.String(0)
 }
+
+func (m *MockKB) GetPathSatisfactionsFromEdge(
+	source, target construct.ResourceId,
+) ([]knowledgebase.EdgePathSatisfaction, error) {
+	args := m.Called(source, target)
+	return args.Get(0).([]knowledgebase.EdgePathSatisfaction), args.Error(1)
+}