Repository: klothoplatform/klotho
Pull request #830 (2023-12-18 15:35:36)

return descriptions of properties
return description as a part of list resource types

### 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/compare.sh b/compare.sh
index c3278b46..78891b61 100755
--- a/compare.sh
+++ b/compare.sh
@@ -12,10 +12,13 @@ fi
 
 rm out.log out2.log
 export NO_COLOR=1
-export COLUMNS=80
-go run ./cmd/engine Run -i "$file" -o "./out/$(basename $file .yaml)" -v 2> out.log
+# Set columns really high so that the right-aligned fields that we grep for are on the same line as the message.
+export COLUMNS=1000
+KLOTHO_DEBUG_DIR="./out/$(basename $file .yaml)" \
+    go run ./cmd/engine Run -i "$file" -o "./out/$(basename $file .yaml)" -v 2> out.log
 sleep 2
-go run ./cmd/engine Run -i "$file" -o "./out/$(basename $file .yaml)2" -v 2> out2.log
+KLOTHO_DEBUG_DIR="./out/$(basename $file .yaml)2" \
+    go run ./cmd/engine Run -i "$file" -o "./out/$(basename $file .yaml)2" -v 2> out2.log
 
 rm out.queue.log out2.queue.log
 grep -E -e 'op: dequeue|eval|poll-deps' -e 'Satisfied' out.log > out.queue.log
diff --git a/pkg/engine2/cli.go b/pkg/engine2/cli.go
index 6c8f8ca1..2bf51571 100644
--- a/pkg/engine2/cli.go
+++ b/pkg/engine2/cli.go
@@ -180,6 +180,7 @@ func addSubProperties(properties map[string]any, subProperties map[string]knowle
 			"deployTime":            details.DeployTime,
 			"configurationDisabled": details.ConfigurationDisabled,
 			"required":              details.Required,
+			"description":           details.Description,
 		}
 		for _, validationField := range validationFields {
 			valField := reflect.ValueOf(subProperty).Elem().FieldByName(validationField)
diff --git a/pkg/engine2/operational_eval/dot.go b/pkg/engine2/operational_eval/dot.go
index 3896409d..fd5e7f8a 100644
--- a/pkg/engine2/operational_eval/dot.go
+++ b/pkg/engine2/operational_eval/dot.go
@@ -39,10 +39,10 @@ func keyAttributes(eval *Evaluator, key Key) map[string]string {
 		if key.PathSatisfication.Classification != "" {
 			extra = append(extra, fmt.Sprintf("<%s>", key.PathSatisfication.Classification))
 		}
-		if propertyReferenceInfluencesEdge(key.PathSatisfication.Target) {
+		if propertyReferenceChangesBoundary(key.PathSatisfication.Target) {
 			extra = append(extra, fmt.Sprintf("target#%s", key.PathSatisfication.Target.PropertyReference))
 		}
-		if propertyReferenceInfluencesEdge(key.PathSatisfication.Source) {
+		if propertyReferenceChangesBoundary(key.PathSatisfication.Source) {
 			extra = append(extra, fmt.Sprintf("source#%s", key.PathSatisfication.Target.PropertyReference))
 		}
 		if len(extra) > 0 {
diff --git a/pkg/engine2/operational_eval/evaluator.go b/pkg/engine2/operational_eval/evaluator.go
index ea56edff..8a6f3791 100644
--- a/pkg/engine2/operational_eval/evaluator.go
+++ b/pkg/engine2/operational_eval/evaluator.go
@@ -125,10 +125,10 @@ func (key Key) String() string {
 		if key.PathSatisfication.Classification != "" {
 			args = append(args, fmt.Sprintf("<%s>", key.PathSatisfication.Classification))
 		}
-		if propertyReferenceInfluencesEdge(key.PathSatisfication.Target) {
+		if propertyReferenceChangesBoundary(key.PathSatisfication.Target) {
 			args = append(args, fmt.Sprintf("target#%s", key.PathSatisfication.Target.PropertyReference))
 		}
-		if propertyReferenceInfluencesEdge(key.PathSatisfication.Source) {
+		if propertyReferenceChangesBoundary(key.PathSatisfication.Source) {
 			args = append(args, fmt.Sprintf("source#%s", key.PathSatisfication.Target.PropertyReference))
 		}
 		return fmt.Sprintf("Expand(%s)", strings.Join(args, ", "))
@@ -359,6 +359,9 @@ func (changes graphChanges) addEdge(source, target Key) {
 
 // addEdges is a convenient lower-level add for [graphChanges.edges] for multiple targets
 func (changes graphChanges) addEdges(source Key, targets set.Set[Key]) {
+	if len(targets) == 0 {
+		return
+	}
 	out, ok := changes.edges[source]
 	if !ok {
 		out = make(set.Set[Key])
diff --git a/pkg/engine2/operational_eval/graph.go b/pkg/engine2/operational_eval/graph.go
index 5856d52c..87e24d0d 100644
--- a/pkg/engine2/operational_eval/graph.go
+++ b/pkg/engine2/operational_eval/graph.go
@@ -94,7 +94,7 @@ func (eval *Evaluator) pathVertices(source, target construct.ResourceId) (graphC
 		// We are checking to see if either of the source or target nodes will change due to property references,
 		// if there are property references we want to ensure the correct dependency ordering is in place so
 		// we cannot yet split the expansion vertex up or build the temp graph
-		if propertyReferenceInfluencesEdge(satisfication.Source) || propertyReferenceInfluencesEdge(satisfication.Target) {
+		if propertyReferenceChangesBoundary(satisfication.Source) || propertyReferenceChangesBoundary(satisfication.Target) {
 			buildTempGraph = false
 		}
 
diff --git a/pkg/engine2/operational_eval/vertex_path_expand.go b/pkg/engine2/operational_eval/vertex_path_expand.go
index 9609830f..545704a2 100644
--- a/pkg/engine2/operational_eval/vertex_path_expand.go
+++ b/pkg/engine2/operational_eval/vertex_path_expand.go
@@ -13,6 +13,7 @@ import (
 	"github.com/klothoplatform/klotho/pkg/engine2/solution_context"
 	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	"github.com/klothoplatform/klotho/pkg/set"
+	"go.uber.org/zap"
 )
 
 type (
@@ -33,6 +34,13 @@ func (v *pathExpandVertex) Evaluate(eval *Evaluator) error {
 	if err != nil {
 		errs = errors.Join(errs, fmt.Errorf("could not get expansions to run: %w", err))
 	}
+	log := eval.Log()
+	if len(expansions) > 1 && log.Desugar().Core().Enabled(zap.DebugLevel) {
+		log.Debugf("Expansion %s subexpansions:", v.Edge)
+		for _, expansion := range expansions {
+			log.Debugf(" %s -> %s", expansion.Dep.Source.ID, expansion.Dep.Target.ID)
+		}
+	}
 
 	for _, expansion := range expansions {
 		err := v.runExpansion(eval, expansion)
@@ -292,7 +300,6 @@ func (v *pathExpandVertex) handleResultProperties(
 }
 
 func (v *pathExpandVertex) getExpansionsToRun(eval *Evaluator) ([]path_selection.ExpansionInput, error) {
-	var result []path_selection.ExpansionInput
 	var errs error
 	sourceRes, err := eval.Solution.RawView().Vertex(v.Edge.Source)
 	if err != nil {
@@ -308,7 +315,8 @@ func (v *pathExpandVertex) getExpansionsToRun(eval *Evaluator) ([]path_selection
 		errs = errors.Join(errs, err)
 	}
 
-	for _, expansion := range expansions {
+	result := make([]path_selection.ExpansionInput, len(expansions))
+	for i, expansion := range expansions {
 		input := path_selection.ExpansionInput{
 			Dep:            expansion.Dep,
 			Classification: expansion.Classification,
@@ -321,16 +329,9 @@ func (v *pathExpandVertex) getExpansionsToRun(eval *Evaluator) ([]path_selection
 				errs = errors.Join(errs, fmt.Errorf("error getting expansions to run. could not build path selection graph: %w", err))
 				continue
 			}
-			temp, err := tempGraph.Clone()
-			if err != nil {
-				errs = errors.Join(errs, fmt.Errorf("error getting expansions to run. could not clone path selection graph: %w", err))
-				continue
-			}
-			input.TempGraph = temp
-			result = append(result, input)
-		} else {
-			result = append(result, input)
+			input.TempGraph = tempGraph
 		}
+		result[i] = input
 	}
 	return result, errs
 }
@@ -471,15 +472,8 @@ func (v *pathExpandVertex) Dependencies(eval *Evaluator) (graphChanges, error) {
 	changes := newChanges()
 	srcKey := v.Key()
 
-	var errs error
-	if propertyReferenceInfluencesEdge(v.Satisfication.Source) {
-		keys := getDepsForPropertyRef(eval.Solution, v.Edge.Source, v.Satisfication.Source.PropertyReference)
-		changes.addEdges(srcKey, keys)
-	}
-	if propertyReferenceInfluencesEdge(v.Satisfication.Target) {
-		keys := getDepsForPropertyRef(eval.Solution, v.Edge.Target, v.Satisfication.Target.PropertyReference)
-		changes.addEdges(srcKey, keys)
-	}
+	changes.addEdges(srcKey, getDepsForPropertyRef(eval.Solution, v.Edge.Source, v.Satisfication.Source.PropertyReference))
+	changes.addEdges(srcKey, getDepsForPropertyRef(eval.Solution, v.Edge.Target, v.Satisfication.Target.PropertyReference))
 
 	// if we have a temp graph we can analyze the paths in it for possible dependencies on property vertices
 	// if we dont, we should return what we currently have
@@ -489,6 +483,7 @@ func (v *pathExpandVertex) Dependencies(eval *Evaluator) (graphChanges, error) {
 		return changes, nil
 	}
 
+	var errs error
 	srcDeps, err := construct.AllDownstreamDependencies(v.TempGraph, v.Edge.Source)
 	if err != nil {
 		return changes, err
@@ -523,7 +518,7 @@ func DeterminePathSatisfactionInputs(
 	srcIds := []construct.ResourceId{edge.Source.ID}
 	targetIds := []construct.ResourceId{edge.Target.ID}
 	var err error
-	if propertyReferenceInfluencesEdge(satisfaction.Source) {
+	if propertyReferenceChangesBoundary(satisfaction.Source) {
 		srcIds, err = solution_context.GetResourcesFromPropertyReference(sol, edge.Source.ID, satisfaction.Source.PropertyReference)
 		if err != nil {
 			errs = errors.Join(errs, fmt.Errorf(
@@ -532,7 +527,7 @@ func DeterminePathSatisfactionInputs(
 			))
 		}
 	}
-	if propertyReferenceInfluencesEdge(satisfaction.Target) {
+	if propertyReferenceChangesBoundary(satisfaction.Target) {
 		targetIds, err = solution_context.GetResourcesFromPropertyReference(sol, edge.Target.ID, satisfaction.Target.PropertyReference)
 		if err != nil {
 			errs = errors.Join(errs, fmt.Errorf(
@@ -584,6 +579,9 @@ func getDepsForPropertyRef(
 	res construct.ResourceId,
 	propertyRef string,
 ) set.Set[Key] {
+	if propertyRef == "" {
+		return nil
+	}
 	keys := make(set.Set[Key])
 	cfgCtx := solution_context.DynamicCtx(sol)
 	currResources := []construct.ResourceId{res}
@@ -591,9 +589,10 @@ func getDepsForPropertyRef(
 	for _, part := range parts {
 		var nextResources []construct.ResourceId
 		for _, currResource := range currResources {
+			keys.Add(Key{Ref: construct.PropertyRef{Resource: currResource, Property: part}})
 			val, err := cfgCtx.FieldValue(part, currResource)
 			if err != nil {
-				keys.Add(Key{Ref: construct.PropertyRef{Resource: currResource, Property: part}})
+				// The field hasn't resolved yet. Skip it for now, future calls to dependencies will pick it up.
 				continue
 			}
 			if id, ok := val.(construct.ResourceId); ok {
@@ -607,7 +606,9 @@ func getDepsForPropertyRef(
 	return keys
 }
 
-func propertyReferenceInfluencesEdge(v knowledgebase.PathSatisfactionRoute) bool {
+// propertyReferenceChangesBoundary returns whether the [PathSatisfactionRoute] changes the boundary of the path
+// via satisfaction rules. Note: validity checks do not change the boundary, so those return false.
+func propertyReferenceChangesBoundary(v knowledgebase.PathSatisfactionRoute) bool {
 	if v.Validity != "" {
 		return false
 	}
diff --git a/pkg/engine2/path_selection/candidate_validity.go b/pkg/engine2/path_selection/candidate_validity.go
index f8b05b42..66975ea5 100644
--- a/pkg/engine2/path_selection/candidate_validity.go
+++ b/pkg/engine2/path_selection/candidate_validity.go
@@ -31,11 +31,6 @@ func checkCandidatesValidity(
 	if len(path) <= 3 {
 		return true, nil
 	}
-	nonBoundaryResources := path[1 : len(path)-1]
-	matchIdx := matchesNonBoundary(resource.ID, nonBoundaryResources)
-	if matchIdx < 0 {
-		return false, nil
-	}
 	rt, err := ctx.KnowledgeBase().GetResourceTemplate(resource.ID)
 	if err != nil || rt == nil {
 		return false, err
@@ -43,27 +38,23 @@ func checkCandidatesValidity(
 
 	var errs error
 	// check validity of candidate being a target if not direct edge to source
-	if matchIdx >= 1 {
-		valid, err := checkAsTargetValidity(ctx, resource, path[:matchIdx+1], classification)
-		if err != nil {
-			errs = errors.Join(errs, err)
-		}
-		if !valid {
-			zap.S().Debugf("candidate %s is not valid as target", resource.ID)
-			return false, errs
-		}
+	valid, err := checkAsTargetValidity(ctx, resource, path[0], classification)
+	if err != nil {
+		errs = errors.Join(errs, err)
+	}
+	if !valid {
+		zap.S().Debugf("candidate %s is not valid as target", resource.ID)
+		return false, errs
 	}
 
 	// check validity of candidate being a source if not direct edge to target
-	if matchIdx <= len(path)-3 {
-		valid, err := checkAsSourceValidity(ctx, resource, path[matchIdx:], classification)
-		if err != nil {
-			errs = errors.Join(errs, err)
-		}
-		if !valid {
-			zap.S().Debugf("candidate %s is not valid as source", resource.ID)
-			return false, errs
-		}
+	valid, err = checkAsSourceValidity(ctx, resource, path[len(path)-1], classification)
+	if err != nil {
+		errs = errors.Join(errs, err)
+	}
+	if !valid {
+		zap.S().Debugf("candidate %s is not valid as source", resource.ID)
+		return false, errs
 	}
 	return true, errs
 }
@@ -102,7 +93,7 @@ func checkNamespaceValidity(
 func checkAsTargetValidity(
 	ctx solution_context.SolutionContext,
 	resource *construct.Resource,
-	path []construct.ResourceId,
+	source construct.ResourceId,
 	classification string,
 ) (bool, error) {
 	rt, err := ctx.KnowledgeBase().GetResourceTemplate(resource.ID)
@@ -122,15 +113,18 @@ func checkAsTargetValidity(
 				if err != nil {
 					// dont return error because it just means that the property isnt set and we can make the
 					// resource valid
-					zap.S().Debug("no resource available from resource %s from property ref %s", resource.ID, ps.PropertyReference)
+					zap.S().Debugf(
+						"no resource available from resource %s from property ref %s: %v",
+						resource.ID, ps.PropertyReference, err,
+					)
 				}
 				if len(resources) == 0 {
-					err = assignForValidity(ctx, resource, path[0], ps)
+					err = assignForValidity(ctx, resource, source, ps)
 					errs = errors.Join(errs, err)
 				}
 			}
 			for _, res := range resources {
-				valid, err := checkValidityOperation(ctx, path[0], res, ps)
+				valid, err := checkValidityOperation(ctx, source, res, ps)
 				if err != nil {
 					errs = errors.Join(errs, err)
 				}
@@ -149,7 +143,7 @@ func checkAsTargetValidity(
 func checkAsSourceValidity(
 	ctx solution_context.SolutionContext,
 	resource *construct.Resource,
-	path []construct.ResourceId,
+	target construct.ResourceId,
 	classification string,
 ) (bool, error) {
 	rt, err := ctx.KnowledgeBase().GetResourceTemplate(resource.ID)
@@ -169,15 +163,18 @@ func checkAsSourceValidity(
 				if err != nil {
 					// dont return error because it just means that the property isnt set and we can make the
 					// resource valid
-					zap.S().Debug("no resource available from resource %s from property ref %s", resource.ID, ps.PropertyReference)
+					zap.S().Debugf(
+						"no resource available from resource %s from property ref %s: %v",
+						resource.ID, ps.PropertyReference, err,
+					)
 				}
 				if len(resources) == 0 {
-					err = assignForValidity(ctx, resource, path[len(path)-1], ps)
+					err = assignForValidity(ctx, resource, target, ps)
 					errs = errors.Join(errs, err)
 				}
 			}
 			for _, res := range resources {
-				valid, err := checkValidityOperation(ctx, res, path[len(path)-1], ps)
+				valid, err := checkValidityOperation(ctx, res, target, ps)
 				if err != nil {
 					errs = errors.Join(errs, err)
 				}
@@ -256,12 +253,12 @@ func (d downstreamChecker) makeValid(resource, operationResource *construct.Reso
 			val, err := knowledgebase.TransformToPropertyValue(r, property, downstream, cfgCtx,
 				knowledgebase.DynamicValueData{Resource: r})
 			if err != nil || val == nil {
-				continue // Becuase this error may just mean that its not the right type of resource
+				continue // Because this error may just mean that its not the right type of resource
 			}
 			// We need to check if the current resource is what we are operating on and if so not search our raw view
 			// this is because it could be a phantom resource
 			var currRes *construct.Resource
-			if resource.ID.Matches(r) {
+			if resource.ID == r {
 				currRes = resource
 			} else {
 				currRes, err = d.ctx.RawView().Vertex(r)
@@ -297,7 +294,7 @@ func (d downstreamChecker) makeValid(resource, operationResource *construct.Reso
 		}
 		currResources = nextResources
 	}
-	return nil
+	return errs
 }
 
 // isValid checks if the candidate is valid based on what is downstream of the resourceToCheck
diff --git a/pkg/engine2/path_selection/path_expansion.go b/pkg/engine2/path_selection/path_expansion.go
index e48c9889..889e6ad0 100644
--- a/pkg/engine2/path_selection/path_expansion.go
+++ b/pkg/engine2/path_selection/path_expansion.go
@@ -78,7 +78,10 @@ func expandEdge(
 	var errs error
 	// represents id to qualified type because we dont need to do that processing more than once
 	for _, path := range paths {
-		errs = errors.Join(errs, expandPath(ctx, input, path, g))
+		err := expandPath(ctx, input, path, g)
+		if err != nil {
+			errs = errors.Join(errs, fmt.Errorf("error expanding path %s: %w", construct.Path(path), err))
+		}
 	}
 	if errs != nil {
 		return nil, errs
@@ -111,10 +114,14 @@ func renameAndReplaceInTempGraph(
 	var errs error
 	name := fmt.Sprintf("%s-%s", input.Dep.Source.ID.Name, input.Dep.Target.ID.Name)
 	// rename phantom nodes
-	result := make(construct.Path, len(path))
+	result := make([]*construct.Resource, len(path))
 	for i, id := range path {
+		r, props, err := input.TempGraph.VertexWithProperties(id)
+		if err != nil {
+			errs = errors.Join(errs, err)
+			continue
+		}
 		if strings.HasPrefix(id.Name, PHANTOM_PREFIX) {
-			oldId := id
 			id.Name = name
 			// because certain resources may be namespaced, we will check against all resource type names
 			currNames, err := getCurrNames(ctx, &id)
@@ -128,23 +135,45 @@ func renameAndReplaceInTempGraph(
 				}
 				id.Name = fmt.Sprintf("%s-%d", name, suffix)
 			}
-			_, props, err := input.TempGraph.VertexWithProperties(oldId)
-			if err == nil && props.Attributes != nil {
+			if props.Attributes != nil {
 				props.Attributes["new_id"] = id.String()
 			}
+			phantomRes := r
+			r, err = knowledgebase.CreateResource(ctx.KnowledgeBase(), id)
+			if err != nil {
+				errs = errors.Join(errs, err)
+				continue
+			}
+			r.Properties = phantomRes.Properties
+			phantomRes.Properties = make(construct.Properties)
+		}
+		err = g.AddVertex(r)
+		switch {
+		case errors.Is(err, graph.ErrVertexAlreadyExists):
+			r, err = g.Vertex(id)
+			if err != nil {
+				errs = errors.Join(errs, err)
+				continue
+			}
+
+		case err != nil:
+			errs = errors.Join(errs, err)
+			continue
+		}
+		result[i] = r
+		if i > 0 {
+			err = g.AddEdge(result[i-1].ID, r.ID)
+			if err != nil && !errors.Is(err, graph.ErrEdgeAlreadyExists) {
+				errs = errors.Join(errs, fmt.Errorf("error adding edge for path[%d]: %w", i, err))
+			}
 		}
-		result[i] = id
-	}
-	resultResources, err := addPathToGraph(ctx, g, result)
-	if err != nil {
-		return nil, errors.Join(errs, err)
 	}
 
 	// We need to replace the phantom nodes in the temp graph in case we reuse the temp graph for sub expansions
-	for i, res := range resultResources {
+	for i, res := range result {
 		errs = errors.Join(errs, construct.ReplaceResource(input.TempGraph, path[i], res))
 	}
-	return resultResources, errs
+	return result, errs
 }
 
 func getCurrNames(sol solution_context.SolutionContext, resourceToSet *construct.ResourceId) (set.Set[string], error) {
@@ -265,14 +294,14 @@ func expandPath(
 		candidates[i][node] = 0
 		resource, err := input.TempGraph.Vertex(node)
 		if err != nil {
-			errs = errors.Join(errs, err)
+			errs = errors.Join(errs, fmt.Errorf("error getting vertex for path[%d]: %w", i, err))
 			continue
 		}
 		// we know phantoms are always able to be valid, so we want to ensure we make them valid based on src and target validity checks
 		// right now we dont want validity checks to be blocking, just preference so we use them to modify the weight
 		valid, err := checkCandidatesValidity(ctx, resource, path, input.Classification)
 		if err != nil {
-			errs = errors.Join(errs, err)
+			errs = errors.Join(errs, fmt.Errorf("error checking validity of path[%d]: %w", i, err))
 			continue
 		}
 		if !valid {
@@ -291,7 +320,7 @@ func expandPath(
 		}
 		valid, err := checkNamespaceValidity(ctx, resource, input.Dep.Target.ID)
 		if err != nil {
-			return errors.Join(nerr, err)
+			return errors.Join(nerr, fmt.Errorf("error checking namespace validity of %s: %w", resource, err))
 		}
 		if !valid {
 			return nerr
@@ -379,13 +408,20 @@ func expandPath(
 				return
 			}
 		} else if err != nil {
-			errs = errors.Join(errs, err)
+			errs = errors.Join(errs, fmt.Errorf("unexpected error from raw edge: %v", err))
 			return
 		}
 
 		err = input.TempGraph.AddEdge(source.id, target.id, graph.EdgeWeight(weight))
-		if err != nil && !errors.Is(err, graph.ErrEdgeAlreadyExists) && !errors.Is(err, graph.ErrEdgeCreatesCycle) {
-			errs = errors.Join(errs, err)
+		switch {
+		case errors.Is(err, graph.ErrEdgeAlreadyExists):
+			errs = errors.Join(errs, input.TempGraph.UpdateEdge(source.id, target.id, graph.EdgeWeight(weight)))
+
+		case errors.Is(err, graph.ErrEdgeCreatesCycle):
+			// ignore cycles
+
+		case err != nil:
+			errs = errors.Join(errs, fmt.Errorf("unexpected error adding edge to temp graph: %v", err))
 		}
 	}
 
@@ -470,49 +506,6 @@ func connectThroughNamespace(src, target *construct.Resource, ctx solution_conte
 	return
 }
 
-func addPathToGraph(ctx solution_context.SolutionContext, g construct.Graph, path construct.Path) (
-	[]*construct.Resource,
-	error,
-) {
-	var errs error
-	result := make([]*construct.Resource, len(path))
-	for i, resource := range path {
-		r, err := ctx.RawView().Vertex(resource)
-		if err != nil && !errors.Is(err, graph.ErrVertexNotFound) {
-			errs = errors.Join(errs, err)
-			continue
-		}
-		if r == nil {
-			r, err = knowledgebase.CreateResource(ctx.KnowledgeBase(), resource)
-			if err != nil {
-				errs = errors.Join(errs, err)
-				continue
-			}
-		}
-		result[i] = r
-	}
-	if errs != nil {
-		return nil, errs
-	}
-	var prevRes construct.ResourceId
-	for i, r := range result {
-		err := g.AddVertex(r)
-		if err != nil && !errors.Is(err, graph.ErrVertexAlreadyExists) {
-			errs = errors.Join(errs, err)
-		}
-		if i == 0 {
-			prevRes = r.ID
-			continue
-		}
-		err = g.AddEdge(prevRes, r.ID)
-		if err != nil && !errors.Is(err, graph.ErrEdgeAlreadyExists) {
-			errs = errors.Join(errs, err)
-		}
-		prevRes = r.ID
-	}
-	return result, errs
-}
-
 // NOTE(gg): if for some reason the path could contain a duplicated selector
 // this would just add the resource to the first match. I don't
 // think this should happen for a call into [ExpandEdge], but noting it just in case.
diff --git a/pkg/knowledge_base2/reader/properties.go b/pkg/knowledge_base2/reader/properties.go
index 386d946e..f8e27c06 100644
--- a/pkg/knowledge_base2/reader/properties.go
+++ b/pkg/knowledge_base2/reader/properties.go
@@ -23,6 +23,8 @@ type (
 		// Type defines the type of the property
 		Type string `json:"type" yaml:"type"`
 
+		Description string `json:"description" yaml:"description"`
+
 		Namespace bool `json:"namespace" yaml:"namespace"`
 
 		DefaultValue any `json:"default_value" yaml:"default_value"`
diff --git a/pkg/knowledge_base2/resource_template.go b/pkg/knowledge_base2/resource_template.go
index 75948939..6670199f 100644
--- a/pkg/knowledge_base2/resource_template.go
+++ b/pkg/knowledge_base2/resource_template.go
@@ -59,6 +59,8 @@ type (
 		DeployTime bool `json:"deploy_time" yaml:"deploy_time"`
 		// OperationalRule defines a rule that is executed at runtime to determine the value of the property
 		OperationalRule *PropertyRule `json:"operational_rule" yaml:"operational_rule"`
+		// Description is a description of the property. This is not used in the engine solving, but is metadata returned by the `ListResourceTypes` CLI command.
+		Description string `json:"description" yaml:"description"`
 		// Path is the path to the property in the resource
 		Path string `json:"-" yaml:"-"`
 	}


Test patch:
diff --git a/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml b/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml
new file mode 100755
index 00000000..a75e510a
--- /dev/null
+++ b/pkg/engine2/testdata/lambda_efs.dataflow-viz.yaml
@@ -0,0 +1,14 @@
+provider: aws
+resources:
+  efs_file_system/test-efs-fs:
+
+  vpc/vpc-0:
+
+  lambda_function/lambda_test_app:
+    children: aws:ecr_image:lambda_test_app-image,aws:iam_role:lambda_test_app-ExecutionRole,aws:log_group:lambda_test_app-log-group
+    parent: vpc/vpc-0
+
+  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
+
+
diff --git a/pkg/engine2/testdata/lambda_efs.expect.yaml b/pkg/engine2/testdata/lambda_efs.expect.yaml
new file mode 100755
index 00000000..da403800
--- /dev/null
+++ b/pkg/engine2/testdata/lambda_efs.expect.yaml
@@ -0,0 +1,279 @@
+resources:
+    aws:security_group:vpc-0:lambda_test_app-security_group:
+        EgressRules:
+            - CidrBlocks:
+                - 0.0.0.0/0
+              Description: Allows all outbound IPv4 traffic
+              FromPort: 0
+              Protocol: "-1"
+              ToPort: 0
+        IngressRules:
+            - Description: Allow ingress traffic from within the same security group
+              FromPort: 0
+              Protocol: "-1"
+              Self: true
+              ToPort: 0
+        Vpc: aws:vpc:vpc-0
+    aws:lambda_function:lambda_test_app:
+        EfsAccessPoint: aws:efs_access_point:test-efs-fs:lambda_test_app-test-efs-fs
+        ExecutionRole: aws:iam_role:lambda_test_app-ExecutionRole
+        Image: aws:ecr_image:lambda_test_app-image
+        LogGroup: aws:log_group:lambda_test_app-log-group
+        MemorySize: 512
+        SecurityGroups:
+            - aws:security_group:vpc-0:lambda_test_app-security_group
+        Subnets:
+            - aws:subnet:vpc-0:lambda_test_app-test-efs-fs
+            - aws:subnet:vpc-0:subnet-1
+        Timeout: 180
+    aws:ecr_image:lambda_test_app-image:
+        Context: .
+        Dockerfile: lambda_test_app-image.Dockerfile
+        Repo: aws:ecr_repo:ecr_repo-0
+    aws:efs_access_point:test-efs-fs:lambda_test_app-test-efs-fs:
+        FileSystem: aws:efs_file_system:test-efs-fs
+        PosixUser:
+            Gid: 1000
+            Uid: 1000
+        RootDirectory:
+            CreationInfo:
+                OwnerGid: 1000
+                OwnerUid: 1000
+                Permissions: "777"
+            Path: /mnt/efs
+    aws:iam_role:lambda_test_app-ExecutionRole:
+        AssumeRolePolicyDoc:
+            Statement:
+                - Action:
+                    - sts:AssumeRole
+                  Effect: Allow
+                  Principal:
+                    Service:
+                        - lambda.amazonaws.com
+            Version: "2012-10-17"
+        InlinePolicies:
+            - Name: test-efs-fs-policy
+              Policy:
+                Statement:
+                    - Action:
+                        - efs:Client*
+                      Effect: Allow
+                      Resource:
+                        - 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:
+        ForceDelete: true
+    aws:efs_mount_target:test-efs-fs:lambda_test_app-test-efs-fs:
+        FileSystem: aws:efs_file_system:test-efs-fs
+        SecurityGroups:
+            - aws:security_group:vpc-0:lambda_test_app-test-efs-fs
+        Subnet: aws:subnet:vpc-0:lambda_test_app-test-efs-fs
+    aws:subnet:vpc-0:lambda_test_app-test-efs-fs:
+        AvailabilityZone: aws:availability_zone:region-0:availability_zone-1
+        CidrBlock: 10.0.192.0/18
+        MapPublicIpOnLaunch: false
+        RouteTable: aws:route_table:vpc-0:lambda_test_app-test-efs-fs-route_table
+        Type: private
+        Vpc: aws:vpc:vpc-0
+    aws:route_table_association:lambda_test_app-test-efs-fs-lambda_test_app-test-efs-fs-route_table:
+        RouteTable: aws:route_table:vpc-0:lambda_test_app-test-efs-fs-route_table
+        Subnet: aws:subnet:vpc-0:lambda_test_app-test-efs-fs
+    aws:route_table:vpc-0:lambda_test_app-test-efs-fs-route_table:
+        Routes:
+            - CidrBlock: 0.0.0.0/0
+              NatGateway: aws:nat_gateway:subnet-2:lambda_test_app-test-efs-fs-route_table-nat_gateway
+        Vpc: aws:vpc:vpc-0
+    aws:nat_gateway:subnet-2:lambda_test_app-test-efs-fs-route_table-nat_gateway:
+        ElasticIp: aws:elastic_ip:lambda_test_app-test-efs-fs-route_table-nat_gateway-elastic_ip
+        Subnet: aws:subnet:vpc-0:subnet-2
+    aws:elastic_ip:lambda_test_app-test-efs-fs-route_table-nat_gateway-elastic_ip:
+    aws:subnet:vpc-0:subnet-2:
+        AvailabilityZone: aws:availability_zone:region-0:availability_zone-0
+        CidrBlock: 10.0.0.0/18
+        MapPublicIpOnLaunch: false
+        RouteTable: aws:route_table:vpc-0:subnet-2-route_table
+        Type: public
+        Vpc: aws:vpc:vpc-0
+    aws:route_table_association:subnet-2-subnet-2-route_table:
+        RouteTable: aws:route_table:vpc-0:subnet-2-route_table
+        Subnet: aws:subnet:vpc-0:subnet-2
+    aws:route_table:vpc-0:subnet-2-route_table:
+        Routes:
+            - CidrBlock: 0.0.0.0/0
+              Gateway: aws:internet_gateway:vpc-0:internet_gateway-0
+        Vpc: aws:vpc:vpc-0
+    aws:SERVICE_API:lambda_test_app-lambda_test_app-log-group:
+    aws:log_group:lambda_test_app-log-group:
+        LogGroupName: /aws/lambda/lambda_test_app
+        RetentionInDays: 5
+    aws:availability_zone:region-0:availability_zone-1:
+        Index: 1
+        Region: aws:region:region-0
+    aws:efs_file_system:test-efs-fs:
+        AvailabilityZone: aws:availability_zone:region-0:availability_zone-0
+        Encrypted: true
+        PerformanceMode: generalPurpose
+        ThroughputMode: bursting
+    aws:availability_zone:region-0:availability_zone-0:
+        Index: 0
+        Region: aws:region:region-0
+    aws:region:region-0:
+    aws:efs_mount_target:test-efs-fs:subnet-1-test-efs-fs:
+        FileSystem: aws:efs_file_system:test-efs-fs
+        SecurityGroups:
+            - aws:security_group:vpc-0:subnet-1-test-efs-fs
+        Subnet: aws:subnet:vpc-0:subnet-1
+    aws:subnet:vpc-0:subnet-1:
+        AvailabilityZone: aws:availability_zone:region-0:availability_zone-0
+        CidrBlock: 10.0.128.0/18
+        MapPublicIpOnLaunch: false
+        RouteTable: aws:route_table:vpc-0:subnet-1-route_table
+        Type: private
+        Vpc: aws:vpc:vpc-0
+    aws:route_table_association:subnet-1-subnet-1-route_table:
+        RouteTable: aws:route_table:vpc-0:subnet-1-route_table
+        Subnet: aws:subnet:vpc-0:subnet-1
+    aws:security_group:vpc-0:lambda_test_app-test-efs-fs:
+        EgressRules:
+            - CidrBlocks:
+                - 0.0.0.0/0
+              Description: Allows all outbound IPv4 traffic
+              FromPort: 0
+              Protocol: "-1"
+              ToPort: 0
+        IngressRules:
+            - CidrBlocks:
+                - 10.0.192.0/18
+              Description: Allow ingress traffic from ip addresses within the subnet lambda_test_app-test-efs-fs
+              FromPort: 0
+              Protocol: "-1"
+              ToPort: 0
+            - 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-1
+              FromPort: 0
+              Protocol: "-1"
+              ToPort: 0
+        Vpc: aws:vpc:vpc-0
+    aws:security_group:vpc-0:subnet-1-test-efs-fs:
+        EgressRules:
+            - CidrBlocks:
+                - 0.0.0.0/0
+              Description: Allows all outbound IPv4 traffic
+              FromPort: 0
+              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
+            - 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-1-route_table:
+        Routes:
+            - CidrBlock: 0.0.0.0/0
+              NatGateway: aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway
+        Vpc: aws:vpc:vpc-0
+    aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway:
+        ElasticIp: aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip
+        Subnet: aws:subnet:vpc-0:subnet-3
+    aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip:
+    aws:subnet:vpc-0:subnet-3:
+        AvailabilityZone: aws:availability_zone:region-0:availability_zone-1
+        CidrBlock: 10.0.64.0/18
+        MapPublicIpOnLaunch: false
+        RouteTable: aws:route_table:vpc-0:subnet-3-route_table
+        Type: public
+        Vpc: aws:vpc:vpc-0
+    aws:route_table_association:subnet-3-subnet-3-route_table:
+        RouteTable: aws:route_table:vpc-0:subnet-3-route_table
+        Subnet: aws:subnet:vpc-0:subnet-3
+    aws:route_table:vpc-0:subnet-3-route_table:
+        Routes:
+            - CidrBlock: 0.0.0.0/0
+              Gateway: aws:internet_gateway:vpc-0:internet_gateway-0
+        Vpc: aws:vpc:vpc-0
+    aws:internet_gateway:vpc-0:internet_gateway-0:
+        Vpc: aws:vpc:vpc-0
+    aws:vpc:vpc-0:
+        CidrBlock: 10.0.0.0/16
+        EnableDnsHostnames: true
+        EnableDnsSupport: true
+edges:
+    aws:security_group:vpc-0:lambda_test_app-security_group -> aws:lambda_function:lambda_test_app:
+    aws:security_group:vpc-0:lambda_test_app-security_group -> aws:vpc:vpc-0:
+    aws:lambda_function:lambda_test_app -> aws:SERVICE_API:lambda_test_app-lambda_test_app-log-group:
+    aws:lambda_function:lambda_test_app -> aws:ecr_image:lambda_test_app-image:
+    aws:lambda_function:lambda_test_app -> aws:efs_access_point:test-efs-fs:lambda_test_app-test-efs-fs:
+    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: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:
+    aws:efs_mount_target:test-efs-fs:lambda_test_app-test-efs-fs -> aws:efs_file_system:test-efs-fs:
+    aws:efs_mount_target:test-efs-fs:lambda_test_app-test-efs-fs -> aws:subnet:vpc-0:lambda_test_app-test-efs-fs:
+    aws:subnet:vpc-0:lambda_test_app-test-efs-fs -> aws:SERVICE_API:lambda_test_app-lambda_test_app-log-group:
+    aws:subnet:vpc-0:lambda_test_app-test-efs-fs -> aws:availability_zone:region-0:availability_zone-1:
+    ? aws:subnet:vpc-0:lambda_test_app-test-efs-fs -> aws:route_table_association:lambda_test_app-test-efs-fs-lambda_test_app-test-efs-fs-route_table
+    :
+    aws:subnet:vpc-0:lambda_test_app-test-efs-fs -> aws:security_group:vpc-0:lambda_test_app-test-efs-fs:
+    aws:subnet:vpc-0:lambda_test_app-test-efs-fs -> aws:vpc:vpc-0:
+    ? aws:route_table_association:lambda_test_app-test-efs-fs-lambda_test_app-test-efs-fs-route_table -> aws:route_table:vpc-0:lambda_test_app-test-efs-fs-route_table
+    :
+    ? aws:route_table:vpc-0:lambda_test_app-test-efs-fs-route_table -> aws:nat_gateway:subnet-2:lambda_test_app-test-efs-fs-route_table-nat_gateway
+    :
+    aws:route_table:vpc-0:lambda_test_app-test-efs-fs-route_table -> aws:vpc:vpc-0:
+    ? aws:nat_gateway:subnet-2:lambda_test_app-test-efs-fs-route_table-nat_gateway -> aws: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:
+    aws:subnet:vpc-0:subnet-2 -> aws:availability_zone:region-0:availability_zone-0:
+    aws:subnet:vpc-0:subnet-2 -> aws:route_table_association:subnet-2-subnet-2-route_table:
+    aws:subnet:vpc-0:subnet-2 -> aws:vpc:vpc-0:
+    aws:route_table_association:subnet-2-subnet-2-route_table -> aws:route_table:vpc-0:subnet-2-route_table:
+    aws:route_table:vpc-0:subnet-2-route_table -> aws:internet_gateway:vpc-0:internet_gateway-0:
+    aws:route_table:vpc-0:subnet-2-route_table -> aws:vpc:vpc-0:
+    aws:SERVICE_API:lambda_test_app-lambda_test_app-log-group -> aws:log_group:lambda_test_app-log-group:
+    aws:availability_zone:region-0:availability_zone-1 -> aws:region:region-0:
+    aws:efs_file_system:test-efs-fs -> aws:availability_zone:region-0:availability_zone-0:
+    aws:availability_zone:region-0:availability_zone-0 -> aws:region:region-0:
+    aws:efs_mount_target:test-efs-fs:subnet-1-test-efs-fs -> aws:efs_file_system:test-efs-fs:
+    aws:efs_mount_target:test-efs-fs:subnet-1-test-efs-fs -> aws:subnet:vpc-0:subnet-1:
+    aws:subnet:vpc-0:subnet-1 -> aws:SERVICE_API:lambda_test_app-lambda_test_app-log-group:
+    aws:subnet:vpc-0:subnet-1 -> aws:availability_zone:region-0:availability_zone-0:
+    aws:subnet:vpc-0:subnet-1 -> aws:route_table_association:subnet-1-subnet-1-route_table:
+    aws:subnet:vpc-0:subnet-1 -> aws:security_group:vpc-0:lambda_test_app-test-efs-fs:
+    aws:subnet:vpc-0:subnet-1 -> aws:security_group:vpc-0:subnet-1-test-efs-fs:
+    aws:subnet:vpc-0:subnet-1 -> aws:vpc:vpc-0:
+    aws:route_table_association:subnet-1-subnet-1-route_table -> aws:route_table:vpc-0:subnet-1-route_table:
+    aws:security_group:vpc-0:lambda_test_app-test-efs-fs -> aws:efs_mount_target:test-efs-fs:lambda_test_app-test-efs-fs:
+    aws:security_group:vpc-0:lambda_test_app-test-efs-fs -> aws:vpc:vpc-0:
+    aws:security_group:vpc-0:subnet-1-test-efs-fs -> aws:efs_mount_target:test-efs-fs:subnet-1-test-efs-fs:
+    aws:security_group:vpc-0:subnet-1-test-efs-fs -> aws:vpc:vpc-0:
+    aws:route_table:vpc-0:subnet-1-route_table -> aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway:
+    aws:route_table:vpc-0:subnet-1-route_table -> aws:vpc:vpc-0:
+    aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway -> aws:elastic_ip:subnet-1-route_table-nat_gateway-elastic_ip:
+    aws:nat_gateway:subnet-3:subnet-1-route_table-nat_gateway -> aws:subnet:vpc-0:subnet-3:
+    aws:subnet:vpc-0:subnet-3 -> aws:availability_zone:region-0:availability_zone-1:
+    aws:subnet:vpc-0:subnet-3 -> aws:route_table_association:subnet-3-subnet-3-route_table:
+    aws:subnet:vpc-0:subnet-3 -> aws:vpc:vpc-0:
+    aws:route_table_association:subnet-3-subnet-3-route_table -> aws:route_table:vpc-0:subnet-3-route_table:
+    aws:route_table:vpc-0:subnet-3-route_table -> aws:internet_gateway:vpc-0:internet_gateway-0:
+    aws:route_table:vpc-0:subnet-3-route_table -> aws:vpc:vpc-0:
+    aws:internet_gateway:vpc-0:internet_gateway-0 -> aws:vpc:vpc-0:
diff --git a/pkg/engine2/testdata/lambda_efs.iac-viz.yaml b/pkg/engine2/testdata/lambda_efs.iac-viz.yaml
new file mode 100755
index 00000000..969dafc2
--- /dev/null
+++ b/pkg/engine2/testdata/lambda_efs.iac-viz.yaml
@@ -0,0 +1,122 @@
+provider: aws
+resources:
+  region/region-0:
+
+  aws:availability_zone:region-0/availability_zone-1:
+  aws:availability_zone:region-0/availability_zone-1 -> region/region-0:
+
+  vpc/vpc-0:
+
+  aws:availability_zone:region-0/availability_zone-0:
+  aws:availability_zone:region-0/availability_zone-0 -> region/region-0:
+
+  elastic_ip/subnet-1-route_table-nat_gateway-elastic_ip:
+
+  aws:subnet:vpc-0/subnet-3:
+  aws:subnet:vpc-0/subnet-3 -> aws:availability_zone:region-0/availability_zone-1:
+  aws:subnet:vpc-0/subnet-3 -> vpc/vpc-0:
+
+  elastic_ip/lambda_test_app-test-efs-fs-route_table-nat_gateway-elastic_ip:
+
+  aws:subnet:vpc-0/subnet-2:
+  aws:subnet:vpc-0/subnet-2 -> aws:availability_zone:region-0/availability_zone-0:
+  aws:subnet:vpc-0/subnet-2 -> vpc/vpc-0:
+
+  aws:internet_gateway:vpc-0/internet_gateway-0:
+  aws:internet_gateway:vpc-0/internet_gateway-0 -> vpc/vpc-0:
+
+  aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway:
+  aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway -> elastic_ip/subnet-1-route_table-nat_gateway-elastic_ip:
+  aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway -> aws:subnet:vpc-0/subnet-3:
+
+  aws:security_group:vpc-0/lambda_test_app-test-efs-fs:
+  aws:security_group:vpc-0/lambda_test_app-test-efs-fs -> vpc/vpc-0:
+
+  aws:security_group:vpc-0/subnet-1-test-efs-fs:
+  aws:security_group:vpc-0/subnet-1-test-efs-fs -> vpc/vpc-0:
+
+  aws:nat_gateway:subnet-2/lambda_test_app-test-efs-fs-route_table-nat_gateway:
+  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:
+
+  efs_file_system/test-efs-fs:
+  efs_file_system/test-efs-fs -> aws:availability_zone:region-0/availability_zone-0:
+
+  log_group/lambda_test_app-log-group:
+
+  aws:route_table:vpc-0/subnet-3-route_table:
+  aws:route_table:vpc-0/subnet-3-route_table -> aws:internet_gateway:vpc-0/internet_gateway-0:
+  aws:route_table:vpc-0/subnet-3-route_table -> vpc/vpc-0:
+
+  aws:route_table:vpc-0/subnet-2-route_table:
+  aws:route_table:vpc-0/subnet-2-route_table -> aws:internet_gateway:vpc-0/internet_gateway-0:
+  aws:route_table:vpc-0/subnet-2-route_table -> vpc/vpc-0:
+
+  aws:route_table:vpc-0/subnet-1-route_table:
+  aws:route_table:vpc-0/subnet-1-route_table -> aws:nat_gateway:subnet-3/subnet-1-route_table-nat_gateway:
+  aws:route_table:vpc-0/subnet-1-route_table -> vpc/vpc-0:
+
+  aws:subnet:vpc-0/subnet-1:
+  aws:subnet:vpc-0/subnet-1 -> aws:availability_zone:region-0/availability_zone-0:
+  aws:subnet:vpc-0/subnet-1 -> aws:security_group:vpc-0/lambda_test_app-test-efs-fs:
+  aws:subnet:vpc-0/subnet-1 -> aws:security_group:vpc-0/subnet-1-test-efs-fs:
+  aws:subnet:vpc-0/subnet-1 -> vpc/vpc-0:
+
+  aws:route_table:vpc-0/lambda_test_app-test-efs-fs-route_table:
+  aws:route_table:vpc-0/lambda_test_app-test-efs-fs-route_table -> aws:nat_gateway:subnet-2/lambda_test_app-test-efs-fs-route_table-nat_gateway:
+  aws:route_table:vpc-0/lambda_test_app-test-efs-fs-route_table -> vpc/vpc-0:
+
+  aws:subnet:vpc-0/lambda_test_app-test-efs-fs:
+  aws:subnet:vpc-0/lambda_test_app-test-efs-fs -> aws:availability_zone:region-0/availability_zone-1:
+  aws:subnet:vpc-0/lambda_test_app-test-efs-fs -> aws:security_group:vpc-0/lambda_test_app-test-efs-fs:
+  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:
+
+  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:
+
+  iam_role/lambda_test_app-executionrole:
+  iam_role/lambda_test_app-executionrole -> efs_file_system/test-efs-fs:
+  iam_role/lambda_test_app-executionrole -> log_group/lambda_test_app-log-group:
+
+  aws:security_group:vpc-0/lambda_test_app-security_group:
+  aws:security_group:vpc-0/lambda_test_app-security_group -> vpc/vpc-0:
+
+  route_table_association/subnet-3-subnet-3-route_table:
+  route_table_association/subnet-3-subnet-3-route_table -> aws:route_table:vpc-0/subnet-3-route_table:
+  route_table_association/subnet-3-subnet-3-route_table -> aws:subnet:vpc-0/subnet-3:
+
+  route_table_association/subnet-2-subnet-2-route_table:
+  route_table_association/subnet-2-subnet-2-route_table -> aws:route_table:vpc-0/subnet-2-route_table:
+  route_table_association/subnet-2-subnet-2-route_table -> aws:subnet:vpc-0/subnet-2:
+
+  route_table_association/subnet-1-subnet-1-route_table:
+  route_table_association/subnet-1-subnet-1-route_table -> aws:route_table:vpc-0/subnet-1-route_table:
+  route_table_association/subnet-1-subnet-1-route_table -> aws:subnet:vpc-0/subnet-1:
+
+  route_table_association/lambda_test_app-test-efs-fs-lambda_test_app-test-efs-fs-route_table:
+  route_table_association/lambda_test_app-test-efs-fs-lambda_test_app-test-efs-fs-route_table -> aws:route_table:vpc-0/lambda_test_app-test-efs-fs-route_table:
+  route_table_association/lambda_test_app-test-efs-fs-lambda_test_app-test-efs-fs-route_table -> aws:subnet:vpc-0/lambda_test_app-test-efs-fs:
+
+  lambda_function/lambda_test_app:
+  lambda_function/lambda_test_app -> ecr_image/lambda_test_app-image:
+  lambda_function/lambda_test_app -> aws:efs_access_point:test-efs-fs/lambda_test_app-test-efs-fs:
+  lambda_function/lambda_test_app -> iam_role/lambda_test_app-executionrole:
+  lambda_function/lambda_test_app -> aws:security_group:vpc-0/lambda_test_app-security_group:
+  lambda_function/lambda_test_app -> aws:subnet:vpc-0/lambda_test_app-test-efs-fs:
+  lambda_function/lambda_test_app -> aws:subnet:vpc-0/subnet-1:
+
+  aws:efs_mount_target:test-efs-fs/subnet-1-test-efs-fs:
+  aws:efs_mount_target:test-efs-fs/subnet-1-test-efs-fs -> efs_file_system/test-efs-fs:
+  aws:efs_mount_target:test-efs-fs/subnet-1-test-efs-fs -> aws:security_group:vpc-0/subnet-1-test-efs-fs:
+  aws:efs_mount_target:test-efs-fs/subnet-1-test-efs-fs -> aws:subnet:vpc-0/subnet-1:
+
+  aws:efs_mount_target:test-efs-fs/lambda_test_app-test-efs-fs:
+  aws:efs_mount_target:test-efs-fs/lambda_test_app-test-efs-fs -> efs_file_system/test-efs-fs:
+  aws:efs_mount_target:test-efs-fs/lambda_test_app-test-efs-fs -> aws:security_group:vpc-0/lambda_test_app-test-efs-fs:
+  aws:efs_mount_target:test-efs-fs/lambda_test_app-test-efs-fs -> aws:subnet:vpc-0/lambda_test_app-test-efs-fs:
+
diff --git a/pkg/engine2/testdata/lambda_efs.input.yaml b/pkg/engine2/testdata/lambda_efs.input.yaml
new file mode 100644
index 00000000..68fe5b67
--- /dev/null
+++ b/pkg/engine2/testdata/lambda_efs.input.yaml
@@ -0,0 +1,12 @@
+constraints:
+- node: aws:lambda_function:lambda_test_app
+  operator: add
+  scope: application
+- node: aws:efs_file_system:test-efs-fs
+  operator: add
+  scope: application
+- operator: must_exist
+  scope: edge
+  target:
+    source: aws:lambda_function:lambda_test_app
+    target: aws:efs_file_system:test-efs-fs
diff --git a/pkg/engine2/testdata/namespace_pathselect.expect.yaml b/pkg/engine2/testdata/namespace_pathselect.expect.yaml
index d86c6736..a7167502 100755
--- a/pkg/engine2/testdata/namespace_pathselect.expect.yaml
+++ b/pkg/engine2/testdata/namespace_pathselect.expect.yaml
@@ -62,8 +62,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_1:lambda_function_2-vpc_1:
         AvailabilityZone: aws:availability_zone:region-0:availability_zone-0
         CidrBlock: 10.0.128.0/18