Repository: klothoplatform/klotho
Pull request #808 (2023-12-07 16:22:39)

Adds GetValidEdgeTargets command to the engine


Patch:
diff --git a/cmd/engine/main.go b/cmd/engine/main.go
index d298f63e..7c12ced3 100644
--- a/cmd/engine/main.go
+++ b/cmd/engine/main.go
@@ -9,12 +9,23 @@ import (
 )
 
 func main() {
-	em := &engine.EngineMain{}
-	var root = &cobra.Command{}
-	em.AddEngineCli(root)
+	root := newRootCmd()
 	err := root.Execute()
 	if err != nil {
-		fmt.Printf("Error: %v\n", err)
-		os.Exit(1)
+		switch err.(type) {
+		case engine.ConfigValidationError:
+			fmt.Printf("Error: %v\n", err)
+			os.Exit(2)
+		default:
+			fmt.Printf("Error: %v\n", err)
+			os.Exit(1)
+		}
 	}
 }
+
+func newRootCmd() *cobra.Command {
+	em := &engine.EngineMain{}
+	var root = &cobra.Command{}
+	em.AddEngineCli(root)
+	return root
+}
diff --git a/go.mod b/go.mod
index c5e2ab14..b1dd49db 100644
--- a/go.mod
+++ b/go.mod
@@ -41,6 +41,7 @@ require (
 )
 
 require (
+	github.com/alitto/pond v1.8.3 // indirect
 	github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 // indirect
 	github.com/gomodule/redigo v2.0.0+incompatible // indirect
 	github.com/kr/pretty v0.3.0 // indirect
diff --git a/go.sum b/go.sum
index 07a21dcb..bbe3ac97 100644
--- a/go.sum
+++ b/go.sum
@@ -69,6 +69,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:H
 github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alitto/pond v1.8.3 h1:ydIqygCLVPqIX/USe5EaV/aSRXTRXDEI9JwuDdu+/xs=
+github.com/alitto/pond v1.8.3/go.mod h1:CmvIIGd5jKLasGI3D87qDkQxjzChdKMmnXMg3fG6M6Q=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
diff --git a/pkg/engine2/cli.go b/pkg/engine2/cli.go
index b53b9f8a..513fe51c 100644
--- a/pkg/engine2/cli.go
+++ b/pkg/engine2/cli.go
@@ -47,6 +47,14 @@ var architectureEngineCfg struct {
 	verbose     bool
 }
 
+var getValidEdgeTargetsCfg struct {
+	guardrails string
+	inputGraph string
+	configFile string
+	outputDir  string
+	verbose    bool
+}
+
 var hadWarnings = atomic.NewBool(false)
 var hadErrors = atomic.NewBool(false)
 
@@ -121,10 +129,27 @@ func (em *EngineMain) AddEngineCli(root *cobra.Command) {
 	flags.BoolVar(&engineCfg.jsonLog, "json-log", false, "Output logs in JSON format.")
 	flags.StringVar(&engineCfg.profileTo, "profiling", "", "Profile to file")
 
+	getPossibleEdgesCmd := &cobra.Command{
+		Use:     "GetValidEdgeTargets",
+		Short:   "Get the valid topological edge targets for the supplied configuration and input graph",
+		GroupID: engineGroup.ID,
+		RunE:    em.GetValidEdgeTargets,
+	}
+
+	flags = getPossibleEdgesCmd.Flags()
+	flags.StringVar(&getValidEdgeTargetsCfg.guardrails, "guardrails", "", "Guardrails file")
+	flags.StringVarP(&getValidEdgeTargetsCfg.inputGraph, "input-graph", "i", "", "Input graph file")
+	flags.StringVarP(&getValidEdgeTargetsCfg.configFile, "config", "c", "", "config file")
+	flags.StringVarP(&getValidEdgeTargetsCfg.outputDir, "output-dir", "o", "", "Output directory")
+	flags.BoolVarP(&getValidEdgeTargetsCfg.verbose, "verbose", "v", false, "Verbose flag")
+	flags.BoolVar(&engineCfg.jsonLog, "json-log", false, "Output logs in JSON format.")
+	flags.StringVar(&engineCfg.profileTo, "profiling", "", "Profile to file")
+
 	root.AddGroup(engineGroup)
 	root.AddCommand(listResourceTypesCmd)
 	root.AddCommand(listAttributesCmd)
 	root.AddCommand(runCmd)
+	root.AddCommand(getPossibleEdgesCmd)
 }
 
 func (em *EngineMain) AddEngine() error {
@@ -287,9 +312,98 @@ func (em *EngineMain) RunEngine(cmd *cobra.Command, args []string) error {
 	},
 	)
 
+	configErrors, configErr := em.Engine.getPropertyValidation(context.Solutions[0])
+	if len(configErrors) > 0 {
+		configErrorData, err := json.Marshal(configErrors)
+		if err != nil {
+			return errors.Errorf("failed to marshal config errors: %s", err.Error())
+		}
+		files = append(files, &io.RawFile{
+			FPath:   "config_errors.json",
+			Content: configErrorData,
+		})
+	}
+
 	err = io.OutputTo(files, architectureEngineCfg.outputDir)
 	if err != nil {
 		return errors.Errorf("failed to write output files: %s", err.Error())
 	}
+	if configErr != nil {
+		return ConfigValidationError{Err: configErr}
+	}
+	return nil
+}
+
+func (em *EngineMain) GetValidEdgeTargets(cmd *cobra.Command, args []string) error {
+	if engineCfg.profileTo != "" {
+		err := os.MkdirAll(filepath.Dir(engineCfg.profileTo), 0755)
+		if err != nil {
+			return fmt.Errorf("failed to create profile directory: %w", err)
+		}
+		profileF, err := os.OpenFile(engineCfg.profileTo, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
+		if err != nil {
+			return fmt.Errorf("failed to open profile file: %w", err)
+		}
+		defer func() {
+			pprof.StopCPUProfile()
+			profileF.Close()
+		}()
+		err = pprof.StartCPUProfile(profileF)
+		if err != nil {
+			return fmt.Errorf("failed to start profile: %w", err)
+		}
+	}
+
+	// Set up analytics, and hook them up to the logs
+	analyticsClient := analytics.NewClient()
+	analyticsClient.AppendProperties(map[string]any{})
+	z, err := setupLogger(analyticsClient)
+	if err != nil {
+		return err
+	}
+	defer closenicely.FuncOrDebug(z.Sync)
+	zap.ReplaceGlobals(z)
+
+	err = em.AddEngine()
+	if err != nil {
+		return err
+	}
+	zap.S().Info("loading config")
+
+	inputF, err := os.ReadFile(getValidEdgeTargetsCfg.inputGraph)
+	if err != nil {
+		return err
+	}
+
+	config, err := ReadGetValidEdgeTargetsConfig(getValidEdgeTargetsCfg.configFile)
+	if err != nil {
+		return errors.Errorf("failed to load constraints: %s", err.Error())
+	}
+	context := &GetPossibleEdgesContext{
+		InputGraph:                inputF,
+		GetValidEdgeTargetsConfig: config,
+	}
+
+	zap.S().Info("getting valid edge targets")
+	validTargets, err := em.Engine.GetValidEdgeTargets(context)
+	if err != nil {
+		return errors.Errorf("failed to run engine: %s", err.Error())
+	}
+
+	zap.S().Info("writing output files")
+	b, err := yaml.Marshal(validTargets)
+	if err != nil {
+		return errors.Errorf("failed to marshal possible edges: %s", err.Error())
+	}
+	var files []io.File
+	files = append(files, &io.RawFile{
+		FPath:   "valid_edge_targets.yaml",
+		Content: b,
+	})
+
+	err = io.OutputTo(files, getValidEdgeTargetsCfg.outputDir)
+	if err != nil {
+		return errors.Errorf("failed to write output files: %s", err.Error())
+	}
 	return nil
 }
diff --git a/pkg/engine2/edge_targets.go b/pkg/engine2/edge_targets.go
new file mode 100644
index 00000000..11f7e90a
--- /dev/null
+++ b/pkg/engine2/edge_targets.go
@@ -0,0 +1,297 @@
+package engine2
+
+import (
+	"github.com/alitto/pond"
+	"github.com/dominikbraun/graph"
+	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/path_selection"
+	"github.com/klothoplatform/klotho/pkg/set"
+	"go.uber.org/zap"
+	"gopkg.in/yaml.v3"
+	"os"
+	"slices"
+	"sync"
+	"time"
+)
+
+type (
+	GetValidEdgeTargetsConfig struct {
+		Resources struct {
+			Sources []construct.ResourceId
+			Targets []construct.ResourceId
+		}
+		ResourceTypes struct {
+			Sources []construct.ResourceId
+			Targets []construct.ResourceId
+		}
+		Tags []Tag
+	}
+	GetPossibleEdgesContext struct {
+		InputGraph []byte
+		GetValidEdgeTargetsConfig
+	}
+)
+
+// EdgeCanBeExpanded returns true if there is a set of kb paths between the source and target
+// that satisfies path satisfaction classifications for both the source and target.
+//
+// This is used to determine (on a best-effort basis) if an edge can be expanded
+// without fully solving the graph (which is expensive).
+func (e *Engine) EdgeCanBeExpanded(source construct.ResourceId, target construct.ResourceId) (bool, error) {
+
+	if source.Matches(target) {
+		return false, nil
+	}
+
+	edge := construct.SimpleEdge{
+		Source: source,
+		Target: target,
+	}
+
+	sourceTemplate, err := e.Kb.GetResourceTemplate(edge.Source)
+	if err != nil {
+		return false, err
+	}
+	targetTemplate, err := e.Kb.GetResourceTemplate(edge.Target)
+	if err != nil {
+		return false, err
+	}
+	classifications := make(set.Set[string])
+	sourceClassifications := sourceTemplate.PathSatisfaction.AsSource
+	if len(sourceClassifications) == 0 {
+		return false, nil
+	}
+	for _, satisfaction := range sourceClassifications {
+		if satisfaction.Classification == "" {
+			continue
+		}
+		classifications.Add(satisfaction.Classification)
+	}
+	targetClassifications := targetTemplate.PathSatisfaction.AsTarget
+	if len(targetClassifications) == 0 {
+		return false, nil
+	}
+	for _, satisfaction := range targetClassifications {
+		if satisfaction.Classification == "" {
+			continue
+		}
+		classifications.Add(satisfaction.Classification)
+	}
+
+	for classification := range classifications {
+		zap.S().Debugf("Checking for valid path between %s and %s for classification %s", source, target, classification)
+		pathSelectionGraph, err := path_selection.BuildPathSelectionGraph(edge, e.Kb, classification)
+		if err != nil {
+			return false, err
+		}
+		paths, err := graph.AllPathsBetween(pathSelectionGraph, edge.Source, edge.Target)
+		if len(paths) == 0 || err != nil {
+			return false, err
+		}
+
+		hasValidPath := false
+		for _, path := range paths {
+			if path_selection.PathSatisfiesClassification(e.Kb, path, classification) {
+				hasValidPath = true
+				break
+			}
+		}
+		if !hasValidPath {
+			return false, nil
+		}
+	}
+	return true, nil
+}
+
+func ReadGetValidEdgeTargetsConfig(path string) (GetValidEdgeTargetsConfig, error) {
+	var config GetValidEdgeTargetsConfig
+	yamlFile, err := os.ReadFile(path)
+	if err != nil {
+		return config, err
+	}
+	err = yaml.Unmarshal(yamlFile, &config)
+	if err != nil {
+		return config, err
+	}
+	return config, nil
+}
+
+/*
+GetValidEdgeTargets returns a map of valid edge targets for each source resource in the supplied graph.
+The returned map is keyed by the source resource's string representation.
+The value for each source resource is a list of valid target resources.
+
+Targets are considered valid if there is a set of kb paths between the source and target
+that satisfies both source and target path satisfaction classifications.
+
+A partial set of valid targets can be generated using the filter criteria in the context's config.
+*/
+func (e *Engine) GetValidEdgeTargets(context *GetPossibleEdgesContext) (map[string][]string, error) {
+
+	inputGraph, err := unmarshallInputGraph(context.InputGraph)
+	if err != nil {
+		return nil, err
+	}
+	solutionCtx := NewSolutionContext(e.Kb)
+	err = solutionCtx.LoadGraph(inputGraph)
+	if err != nil {
+		return nil, err
+	}
+	topologyGraph, err := e.GetViewsDag(DataflowView, solutionCtx)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var sources []construct.ResourceId
+	var targets []construct.ResourceId
+
+	qualifiedTypeMatcher := func(id construct.ResourceId) func(otherType construct.ResourceId) bool {
+		return func(otherType construct.ResourceId) bool {
+			return otherType.QualifiedTypeName() == id.QualifiedTypeName()
+		}
+	}
+
+	// filter resources based on the context
+	err = construct.WalkGraph(topologyGraph, func(id construct.ResourceId, resource *construct.Resource, nerr error) error {
+		tag := e.GetResourceVizTag(string(DataflowView), id)
+		if len(context.Tags) > 0 && !slices.Contains(context.Tags, tag) {
+			return nil
+		}
+		isSource := true
+		isTarget := true
+		if len(context.Resources.Sources) > 0 && !slices.Contains(context.Resources.Sources, id) {
+			isSource = false
+		}
+		if len(context.Resources.Targets) > 0 && !slices.Contains(context.Resources.Targets, id) {
+			isTarget = false
+		}
+
+		if len(context.ResourceTypes.Sources) > 0 && !slices.ContainsFunc(context.ResourceTypes.Sources, qualifiedTypeMatcher(id)) {
+			isSource = false
+		}
+
+		if len(context.ResourceTypes.Targets) > 0 && !slices.ContainsFunc(context.ResourceTypes.Targets, qualifiedTypeMatcher(id)) {
+			isTarget = false
+		}
+
+		if isSource {
+			sources = append(sources, id)
+		}
+		if isTarget {
+			targets = append(targets, id)
+		}
+
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	results := make(chan *edgeValidity)
+
+	//var detectionGroup sync.WaitGroup
+
+	checkerPool := pond.New(5, 1000, pond.Strategy(pond.Lazy()))
+	knownTargetValidity := make(map[string]map[string]bool)
+	rwLock := &sync.RWMutex{}
+
+	// get all valid-edge combinations for resource types in the supplied graph
+	for _, s := range sources {
+		for _, t := range targets {
+			source := s
+			target := t
+
+			if source.Matches(target) {
+				continue
+			}
+
+			if source.Namespace == target.Name || target.Namespace == source.Name {
+				continue
+			}
+
+			path, err := graph.ShortestPath(topologyGraph, source, target)
+			if len(path) > 0 && err == nil {
+				continue
+			}
+
+			checkerPool.Submit(func() {
+
+				// check if we already know the validity of this edge
+				sourceType := source.QualifiedTypeName()
+				targetType := target.QualifiedTypeName()
+
+				isValid := false
+				previouslyEvaluated := false
+
+				rwLock.RLock()
+				if _, ok := knownTargetValidity[sourceType]; ok {
+					if isValid, ok = knownTargetValidity[sourceType][targetType]; ok {
+						previouslyEvaluated = true
+					}
+				}
+				rwLock.RUnlock()
+
+				// only evaluate the edge if we haven't already done so for the same source and target types
+				if !previouslyEvaluated {
+					isValid, _ = e.EdgeCanBeExpanded(source, target)
+				} else {
+					zap.S().Debugf("Using cached result for %s -> %s: %t", source, target, isValid)
+				}
+				zap.S().Debugf("valid target: %s -> %s: %t", source, target, isValid)
+				results <- &edgeValidity{
+					Source:  source,
+					Target:  target,
+					IsValid: isValid,
+				}
+				if previouslyEvaluated {
+					return
+				}
+
+				// cache the result, so we don't have to recompute it for the same source and target types
+				// performance benefit is unclear given potential lock contention between goroutines
+				rwLock.Lock()
+				if _, ok := knownTargetValidity[sourceType]; !ok {
+					knownTargetValidity[sourceType] = make(map[string]bool)
+				}
+				knownTargetValidity[sourceType][targetType] = isValid
+				rwLock.Unlock()
+			})
+		}
+	}
+
+	output := make(map[string][]string, len(sources))
+	var processResultsGroup sync.WaitGroup
+	processResultsGroup.Add(1)
+	go func() {
+		defer processResultsGroup.Done()
+		for result := range results {
+			if result.IsValid {
+				if _, ok := output[result.Source.String()]; !ok {
+					output[result.Source.String()] = []string{}
+				}
+				output[result.Source.String()] = append(output[result.Source.String()], result.Target.String())
+			}
+		}
+	}()
+	checkerPool.StopAndWaitFor(60 * time.Second)
+	close(results)
+	processResultsGroup.Wait()
+
+	return output, nil
+}
+
+func unmarshallInputGraph(input []byte) (construct.Graph, error) {
+	var yamlGraph construct.YamlGraph
+	err := yaml.Unmarshal(input, &yamlGraph)
+	if err != nil {
+		return nil, err
+	}
+	return yamlGraph.Graph, nil
+}
+
+type edgeValidity struct {
+	Source  construct.ResourceId
+	Target  construct.ResourceId
+	IsValid bool
+}
diff --git a/pkg/engine2/engine.go b/pkg/engine2/engine.go
index b9435595..ec43483a 100644
--- a/pkg/engine2/engine.go
+++ b/pkg/engine2/engine.go
@@ -1,6 +1,8 @@
 package engine2
 
 import (
+	"errors"
+
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
 	"github.com/klothoplatform/klotho/pkg/engine2/constraints"
 	"github.com/klothoplatform/klotho/pkg/engine2/solution_context"
@@ -43,3 +45,24 @@ func (e *Engine) Run(context *EngineContext) error {
 	context.Solutions = append(context.Solutions, solutionCtx)
 	return err
 }
+
+func (e *Engine) getPropertyValidation(ctx solution_context.SolutionContext) ([]solution_context.PropertyValidationDecision, error) {
+	decisions := ctx.GetDecisions().GetRecords()
+	validationDecisions := make([]solution_context.PropertyValidationDecision, 0)
+	for _, decision := range decisions {
+		if validation, ok := decision.(solution_context.PropertyValidationDecision); ok {
+			if validation.Error != nil {
+				validationDecisions = append(validationDecisions, validation)
+			}
+		}
+	}
+	var errs error
+	for _, decision := range validationDecisions {
+		for _, c := range ctx.Constraints().Resources {
+			if c.Target == decision.Resource && c.Property == decision.Property.Details().Path {
+				errs = errors.Join(errs, decision.Error)
+			}
+		}
+	}
+	return validationDecisions, errs
+}
diff --git a/pkg/engine2/errors.go b/pkg/engine2/errors.go
new file mode 100644
index 00000000..0f1d3365
--- /dev/null
+++ b/pkg/engine2/errors.go
@@ -0,0 +1,11 @@
+package engine2
+
+type (
+	ConfigValidationError struct {
+		Err error
+	}
+)
+
+func (e ConfigValidationError) Error() string {
+	return e.Err.Error()
+}
diff --git a/pkg/engine2/operational_eval/vertex_path_expand.go b/pkg/engine2/operational_eval/vertex_path_expand.go
index b1591cb8..a88cb447 100644
--- a/pkg/engine2/operational_eval/vertex_path_expand.go
+++ b/pkg/engine2/operational_eval/vertex_path_expand.go
@@ -268,7 +268,7 @@ func (v *pathExpandVertex) addDepsFromProps(
 				continue
 			}
 			// if this dependency could pass validation for the resources property, consider it as a dependent vertex
-			if err := prop.Validate(resource, dep); err == nil {
+			if err := prop.Validate(resource, dep, solution_context.DynamicCtx(eval.Solution)); err == nil {
 				changes.addEdge(v.Key(), Key{Ref: ref})
 			}
 		}
diff --git a/pkg/engine2/operational_eval/vertex_property.go b/pkg/engine2/operational_eval/vertex_property.go
index aa98c2e3..cbba7c2d 100644
--- a/pkg/engine2/operational_eval/vertex_property.go
+++ b/pkg/engine2/operational_eval/vertex_property.go
@@ -137,6 +137,18 @@ func (v *propertyVertex) Evaluate(eval *Evaluator) error {
 		return eval.AddResources(res)
 	}
 
+	// Now that the vertex is evaluated, we will check it for validity and record our decision
+	val, err := res.GetProperty(v.Ref.Property)
+	if err != nil {
+		return fmt.Errorf("error while validating resource property: could not get property %s on resource %s: %w", v.Ref.Property, v.Ref.Resource, err)
+	}
+	err = v.Template.Validate(res, val, solution_context.DynamicCtx(eval.Solution))
+	eval.Solution.RecordDecision(solution_context.PropertyValidationDecision{
+		Resource: v.Ref.Resource,
+		Property: v.Template,
+		Value:    val,
+		Error:    err,
+	})
 	return nil
 }
 
diff --git a/pkg/engine2/path_selection/path_selection.go b/pkg/engine2/path_selection/path_selection.go
index 6b0a2f67..9bee6855 100644
--- a/pkg/engine2/path_selection/path_selection.go
+++ b/pkg/engine2/path_selection/path_selection.go
@@ -81,9 +81,9 @@ func BuildPathSelectionGraph(
 		return nil, fmt.Errorf("failed to add target vertex to path selection graph for %s: %w", dep, err)
 	}
 	for _, path := range paths {
-		resourcePath := []construct.ResourceId{}
-		for _, res := range path {
-			resourcePath = append(resourcePath, res.Id())
+		resourcePath := make([]construct.ResourceId, len(path))
+		for i, res := range path {
+			resourcePath[i] = res.Id()
 		}
 		if !PathSatisfiesClassification(kb, resourcePath, classification) {
 			continue
diff --git a/pkg/engine2/reconciler/reconciler.go b/pkg/engine2/reconciler/reconciler.go
index 8b38e252..ad406dd3 100644
--- a/pkg/engine2/reconciler/reconciler.go
+++ b/pkg/engine2/reconciler/reconciler.go
@@ -49,6 +49,14 @@ func RemoveResource(c solution_context.SolutionContext, resource construct.Resou
 		return err
 	}
 
+	namespacedResources, err := findAllResourcesInNamespace(c, resource)
+	if err != nil {
+		return err
+	}
+	for _, res := range namespacedResources.ToSlice() {
+		// Since we are explicitly deleting the namespace resource, we will explicitly delete the resources namespaced to it
+		errs = errors.Join(errs, RemoveResource(c, res, explicit))
+	}
 	// try to cleanup, if the resource is removable
 	for res := range upstreams.Union(downstreams) {
 		errs = errors.Join(errs, RemoveResource(c, res, false))
@@ -71,14 +79,8 @@ func canDeleteResource(
 	log := zap.S().With(zap.String("id", resource.String()))
 	deletionCriteria := template.DeleteContext
 
-	ignoreUpstream, err := ignoreCriteria(ctx, resource, upstreamNodes)
-	if err != nil {
-		return false, err
-	}
-	ignoreDownstream, err := ignoreCriteria(ctx, resource, downstreamNodes)
-	if err != nil {
-		return false, err
-	}
+	ignoreUpstream := ignoreCriteria(ctx, resource, upstreamNodes, knowledgebase.DirectionUpstream)
+	ignoreDownstream := ignoreCriteria(ctx, resource, downstreamNodes, knowledgebase.DirectionDownstream)
 
 	// Check to see if there are upstream nodes for the resource trying to be deleted
 	// If upstream nodes exist, attempt to delete the resources upstream of the resource before deciding that the deletion process cannot continue
@@ -161,28 +163,71 @@ func canDeleteResource(
 	return true, nil
 }
 
-// ignoreCriteria determines if we can delete a resource because the knowledge base in use by the engine, shows that the initial resource is dependent on the sub resource for deletion.
-// If the sub resource is deletion dependent on any of the dependent resources passed in then we will determine weather we can delete the dependent resource first.
-func ignoreCriteria(ctx solution_context.SolutionContext, resource construct.ResourceId, dependentResources set.Set[construct.ResourceId]) (bool, error) {
-	upstreams, downstreams, err := construct.Neighbors(ctx.DataflowGraph(), resource)
-	if err != nil {
-		return false, err
+// ignoreCriteria determines if we can delete a resource because the knowledge base in use by the engine,
+// shows that the initial resource is dependent on the sub resource for deletion.
+// If the sub resource is deletion dependent on any of the dependent resources passed in then we will determine weather
+// we can delete the dependent resource first.
+func ignoreCriteria(
+	ctx solution_context.SolutionContext,
+	resource construct.ResourceId,
+	nodes set.Set[construct.ResourceId],
+	direction knowledgebase.Direction,
+) bool {
+	if direction == knowledgebase.DirectionDownstream {
+		for down := range nodes {
+			t := ctx.KnowledgeBase().GetEdgeTemplate(resource, down)
+			if t == nil || !t.DeletionDependent {
+				return false
+			}
+		}
+	} else {
+		for up := range nodes {
+			t := ctx.KnowledgeBase().GetEdgeTemplate(up, resource)
+			if t == nil || !t.DeletionDependent {
+				return false
+			}
+		}
 	}
+	return true
+}
 
-	upstreams = upstreams.Intersection(dependentResources)
-	downstreams = downstreams.Intersection(dependentResources)
-
-	for up := range upstreams {
-		t := ctx.KnowledgeBase().GetEdgeTemplate(up, resource)
-		if t == nil || !t.DeletetionDependent {
-			return false, nil
+func findAllResourcesInNamespace(ctx solution_context.SolutionContext, namespace construct.ResourceId) (set.Set[construct.ResourceId], error) {
+	namespacedResources := make(set.Set[construct.ResourceId])
+	err := construct.WalkGraph(ctx.RawView(), func(id construct.ResourceId, resource *construct.Resource, nerr error) error {
+		if id.Namespace == "" || id.Namespace != namespace.Name {
+			return nerr
 		}
-	}
-	for down := range downstreams {
-		t := ctx.KnowledgeBase().GetEdgeTemplate(resource, down)
-		if t == nil || !t.DeletetionDependent {
-			return false, nil
+		rt, err := ctx.KnowledgeBase().GetResourceTemplate(id)
+		if err != nil {
+			return errors.Join(nerr, err)
 		}
+		if rt == nil {
+			return errors.Join(nerr, fmt.Errorf("unable to find resource template for %s", id))
+		}
+		err = rt.LoopProperties(resource, func(p knowledgebase.Property) error {
+			if !p.Details().Namespace {
+				return nil
+			}
+			propVal, err := resource.GetProperty(p.Details().Path)
+			if err != nil {
+				return err
+			}
+			switch val := propVal.(type) {
+			case construct.ResourceId:
+				if val.Matches(namespace) {
+					namespacedResources.Add(id)
+				}
+			case construct.PropertyRef:
+				if val.Resource.Matches(namespace) {
+					namespacedResources.Add(id)
+				}
+			}
+			return nil
+		})
+		return errors.Join(nerr, err)
+	})
+	if err != nil {
+		return nil, err
 	}
-	return true, nil
+	return namespacedResources, nil
 }
diff --git a/pkg/engine2/solution_context/decisions.go b/pkg/engine2/solution_context/decisions.go
index 1fa5c241..7e575819 100644
--- a/pkg/engine2/solution_context/decisions.go
+++ b/pkg/engine2/solution_context/decisions.go
@@ -1,6 +1,8 @@
 package solution_context
 
 import (
+	"encoding/json"
+
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
 	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 )
@@ -18,6 +20,7 @@ type (
 		// FindDecision(decision SolveDecision) []KV
 		// // FindContext returns the various decisions (the what) for a given context (the why)
 		// FindContext(key string, value any) []SolveDecision
+		GetRecords() []SolveDecision
 	}
 
 	SolveDecision interface {
@@ -50,14 +53,33 @@ type (
 		Value    any
 	}
 
-	ResourceConfigurationError struct {
+	PropertyValidationDecision struct {
 		Resource construct.ResourceId
 		Property knowledgebase.Property
+		Value    any
+		Error    error
 	}
 )
 
-func (d AddResourceDecision) internal()      {}
-func (d AddDependencyDecision) internal()    {}
-func (d RemoveResourceDecision) internal()   {}
-func (d RemoveDependencyDecision) internal() {}
-func (d SetPropertyDecision) internal()      {}
+func (d AddResourceDecision) internal()        {}
+func (d AddDependencyDecision) internal()      {}
+func (d RemoveResourceDecision) internal()     {}
+func (d RemoveDependencyDecision) internal()   {}
+func (d SetPropertyDecision) internal()        {}
+func (d PropertyValidationDecision) internal() {}
+
+func (d PropertyValidationDecision) MarshalJSON() ([]byte, error) {
+	if d.Value != nil {
+		return json.Marshal(map[string]any{
+			"resource": d.Resource,
+			"property": d.Property.Details().Path,
+			"value":    d.Value,
+			"error":    d.Error,
+		})
+	}
+	return json.Marshal(map[string]any{
+		"resource": d.Resource,
+		"property": d.Property.Details().Path,
+		"error":    d.Error,
+	})
+}
diff --git a/pkg/engine2/solution_context/interface.go b/pkg/engine2/solution_context/interface.go
index 1bb4ccb0..a76716fa 100644
--- a/pkg/engine2/solution_context/interface.go
+++ b/pkg/engine2/solution_context/interface.go
@@ -14,6 +14,7 @@ type (
 		KnowledgeBase() knowledgebase.TemplateKB
 		Constraints() *constraints.Constraints
 		RecordDecision(d SolveDecision)
+		GetDecisions() DecisionRecords
 
 		DataflowGraph() construct.Graph
 		DeploymentGraph() construct.Graph
diff --git a/pkg/engine2/solution_context/memory_record.go b/pkg/engine2/solution_context/memory_record.go
index 3f43ac2d..1e8237aa 100644
--- a/pkg/engine2/solution_context/memory_record.go
+++ b/pkg/engine2/solution_context/memory_record.go
@@ -14,3 +14,11 @@ type (
 func (m *MemoryRecord) AddRecord(context []KV, decision SolveDecision) {
 	m.records = append(m.records, record{context: context, decision: decision})
 }
+
+func (m *MemoryRecord) GetRecords() []SolveDecision {
+	var decisions []SolveDecision
+	for _, record := range m.records {
+		decisions = append(decisions, record.decision)
+	}
+	return decisions
+}
diff --git a/pkg/knowledge_base2/edge_template.go b/pkg/knowledge_base2/edge_template.go
index 33fff559..ab073888 100644
--- a/pkg/knowledge_base2/edge_template.go
+++ b/pkg/knowledge_base2/edge_template.go
@@ -22,7 +22,7 @@ type (
 		// DeletetionDependent is used to specify edges which should not influence the deletion criteria of a resource
 		// a true value specifies the target being deleted is dependent on the source and do not need to depend on
 		// satisfication of the deletion criteria to attempt to delete the true source of the edge.
-		DeletetionDependent bool `yaml:"deletion_dependent"`
+		DeletionDependent bool `yaml:"deletion_dependent"`
 
 		// Unique see type [Unique]
 		Unique Unique `yaml:"unique"`
@@ -51,7 +51,7 @@ type (
 		// DeletetionDependent is used to specify edges which should not influence the deletion criteria of a resource
 		// a true value specifies the target being deleted is dependent on the source and do not need to depend on
 		// satisfication of the deletion criteria to attempt to delete the true source of the edge.
-		DeletetionDependent bool `yaml:"deletion_dependent"`
+		DeletionDependent bool `yaml:"deletion_dependent"`
 
 		// Unique see type [Unique]
 		Unique Unique `yaml:"unique"`
@@ -90,7 +90,7 @@ func EdgeTemplatesFromMulti(multi MultiEdgeTemplate) []EdgeTemplate {
 			Target:                  multi.Resource,
 			DirectEdgeOnly:          multi.DirectEdgeOnly,
 			DeploymentOrderReversed: multi.DeploymentOrderReversed,
-			DeletetionDependent:     multi.DeletetionDependent,
+			DeletionDependent:       multi.DeletionDependent,
 			Unique:                  multi.Unique,
 			OperationalRules:        multi.OperationalRules,
 			EdgeWeightMultiplier:    multi.EdgeWeightMultiplier,
@@ -103,7 +103,7 @@ func EdgeTemplatesFromMulti(multi MultiEdgeTemplate) []EdgeTemplate {
 			Target:                  target,
 			DirectEdgeOnly:          multi.DirectEdgeOnly,
 			DeploymentOrderReversed: multi.DeploymentOrderReversed,
-			DeletetionDependent:     multi.DeletetionDependent,
+			DeletionDependent:       multi.DeletionDependent,
 			Unique:                  multi.Unique,
 			OperationalRules:        multi.OperationalRules,
 			EdgeWeightMultiplier:    multi.EdgeWeightMultiplier,
diff --git a/pkg/knowledge_base2/properties/any_property.go b/pkg/knowledge_base2/properties/any_property.go
index 23fcda30..02709d45 100644
--- a/pkg/knowledge_base2/properties/any_property.go
+++ b/pkg/knowledge_base2/properties/any_property.go
@@ -1,6 +1,8 @@
 package properties
 
 import (
+	"fmt"
+
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
 	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 )
@@ -107,7 +109,10 @@ func (a *AnyProperty) Type() string {
 	return "any"
 }
 
-func (a *AnyProperty) Validate(resource *construct.Resource, value any) error {
+func (a *AnyProperty) Validate(resource *construct.Resource, value any, ctx knowledgebase.DynamicContext) error {
+	if a.Required && value == nil {
+		return fmt.Errorf(knowledgebase.ErrRequiredProperty, a.Path, resource.ID)
+	}
 	return nil
 }
 
diff --git a/pkg/knowledge_base2/properties/bool_property.go b/pkg/knowledge_base2/properties/bool_property.go
index 2b86cc83..d270bac9 100644
--- a/pkg/knowledge_base2/properties/bool_property.go
+++ b/pkg/knowledge_base2/properties/bool_property.go
@@ -83,7 +83,13 @@ func (b *BoolProperty) Type() string {
 	return "bool"
 }
 
-func (b *BoolProperty) Validate(resource *construct.Resource, value any) error {
+func (b *BoolProperty) Validate(resource *construct.Resource, value any, ctx knowledgebase.DynamicContext) error {
+	if value == nil {
+		if b.Required {
+			return fmt.Errorf(knowledgebase.ErrRequiredProperty, b.Path, resource.ID)
+		}
+		return nil
+	}
 	if _, ok := value.(bool); !ok {
 		return fmt.Errorf("invalid bool value %v", value)
 	}
diff --git a/pkg/knowledge_base2/properties/float_property.go b/pkg/knowledge_base2/properties/float_property.go
index 905673bb..4b926cea 100644
--- a/pkg/knowledge_base2/properties/float_property.go
+++ b/pkg/knowledge_base2/properties/float_property.go
@@ -91,16 +91,22 @@ func (f *FloatProperty) Type() string {
 	return "float"
 }
 
-func (f *FloatProperty) Validate(resource *construct.Resource, value any) error {
+func (f *FloatProperty) Validate(resource *construct.Resource, value any, ctx knowledgebase.DynamicContext) error {
+	if value == nil {
+		if f.Required {
+			return fmt.Errorf(knowledgebase.ErrRequiredProperty, f.Path, resource.ID)
+		}
+		return nil
+	}
 	floatVal, ok := value.(float64)
 	if !ok {
-		return fmt.Errorf("invalid int value %v", value)
+		return fmt.Errorf("invalid float value %v", value)
 	}
 	if f.MinValue != nil && floatVal < *f.MinValue {
-		return fmt.Errorf("int value %f is less than lower bound %f", value, *f.MinValue)
+		return fmt.Errorf("float value %f is less than lower bound %f", value, *f.MinValue)
 	}
 	if f.MaxValue != nil && floatVal > *f.MaxValue {
-		return fmt.Errorf("int value %f is greater than upper bound %f", value, *f.MaxValue)
+		return fmt.Errorf("float value %f is greater than upper bound %f", value, *f.MaxValue)
 	}
 	return nil
 }
diff --git a/pkg/knowledge_base2/properties/int_property.go b/pkg/knowledge_base2/properties/int_property.go
index 10459fc1..c8d18ae4 100644
--- a/pkg/knowledge_base2/properties/int_property.go
+++ b/pkg/knowledge_base2/properties/int_property.go
@@ -84,7 +84,13 @@ func (i *IntProperty) Type() string {
 	return "int"
 }
 
-func (i *IntProperty) Validate(resource *construct.Resource, value any) error {
+func (i *IntProperty) Validate(resource *construct.Resource, value any, ctx knowledgebase.DynamicContext) error {
+	if value == nil {
+		if i.Required {
+			return fmt.Errorf(knowledgebase.ErrRequiredProperty, i.Path, resource.ID)
+		}
+		return nil
+	}
 	intVal, ok := value.(int)
 	if !ok {
 		return fmt.Errorf("invalid int value %v", value)
diff --git a/pkg/knowledge_base2/properties/list_property.go b/pkg/knowledge_base2/properties/list_property.go
index cb5e805f..31da2ba1 100644
--- a/pkg/knowledge_base2/properties/list_property.go
+++ b/pkg/knowledge_base2/properties/list_property.go
@@ -167,7 +167,14 @@ func (l *ListProperty) Type() string {
 	return "list"
 }
 
-func (l *ListProperty) Validate(resource *construct.Resource, value any) error {
+func (l *ListProperty) Validate(resource *construct.Resource, value any, ctx knowledgebase.DynamicContext) error {
+	if value == nil {
+		if l.Required {
+			return fmt.Errorf(knowledgebase.ErrRequiredProperty, l.Path, resource.ID)
+		}
+		return nil
+	}
+
 	listVal, ok := value.([]any)
 	if !ok {
 		return fmt.Errorf("invalid map value %v", value)
@@ -183,20 +190,15 @@ func (l *ListProperty) Validate(resource *construct.Resource, value any) error {
 		}
 	}
 	var errs error
-
-	for _, v := range listVal {
-		if len(l.Properties) != 0 {
-			m := MapProperty{Properties: l.Properties}
-			err := m.Validate(resource, v)
-			if err != nil {
-				errs = errors.New(errs.Error() + "\n" + err.Error())
-			}
-		} else {
-			err := l.ItemProperty.Validate(resource, v)
+	// Only validate values if its a primitive list, otherwise let the sub properties handle their own validation
+	if l.ItemProperty != nil {
+		for _, v := range listVal {
+			err := l.ItemProperty.Validate(resource, v, ctx)
 			if err != nil {
 				errs = errors.New(errs.Error() + "\n" + err.Error())
 			}
 		}
+
 	}
 	if errs != nil {
 		return errs
diff --git a/pkg/knowledge_base2/properties/map_property.go b/pkg/knowledge_base2/properties/map_property.go
index 25319ae5..ee8a9b42 100644
--- a/pkg/knowledge_base2/properties/map_property.go
+++ b/pkg/knowledge_base2/properties/map_property.go
@@ -176,7 +176,13 @@ func (m *MapProperty) Type() string {
 	return "map"
 }
 
-func (m *MapProperty) Validate(resource *construct.Resource, value any) error {
+func (m *MapProperty) Validate(resource *construct.Resource, value any, ctx knowledgebase.DynamicContext) error {
+	if value == nil {
+		if m.Required {
+			return fmt.Errorf(knowledgebase.ErrRequiredProperty, m.Path, resource.ID)
+		}
+		return nil
+	}
 	mapVal, ok := value.(map[string]any)
 	if !ok {
 		return fmt.Errorf("invalid map value %v", value)
@@ -192,18 +198,13 @@ func (m *MapProperty) Validate(resource *construct.Resource, value any) error {
 		}
 	}
 	var errs error
+	// Only validate values if its a primitive map, otherwise let the sub properties handle their own validation
 	if m.KeyProperty != nil && m.ValueProperty != nil {
 		for k, v := range mapVal {
-			if err := m.KeyProperty.Validate(resource, k); err != nil {
+			if err := m.KeyProperty.Validate(resource, k, ctx); err != nil {
 				errs = errors.Join(errs, fmt.Errorf("invalid key %v for map property type %s: %w", k, m.KeyProperty.Type(), err))
 			}
-			if err := m.ValueProperty.Validate(resource, v); err != nil {
-				errs = errors.Join(errs, fmt.Errorf("invalid value %v for map property type %s: %w", v, m.ValueProperty.Type(), err))
-			}
-		}
-	} else {
-		for _, v := range mapVal {
-			if err := m.ValueProperty.Validate(resource, v); err != nil {
+			if err := m.ValueProperty.Validate(resource, v, ctx); err != nil {
 				errs = errors.Join(errs, fmt.Errorf("invalid value %v for map property type %s: %w", v, m.ValueProperty.Type(), err))
 			}
 		}
diff --git a/pkg/knowledge_base2/properties/property.go b/pkg/knowledge_base2/properties/property.go
index 500ea8ce..ffc67dff 100644
--- a/pkg/knowledge_base2/properties/property.go
+++ b/pkg/knowledge_base2/properties/property.go
@@ -47,6 +47,35 @@ func ParsePropertyRef(value any, ctx knowledgebase.DynamicContext, data knowledg
 	return construct.PropertyRef{}, fmt.Errorf("invalid property reference value %v", value)
 }
 
+func ValidatePropertyRef(value construct.PropertyRef, propertyType string, ctx knowledgebase.DynamicContext) (refVal any, err error) {
+	resource, err := ctx.DAG().Vertex(value.Resource)
+	if err != nil {
+		return nil, fmt.Errorf("error getting resource %s, while validating property ref: %w", value.Resource, err)
+	}
+	if resource == nil {
+		return nil, fmt.Errorf("resource %s does not exist", value.Resource)
+	}
+	rt, err := ctx.KB().GetResourceTemplate(value.Resource)
+	if err != nil {
+		return nil, err
+	}
+	prop := rt.GetProperty(value.Property)
+	if prop == nil {
+		return nil, fmt.Errorf("property %s does not exist on resource %s", value.Property, value.Resource)
+	}
+	if prop.Type() != propertyType {
+		return nil, fmt.Errorf("property %s on resource %s is not of type %s", value.Property, value.Resource, propertyType)
+	}
+	if prop.Details().DeployTime {
+		return nil, nil
+	}
+	propVal, err := resource.GetProperty(value.Property)
+	if err != nil {
+		return nil, fmt.Errorf("error getting property %s on resource %s, while validating property ref: %w", value.Property, value.Resource, err)
+	}
+	return propVal, nil
+}
+
 func (p *PropertyValidityCheck) Validate(value any, properties construct.Properties) error {
 	var buff bytes.Buffer
 	data := ValidityCheckData{
diff --git a/pkg/knowledge_base2/properties/resource_property.go b/pkg/knowledge_base2/properties/resource_property.go
index 5d559cfe..75435857 100644
--- a/pkg/knowledge_base2/properties/resource_property.go
+++ b/pkg/knowledge_base2/properties/resource_property.go
@@ -3,7 +3,6 @@ package properties
 import (
 	"fmt"
 
-	"github.com/klothoplatform/klotho/pkg/collectionutil"
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
 	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 )
@@ -126,12 +125,18 @@ func (r *ResourceProperty) Type() string {
 	return "resource"
 }
 
-func (r *ResourceProperty) Validate(resource *construct.Resource, value any) error {
+func (r *ResourceProperty) Validate(resource *construct.Resource, value any, ctx knowledgebase.DynamicContext) error {
+	if value == nil {
+		if r.Required {
+			return fmt.Errorf(knowledgebase.ErrRequiredProperty, r.Path, resource.ID)
+		}
+		return nil
+	}
 	id, ok := value.(construct.ResourceId)
 	if !ok {
 		return fmt.Errorf("invalid resource value %v", value)
 	}
-	if !collectionutil.Contains(r.AllowedTypes, id) {
+	if r.AllowedTypes != nil && len(r.AllowedTypes) > 0 && !r.AllowedTypes.MatchesAny(id) {
 		return fmt.Errorf("resource value %v does not match allowed types %s", value, r.AllowedTypes)
 	}
 	return nil
diff --git a/pkg/knowledge_base2/properties/set_property.go b/pkg/knowledge_base2/properties/set_property.go
index 94a1ccb8..c0abd232 100644
--- a/pkg/knowledge_base2/properties/set_property.go
+++ b/pkg/knowledge_base2/properties/set_property.go
@@ -156,7 +156,13 @@ func (s *SetProperty) Type() string {
 	return "set"
 }
 
-func (s *SetProperty) Validate(resource *construct.Resource, value any) error {
+func (s *SetProperty) Validate(resource *construct.Resource, value any, ctx knowledgebase.DynamicContext) error {
+	if value == nil {
+		if s.Required {
+			return fmt.Errorf(knowledgebase.ErrRequiredProperty, s.Path, resource.ID)
+		}
+		return nil
+	}
 	setVal, ok := value.(set.HashedSet[string, any])
 	if !ok {
 		return fmt.Errorf("could not validate set property: invalid set value %v", value)
@@ -172,14 +178,17 @@ func (s *SetProperty) Validate(resource *construct.Resource, value any) error {
 		}
 	}
 
-	var errs error
-	for _, item := range setVal.ToSlice() {
-		if err := s.ItemProperty.Validate(resource, item); err != nil {
-			errs = errors.Join(errs, fmt.Errorf("invalid item %v: %v", item, err))
+	// Only validate values if its a primitive list, otherwise let the sub properties handle their own validation
+	if s.ItemProperty != nil {
+		var errs error
+		for _, item := range setVal.ToSlice() {
+			if err := s.ItemProperty.Validate(resource, item, ctx); err != nil {
+				errs = errors.Join(errs, fmt.Errorf("invalid item %v: %v", item, err))
+			}
+		}
+		if errs != nil {
+			return errs
 		}
-	}
-	if errs != nil {
-		return errs
 	}
 	return nil
 }
diff --git a/pkg/knowledge_base2/properties/string_property.go b/pkg/knowledge_base2/properties/string_property.go
index b9424191..e58980d4 100644
--- a/pkg/knowledge_base2/properties/string_property.go
+++ b/pkg/knowledge_base2/properties/string_property.go
@@ -92,15 +92,34 @@ func (s *StringProperty) Type() string {
 	return "string"
 }
 
-func (s *StringProperty) Validate(resource *construct.Resource, value any) error {
+func (s *StringProperty) Validate(resource *construct.Resource, value any, ctx knowledgebase.DynamicContext) error {
+	if value == nil {
+		if s.Required {
+			return fmt.Errorf(knowledgebase.ErrRequiredProperty, s.Path, resource.ID)
+		}
+		return nil
+	}
 	stringVal, ok := value.(string)
 	if !ok {
-		return fmt.Errorf("could not validate property: invalid string value %v", value)
-	}
-	if s.AllowedValues != nil {
-		if !collectionutil.Contains(s.AllowedValues, stringVal) {
-			return fmt.Errorf("value %s is not allowed. allowed values are %s", stringVal, s.AllowedValues)
+		propertyRef, ok := value.(construct.PropertyRef)
+		if !ok {
+			return fmt.Errorf("could not validate property: invalid string value %v", value)
 		}
+		refVal, err := ValidatePropertyRef(propertyRef, s.Type(), ctx)
+		if err != nil {
+			return err
+		}
+		if refVal == nil {
+			return nil
+		}
+		stringVal, ok = refVal.(string)
+		if !ok {
+			return fmt.Errorf("could not validate property: invalid string value %v", value)
+		}
+	}
+
+	if s.AllowedValues != nil && len(s.AllowedValues) > 0 && !collectionutil.Contains(s.AllowedValues, stringVal) {
+		return fmt.Errorf("value %s is not allowed. allowed values are %s", stringVal, s.AllowedValues)
 	}
 
 	if s.SanitizeTmpl != nil {
@@ -110,7 +129,7 @@ func (s *StringProperty) Validate(resource *construct.Resource, value any) error
 			return err
 		}
 		if oldVal != stringVal {
-			return fmt.Errorf("value %s was sanitized to %s", oldVal, stringVal)
+			return fmt.Errorf("value %s did not pass sanitization rules. suggested value: %s", oldVal, stringVal)
 		}
 	}
 	return nil
diff --git a/pkg/knowledge_base2/reader/properties.go b/pkg/knowledge_base2/reader/properties.go
index 98224cf5..5a2b53c3 100644
--- a/pkg/knowledge_base2/reader/properties.go
+++ b/pkg/knowledge_base2/reader/properties.go
@@ -198,6 +198,9 @@ var fieldConversion = map[string]func(val reflect.Value, p *Property, kp knowled
 		if !ok {
 			return fmt.Errorf("invalid sanitize template")
 		}
+		if sanitizeTmpl == "" {
+			return nil
+		}
 		// generate random uuid as the name of the template
 		name := uuid.New().String()
 		tmpl, err := knowledgebase.NewSanitizationTmpl(name, sanitizeTmpl)
diff --git a/pkg/knowledge_base2/resource_template.go b/pkg/knowledge_base2/resource_template.go
index 5bf1a07a..38043097 100644
--- a/pkg/knowledge_base2/resource_template.go
+++ b/pkg/knowledge_base2/resource_template.go
@@ -81,7 +81,7 @@ type (
 		// GetDefaultValue returns the default value for the property, pertaining to the specific data being passed in for execution
 		GetDefaultValue(ctx DynamicContext, data DynamicValueData) (any, error)
 		// Validate ensures the value is valid for the property and returns an error if it is not
-		Validate(resource *construct.Resource, value any) error
+		Validate(resource *construct.Resource, value any, ctx DynamicContext) error
 		// SubProperties returns the sub properties of the property, if any. This is used for properties that are complex structures,
 		// such as lists, sets, or maps
 		SubProperties() Properties
@@ -141,6 +141,8 @@ type (
 )
 
 const (
+	ErrRequiredProperty = "required property %s is not set on resource %s"
+
 	Compute   Functionality = "compute"
 	Cluster   Functionality = "cluster"
 	Storage   Functionality = "storage"
diff --git a/pkg/templates/aws/resources/ecr_image.yaml b/pkg/templates/aws/resources/ecr_image.yaml
index 8ce6c099..e742f877 100644
--- a/pkg/templates/aws/resources/ecr_image.yaml
+++ b/pkg/templates/aws/resources/ecr_image.yaml
@@ -4,6 +4,7 @@ display_name: ECR Image
 properties:
   BaseImage:
     type: string
+    required: true
   Tag:
     type: string
   Repo:
diff --git a/pkg/templates/aws/resources/rds_instance.yaml b/pkg/templates/aws/resources/rds_instance.yaml
index 74587f76..0617f570 100644
--- a/pkg/templates/aws/resources/rds_instance.yaml
+++ b/pkg/templates/aws/resources/rds_instance.yaml
@@ -35,6 +35,14 @@ properties:
   DatabaseName:
     type: string
     default_value: main
+    sanitize: |
+      {{ . 
+        | replace `^[^[:alpha:]]+` "" 
+        | replace `--+` "-" 
+        | replace `-$` ""
+        | replace `[^[:alnum:]-]+` "-"
+        | length 1 63
+      }}  
   IamDatabaseAuthenticationEnabled:
     type: bool
     default_value: true
diff --git a/pkg/templates/aws/resources/sqs_queue.yaml b/pkg/templates/aws/resources/sqs_queue.yaml
index 2e90943b..4f226e80 100644
--- a/pkg/templates/aws/resources/sqs_queue.yaml
+++ b/pkg/templates/aws/resources/sqs_queue.yaml
@@ -25,6 +25,7 @@ path_satisfaction:
 classification:
   is:
     - queue
+    - messaging
 
 delete_context:
   requires_no_upstream_or_downstream: true


Test patch:
diff --git a/pkg/engine2/enginetesting/mock_solution.go b/pkg/engine2/enginetesting/mock_solution.go
index f0c2fac3..3a540774 100644
--- a/pkg/engine2/enginetesting/mock_solution.go
+++ b/pkg/engine2/enginetesting/mock_solution.go
@@ -31,6 +31,11 @@ func (m *MockSolution) RecordDecision(d solution_context.SolveDecision) {
 	m.Called(d)
 }
 
+func (m *MockSolution) GetDecisions() solution_context.DecisionRecords {
+	args := m.Called()
+	return args.Get(0).(solution_context.DecisionRecords)
+}
+
 func (m *MockSolution) DataflowGraph() construct.Graph {
 	args := m.Called()
 	return args.Get(0).(construct.Graph)
diff --git a/pkg/engine2/enginetesting/test_solution.go b/pkg/engine2/enginetesting/test_solution.go
index db50f756..52eae8b3 100644
--- a/pkg/engine2/enginetesting/test_solution.go
+++ b/pkg/engine2/enginetesting/test_solution.go
@@ -58,6 +58,10 @@ func (sol *TestSolution) Constraints() *constraints.Constraints {
 
 func (sol *TestSolution) RecordDecision(d solution_context.SolveDecision) {}
 
+func (sol *TestSolution) GetDecisions() solution_context.DecisionRecords {
+	return nil
+}
+
 func (sol *TestSolution) DataflowGraph() construct.Graph {
 	return sol.dataflow
 }
diff --git a/pkg/engine2/reconciler/reconciler_test.go b/pkg/engine2/reconciler/reconciler_test.go
index e3835e9d..8fdd312d 100644
--- a/pkg/engine2/reconciler/reconciler_test.go
+++ b/pkg/engine2/reconciler/reconciler_test.go
@@ -1,1 +1,420 @@
 package reconciler
+
+import (
+	"testing"
+
+	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
+	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
+	"github.com/klothoplatform/klotho/pkg/knowledge_base2/properties"
+	"github.com/klothoplatform/klotho/pkg/set"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func Test_canDeleteResource(t *testing.T) {
+	type args struct {
+		resource    construct.ResourceId
+		explicit    bool
+		template    *knowledgebase.ResourceTemplate
+		upstreams   []construct.ResourceId
+		downstreams []construct.ResourceId
+		mockKBCalls []mock.Call
+	}
+	tests := []struct {
+		name        string
+		args        args
+		mockKBCalls []mock.Call
+		want        bool
+		wantErr     bool
+	}{
+		{
+			name: "can delete resource when no upstreams or downstreams",
+			args: args{
+				resource: construct.ResourceId{},
+				explicit: false,
+				template: &knowledgebase.ResourceTemplate{},
+			},
+			want: true,
+		},
+		{
+			name: "can delete if explicit",
+			args: args{
+				resource: construct.ResourceId{},
+				explicit: true,
+				template: &knowledgebase.ResourceTemplate{},
+				upstreams: []construct.ResourceId{
+					{Type: "one"},
+				},
+				downstreams: []construct.ResourceId{
+					{Type: "two"},
+				},
+			},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetEdgeTemplate",
+					Arguments: mock.Arguments{
+						mock.Anything,
+						mock.Anything,
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.EdgeTemplate{},
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "cannot delete if functional and not explicit",
+			args: args{
+				resource: construct.ResourceId{},
+				explicit: false,
+				template: &knowledgebase.ResourceTemplate{
+					Classification: knowledgebase.Classification{Is: []string{string(knowledgebase.Compute)}},
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			upstreamSet := set.Set[construct.ResourceId]{}
+			upstreamSet.Add(tt.args.upstreams...)
+			downstreamSet := set.Set[construct.ResourceId]{}
+			downstreamSet.Add(tt.args.downstreams...)
+			sol := &enginetesting.TestSolution{
+				KB: enginetesting.MockKB{},
+			}
+			for _, call := range tt.mockKBCalls {
+				sol.KB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			got, err := canDeleteResource(sol, tt.args.resource, tt.args.explicit, tt.args.template, upstreamSet, downstreamSet)
+			if tt.wantErr {
+				assert.Error(err)
+				return
+			}
+			if !assert.NoError(err) {
+				return
+			}
+			assert.Equal(tt.want, got)
+		})
+	}
+}
+
+func Test_ignoreCriteria(t *testing.T) {
+	type args struct {
+		resource  construct.ResourceId
+		nodes     []construct.ResourceId
+		direction knowledgebase.Direction
+	}
+	tests := []struct {
+		name        string
+		args        args
+		mockKBCalls []mock.Call
+		want        bool
+	}{
+		{
+			name: "ignore criteria when no nodes",
+			args: args{
+				resource:  construct.ResourceId{Type: "one"},
+				direction: knowledgebase.DirectionUpstream,
+			},
+			want: true,
+		},
+		{
+			name: "ignore criteria when single upstream node is deletion dependent",
+			args: args{
+				resource: construct.ResourceId{Type: "one"},
+				nodes: []construct.ResourceId{
+					{Type: "two"},
+				},
+				direction: knowledgebase.DirectionUpstream,
+			},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetEdgeTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Type: "two"},
+						construct.ResourceId{Type: "one"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.EdgeTemplate{
+							DeletionDependent: true,
+						},
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "ignore criteria is false when only one upstream node is deletion dependent",
+			args: args{
+				resource: construct.ResourceId{Type: "one"},
+				nodes: []construct.ResourceId{
+					{Type: "two"},
+					{Type: "three"},
+				},
+				direction: knowledgebase.DirectionUpstream,
+			},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetEdgeTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Type: "two"},
+						construct.ResourceId{Type: "one"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.EdgeTemplate{
+							DeletionDependent: true,
+						},
+					},
+				},
+				{
+					Method: "GetEdgeTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Type: "three"},
+						construct.ResourceId{Type: "one"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.EdgeTemplate{},
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "ignore criteria when single downstream node is deletion dependent",
+			args: args{
+				resource: construct.ResourceId{Type: "one"},
+				nodes: []construct.ResourceId{
+					{Type: "two"},
+				},
+				direction: knowledgebase.DirectionDownstream,
+			},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetEdgeTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Type: "one"},
+						construct.ResourceId{Type: "two"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.EdgeTemplate{
+							DeletionDependent: true,
+						},
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "ignore criteria is false when only one downstream node is deletion dependent",
+			args: args{
+				resource: construct.ResourceId{Type: "one"},
+				nodes: []construct.ResourceId{
+					{Type: "two"},
+					{Type: "three"},
+				},
+				direction: knowledgebase.DirectionDownstream,
+			},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetEdgeTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Type: "one"},
+						construct.ResourceId{Type: "two"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.EdgeTemplate{
+							DeletionDependent: true,
+						},
+					},
+				},
+				{
+					Method: "GetEdgeTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Type: "one"},
+						construct.ResourceId{Type: "three"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.EdgeTemplate{},
+					},
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			nodesSet := set.Set[construct.ResourceId]{}
+			nodesSet.Add(tt.args.nodes...)
+			sol := &enginetesting.TestSolution{
+				KB: enginetesting.MockKB{},
+			}
+			for _, call := range tt.mockKBCalls {
+				sol.KB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			got := ignoreCriteria(sol, tt.args.resource, nodesSet, tt.args.direction)
+			assert.Equal(tt.want, got)
+		})
+	}
+}
+
+func Test_findAllResourcesInNamespace(t *testing.T) {
+
+	mockCall := mock.Call{
+		Method: "GetResourceTemplate",
+		Arguments: mock.Arguments{
+			mock.Anything,
+		},
+		ReturnArguments: mock.Arguments{
+			&knowledgebase.ResourceTemplate{
+				Properties: knowledgebase.Properties{
+					"namespaceProp": &properties.ResourceProperty{
+						PropertyDetails: knowledgebase.PropertyDetails{
+							Path:      "namespaceProp",
+							Namespace: true,
+						},
+					},
+				},
+			},
+			nil,
+		},
+	}
+	type args struct {
+		namespace construct.ResourceId
+	}
+	tests := []struct {
+		name        string
+		args        args
+		resources   []*construct.Resource
+		mockKBCalls []mock.Call
+		want        []construct.ResourceId
+	}{
+		{
+			name: "nothing if not namespaced",
+			args: args{
+				namespace: construct.ResourceId{Type: "one", Name: "one"},
+			},
+			mockKBCalls: []mock.Call{mockCall},
+			resources: []*construct.Resource{
+				{
+					ID: construct.ResourceId{Type: "two"},
+					Properties: construct.Properties{
+						"namespaceProp": construct.ResourceId{Type: "one"},
+					},
+				},
+			},
+			want: []construct.ResourceId{},
+		},
+		{
+			name: "nothing if not namespaced to a different resource",
+			args: args{
+				namespace: construct.ResourceId{Type: "one", Name: "one"},
+			},
+			mockKBCalls: []mock.Call{mockCall},
+			resources: []*construct.Resource{
+				{
+					ID: construct.ResourceId{Type: "two", Namespace: "not one"},
+					Properties: construct.Properties{
+						"namespaceProp": construct.ResourceId{Type: "one"},
+					},
+				},
+			},
+			want: []construct.ResourceId{},
+		},
+		{
+			name: "finds matching namespaces by property ref",
+			args: args{
+				namespace: construct.ResourceId{Type: "one", Name: "one"},
+			},
+			mockKBCalls: []mock.Call{{
+				Method: "GetResourceTemplate",
+				Arguments: mock.Arguments{
+					mock.Anything,
+				},
+				ReturnArguments: mock.Arguments{
+					&knowledgebase.ResourceTemplate{
+						Properties: knowledgebase.Properties{
+							"namespaceProp": &properties.StringProperty{
+								PropertyDetails: knowledgebase.PropertyDetails{
+									Path:      "namespaceProp",
+									Namespace: true,
+								},
+							},
+						},
+					},
+					nil,
+				},
+			}},
+			resources: []*construct.Resource{
+				{
+					ID: construct.ResourceId{Type: "two", Namespace: "one"},
+					Properties: construct.Properties{
+						"namespaceProp": "one",
+					},
+				},
+				{
+					ID: construct.ResourceId{Type: "three", Namespace: "one"},
+					Properties: construct.Properties{
+						"namespaceProp": construct.PropertyRef{
+							Resource: construct.ResourceId{Type: "one"},
+							Property: "name",
+						},
+					},
+				},
+			},
+			want: []construct.ResourceId{
+				{Type: "three", Namespace: "one"},
+			},
+		},
+		{
+			name: "finds multiple matching namespace",
+			args: args{
+				namespace: construct.ResourceId{Type: "one", Name: "one"},
+			},
+			mockKBCalls: []mock.Call{mockCall},
+			resources: []*construct.Resource{
+				{
+					ID: construct.ResourceId{Type: "two", Namespace: "one"},
+					Properties: construct.Properties{
+						"namespaceProp": construct.ResourceId{Type: "one"},
+					},
+				},
+				{
+					ID: construct.ResourceId{Type: "three", Namespace: "one"},
+					Properties: construct.Properties{
+						"namespaceProp": construct.ResourceId{Type: "one"},
+					},
+				},
+			},
+			want: []construct.ResourceId{
+				{Type: "two", Namespace: "one"},
+				{Type: "three", Namespace: "one"},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			sol := enginetesting.NewTestSolution()
+			sol.KB = enginetesting.MockKB{}
+			for _, call := range tt.mockKBCalls {
+				sol.KB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			for _, resource := range tt.resources {
+				err := sol.RawView().AddVertex(resource)
+				if !assert.NoError(err) {
+					return
+				}
+			}
+			got, err := findAllResourcesInNamespace(sol, tt.args.namespace)
+			if !assert.NoError(err) {
+				return
+			}
+			assert.ElementsMatch(tt.want, got.ToSlice())
+		})
+	}
+}
diff --git a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
index 801d089c..63130dfb 100755
--- a/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/ecs_rds.dataflow-viz.yaml
@@ -12,6 +12,6 @@ resources:
     parent: vpc/vpc-0
 
   ecs_service/ecs_service_0 -> rds_instance/rds-instance-2:
-    path: aws:ecs_task_definition:ecs_service_0,aws:iam_role:ecs_service_0-rds-instance-2
+    path: aws:ecs_task_definition:ecs_service_0,aws:iam_role:ecs_service_0-execution-role
 
 
diff --git a/pkg/engine2/testdata/ecs_rds.expect.yaml b/pkg/engine2/testdata/ecs_rds.expect.yaml
index aa5311cc..608fb38a 100755
--- a/pkg/engine2/testdata/ecs_rds.expect.yaml
+++ b/pkg/engine2/testdata/ecs_rds.expect.yaml
@@ -34,7 +34,7 @@ resources:
             rds-instance-2_RDS_ENDPOINT: aws:rds_instance:rds-instance-2#Endpoint
             rds-instance-2_RDS_PASSWORD: aws:rds_instance:rds-instance-2#Password
             rds-instance-2_RDS_USERNAME: aws:rds_instance:rds-instance-2#Username
-        ExecutionRole: aws:iam_role:ecs_service_0-rds-instance-2
+        ExecutionRole: aws:iam_role:ecs_service_0-execution-role
         Image: aws:ecr_image:ecs_service_0-image
         LogGroup: aws:log_group:ecs_service_0-log-group
         Memory: "512"
@@ -46,12 +46,12 @@ resources:
         Region: aws:region:region-0
         RequiresCompatibilities:
             - FARGATE
-        TaskRole: aws:iam_role:ecs_service_0-rds-instance-2
+        TaskRole: aws:iam_role:ecs_service_0-execution-role
     aws:ecr_image:ecs_service_0-image:
         Context: .
         Dockerfile: ecs_service_0-image.Dockerfile
         Repo: aws:ecr_repo:ecr_repo-0
-    aws:iam_role:ecs_service_0-rds-instance-2:
+    aws:iam_role:ecs_service_0-execution-role:
         AssumeRolePolicyDoc:
             Statement:
                 - Action:
@@ -209,11 +209,11 @@ edges:
     aws:ecs_service:ecs_service_0 -> aws:subnet:vpc-0:subnet-0:
     aws:ecs_service:ecs_service_0 -> aws:subnet:vpc-0:subnet-1:
     aws:ecs_task_definition:ecs_service_0 -> aws:ecr_image:ecs_service_0-image:
-    aws:ecs_task_definition:ecs_service_0 -> aws:iam_role:ecs_service_0-rds-instance-2:
+    aws:ecs_task_definition:ecs_service_0 -> aws:iam_role:ecs_service_0-execution-role:
     aws:ecs_task_definition:ecs_service_0 -> aws:log_group:ecs_service_0-log-group:
     aws:ecs_task_definition:ecs_service_0 -> aws:region:region-0:
     aws:ecr_image:ecs_service_0-image -> aws:ecr_repo:ecr_repo-0:
-    aws:iam_role:ecs_service_0-rds-instance-2 -> aws:rds_instance:rds-instance-2:
+    aws:iam_role:ecs_service_0-execution-role -> aws:rds_instance:rds-instance-2:
     aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway -> aws:elastic_ip:subnet-0-route_table-nat_gateway-elastic_ip:
     aws:nat_gateway:subnet-2:subnet-0-route_table-nat_gateway -> aws:subnet:vpc-0:subnet-2:
     aws:subnet:vpc-0:subnet-2 -> aws:availability_zone:region-0:availability_zone-0:
diff --git a/pkg/engine2/testdata/ecs_rds.iac-viz.yaml b/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
index 7e272c98..f29d858b 100755
--- a/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
+++ b/pkg/engine2/testdata/ecs_rds.iac-viz.yaml
@@ -59,8 +59,8 @@ resources:
   ecr_image/ecs_service_0-image:
   ecr_image/ecs_service_0-image -> ecr_repo/ecr_repo-0:
 
-  iam_role/ecs_service_0-rds-instance-2:
-  iam_role/ecs_service_0-rds-instance-2 -> rds_instance/rds-instance-2:
+  iam_role/ecs_service_0-execution-role:
+  iam_role/ecs_service_0-execution-role -> rds_instance/rds-instance-2:
 
   log_group/ecs_service_0-log-group:
 
@@ -84,7 +84,7 @@ resources:
 
   ecs_task_definition/ecs_service_0:
   ecs_task_definition/ecs_service_0 -> ecr_image/ecs_service_0-image:
-  ecs_task_definition/ecs_service_0 -> iam_role/ecs_service_0-rds-instance-2:
+  ecs_task_definition/ecs_service_0 -> iam_role/ecs_service_0-execution-role:
   ecs_task_definition/ecs_service_0 -> log_group/ecs_service_0-log-group:
   ecs_task_definition/ecs_service_0 -> region/region-0:
 
diff --git a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
index 5a612b6c..e1a2a5a4 100755
--- a/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
+++ b/pkg/engine2/testdata/k8s_api.dataflow-viz.yaml
@@ -4,11 +4,11 @@ resources:
     parent: eks_cluster/eks_cluster-0
 
 
-  load_balancer/rest-api-4-integbcc77100:
+  load_balancer/rest-api-4-integ6897f0b9:
     parent: vpc/vpc-0
 
-  load_balancer/rest-api-4-integbcc77100 -> kubernetes:pod:eks_cluster-0/pod2:
-    path: aws:load_balancer_listener:rest_api_4_integration_0-pod2,aws:target_group:rest-api-4-integbcc77100,kubernetes:target_group_binding:restapi4integration0-pod2,kubernetes:service:restapi4integration0-pod2
+  load_balancer/rest-api-4-integ6897f0b9 -> kubernetes:pod:eks_cluster-0/pod2:
+    path: aws:load_balancer_listener:rest_api_4_integration_0-eks_cluster-0,aws:target_group:rest-api-4-integ6897f0b9,kubernetes:target_group_binding:restapi4integration0-ekscluster-0,kubernetes:service:restapi4integration0-pod2
 
 
   kubernetes:helm_chart:eks_cluster-0/metricsserver:
@@ -31,7 +31,7 @@ resources:
   aws:api_integration:rest_api_4/rest_api_4_integration_0:
     parent: rest_api/rest_api_4
 
-  aws:api_integration:rest_api_4/rest_api_4_integration_0 -> load_balancer/rest-api-4-integbcc77100:
-    path: aws:vpc_link:rest_api_4_integration_0-pod2
+  aws:api_integration:rest_api_4/rest_api_4_integration_0 -> load_balancer/rest-api-4-integ6897f0b9:
+    path: aws:vpc_link:rest_api_4_integration_0-eks_cluster-0
 
 
diff --git a/pkg/engine2/testdata/k8s_api.expect.yaml b/pkg/engine2/testdata/k8s_api.expect.yaml
index 2000c7f9..a43e5e6d 100755
--- a/pkg/engine2/testdata/k8s_api.expect.yaml
+++ b/pkg/engine2/testdata/k8s_api.expect.yaml
@@ -111,26 +111,26 @@ resources:
         Resource: aws:api_resource:rest_api_4:api_resource-0
         RestApi: aws:rest_api:rest_api_4
         Route: /{proxy+}
-        Target: aws:load_balancer:rest-api-4-integbcc77100
+        Target: aws:load_balancer:rest-api-4-integ6897f0b9
         Type: HTTP_PROXY
         Uri: aws:api_integration:rest_api_4:rest_api_4_integration_0#LbUri
-        VpcLink: aws:vpc_link:rest_api_4_integration_0-pod2
-    aws:vpc_link:rest_api_4_integration_0-pod2:
-        Target: aws:load_balancer:rest-api-4-integbcc77100
-    aws:load_balancer:rest-api-4-integbcc77100:
+        VpcLink: aws:vpc_link:rest_api_4_integration_0-eks_cluster-0
+    aws:vpc_link:rest_api_4_integration_0-eks_cluster-0:
+        Target: aws:load_balancer:rest-api-4-integ6897f0b9
+    aws:load_balancer:rest-api-4-integ6897f0b9:
         Scheme: internal
         Subnets:
             - aws:subnet:vpc-0:subnet-0
             - aws:subnet:vpc-0:subnet-1
         Type: network
-    aws:load_balancer_listener:rest_api_4_integration_0-pod2:
+    aws:load_balancer_listener:rest_api_4_integration_0-eks_cluster-0:
         DefaultActions:
-            - TargetGroup: aws:target_group:rest-api-4-integbcc77100
+            - TargetGroup: aws:target_group:rest-api-4-integ6897f0b9
               Type: forward
-        LoadBalancer: aws:load_balancer:rest-api-4-integbcc77100
+        LoadBalancer: aws:load_balancer:rest-api-4-integ6897f0b9
         Port: 80
         Protocol: TCP
-    aws:target_group:rest-api-4-integbcc77100:
+    aws:target_group:rest-api-4-integ6897f0b9:
         HealthCheck:
             Enabled: true
             HealthyThreshold: 5
@@ -143,19 +143,19 @@ resources:
         Protocol: TCP
         TargetType: ip
         Vpc: aws:vpc:vpc-0
-    kubernetes:target_group_binding:restapi4integration0-pod2:
+    kubernetes:target_group_binding:restapi4integration0-ekscluster-0:
         Object:
             apiVersion: elbv2.k8s.aws/v1beta1
             kind: TargetGroupBinding
             metadata:
                 labels:
-                    KLOTHO_ID_LABEL: restapi4integration0-pod2
-                name: restapi4integration0-pod2
+                    KLOTHO_ID_LABEL: restapi4integration0-ekscluster-0
+                name: restapi4integration0-ekscluster-0
             spec:
                 serviceRef:
                     name: restapi4integration0-pod2
                     port: 80
-                targetGroupARN: aws:target_group:rest-api-4-integbcc77100#Arn
+                targetGroupARN: aws:target_group:rest-api-4-integ6897f0b9#Arn
     kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller:
         Chart: aws-load-balancer-controller
         Cluster: aws:eks_cluster:eks_cluster-0
@@ -285,12 +285,12 @@ resources:
                         - ec2.amazonaws.com
             Version: "2012-10-17"
         ManagedPolicies:
+            - 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
-            - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
     aws:iam_role:pod2:
         AssumeRolePolicyDoc:
             Statement:
@@ -503,27 +503,27 @@ resources:
               ToPort: 0
         IngressRules:
             - CidrBlocks:
-                - 10.0.128.0/18
-              Description: Allow ingress traffic from ip addresses within the subnet subnet-0
+                - 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"
+              Self: true
               ToPort: 0
             - CidrBlocks:
-                - 10.0.192.0/18
-              Description: Allow ingress traffic from ip addresses within the subnet subnet-1
+                - 10.0.128.0/18
+              Description: Allow ingress traffic from ip addresses within the subnet subnet-0
               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
-            - Description: Allow ingress traffic from within the same security group
+                - 10.0.192.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:subnet-0-route_table:
@@ -609,16 +609,17 @@ edges:
     aws:api_resource:rest_api_4:api_resource-0 -> aws:api_integration:rest_api_4:rest_api_4_integration_0:
     aws:api_resource:rest_api_4:api_resource-0 -> aws:api_method:rest_api_4:rest_api_4_integration_0_method:
     aws:api_method:rest_api_4:rest_api_4_integration_0_method -> aws:api_integration:rest_api_4:rest_api_4_integration_0:
-    aws:api_integration:rest_api_4:rest_api_4_integration_0 -> aws:vpc_link:rest_api_4_integration_0-pod2:
-    aws:vpc_link:rest_api_4_integration_0-pod2 -> aws:load_balancer:rest-api-4-integbcc77100:
-    aws:load_balancer:rest-api-4-integbcc77100 -> aws:load_balancer_listener:rest_api_4_integration_0-pod2:
-    aws:load_balancer:rest-api-4-integbcc77100 -> aws:subnet:vpc-0:subnet-0:
-    aws:load_balancer:rest-api-4-integbcc77100 -> aws:subnet:vpc-0:subnet-1:
-    aws:load_balancer_listener:rest_api_4_integration_0-pod2 -> aws:target_group:rest-api-4-integbcc77100:
-    aws:target_group:rest-api-4-integbcc77100 -> kubernetes:target_group_binding:restapi4integration0-pod2:
-    kubernetes:target_group_binding:restapi4integration0-pod2 -> aws:eks_cluster:eks_cluster-0:
-    kubernetes:target_group_binding:restapi4integration0-pod2 -> kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller:
-    kubernetes:target_group_binding:restapi4integration0-pod2 -> kubernetes:service:restapi4integration0-pod2:
+    aws:api_integration:rest_api_4:rest_api_4_integration_0 -> aws:vpc_link:rest_api_4_integration_0-eks_cluster-0:
+    aws:vpc_link:rest_api_4_integration_0-eks_cluster-0 -> aws:load_balancer:rest-api-4-integ6897f0b9:
+    aws:load_balancer:rest-api-4-integ6897f0b9 -> aws:load_balancer_listener:rest_api_4_integration_0-eks_cluster-0:
+    aws:load_balancer:rest-api-4-integ6897f0b9 -> aws:subnet:vpc-0:subnet-0:
+    aws:load_balancer:rest-api-4-integ6897f0b9 -> aws:subnet:vpc-0:subnet-1:
+    aws:load_balancer_listener:rest_api_4_integration_0-eks_cluster-0 -> aws:target_group:rest-api-4-integ6897f0b9:
+    aws:target_group:rest-api-4-integ6897f0b9 -> kubernetes:target_group_binding:restapi4integration0-ekscluster-0:
+    kubernetes:target_group_binding:restapi4integration0-ekscluster-0 -> aws:eks_cluster:eks_cluster-0:
+    ? kubernetes:target_group_binding:restapi4integration0-ekscluster-0 -> kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller
+    :
+    kubernetes:target_group_binding:restapi4integration0-ekscluster-0 -> kubernetes:service:restapi4integration0-pod2:
     kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller -> aws:eks_cluster:eks_cluster-0:
     kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller -> aws:region:region-0:
     ? kubernetes:helm_chart:eks_cluster-0:aws-load-balancer-controller -> kubernetes:service_account:eks_cluster-0:aws-load-balancer-controller
diff --git a/pkg/engine2/testdata/k8s_api.iac-viz.yaml b/pkg/engine2/testdata/k8s_api.iac-viz.yaml
index 30e80855..f9fe7cc3 100755
--- a/pkg/engine2/testdata/k8s_api.iac-viz.yaml
+++ b/pkg/engine2/testdata/k8s_api.iac-viz.yaml
@@ -48,9 +48,9 @@ resources:
   aws:api_resource:rest_api_4/api_resource-0:
   aws:api_resource:rest_api_4/api_resource-0 -> rest_api/rest_api_4:
 
-  load_balancer/rest-api-4-integbcc77100:
-  load_balancer/rest-api-4-integbcc77100 -> aws:subnet:vpc-0/subnet-0:
-  load_balancer/rest-api-4-integbcc77100 -> aws:subnet:vpc-0/subnet-1:
+  load_balancer/rest-api-4-integ6897f0b9:
+  load_balancer/rest-api-4-integ6897f0b9 -> aws:subnet:vpc-0/subnet-0:
+  load_balancer/rest-api-4-integ6897f0b9 -> aws:subnet:vpc-0/subnet-1:
 
   iam_role/aws-load-balancer-controller:
   iam_role/aws-load-balancer-controller -> iam_oidc_provider/eks_cluster-0:
@@ -84,8 +84,8 @@ resources:
   aws:api_method:rest_api_4/rest_api_4_integration_0_method -> aws:api_resource:rest_api_4/api_resource-0:
   aws:api_method:rest_api_4/rest_api_4_integration_0_method -> rest_api/rest_api_4:
 
-  vpc_link/rest_api_4_integration_0-pod2:
-  vpc_link/rest_api_4_integration_0-pod2 -> load_balancer/rest-api-4-integbcc77100:
+  vpc_link/rest_api_4_integration_0-eks_cluster-0:
+  vpc_link/rest_api_4_integration_0-eks_cluster-0 -> load_balancer/rest-api-4-integ6897f0b9:
 
   kubernetes:service_account:eks_cluster-0/aws-load-balancer-controller:
   kubernetes:service_account:eks_cluster-0/aws-load-balancer-controller -> eks_cluster/eks_cluster-0:
@@ -115,9 +115,9 @@ resources:
   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_integration:rest_api_4/rest_api_4_integration_0 -> aws:api_resource:rest_api_4/api_resource-0:
   aws:api_integration:rest_api_4/rest_api_4_integration_0 -> rest_api/rest_api_4:
-  aws:api_integration:rest_api_4/rest_api_4_integration_0 -> vpc_link/rest_api_4_integration_0-pod2:
+  aws:api_integration:rest_api_4/rest_api_4_integration_0 -> vpc_link/rest_api_4_integration_0-eks_cluster-0:
 
-  target_group/rest-api-4-integbcc77100:
+  target_group/rest-api-4-integ6897f0b9:
 
   kubernetes:helm_chart:eks_cluster-0/aws-load-balancer-controller:
   kubernetes:helm_chart:eks_cluster-0/aws-load-balancer-controller -> eks_cluster/eks_cluster-0:
@@ -155,11 +155,11 @@ resources:
   aws:api_deployment:rest_api_4/api_deployment-0 -> aws:api_method:rest_api_4/rest_api_4_integration_0_method:
   aws:api_deployment:rest_api_4/api_deployment-0 -> rest_api/rest_api_4:
 
-  kubernetes:target_group_binding/restapi4integration0-pod2:
-  kubernetes:target_group_binding/restapi4integration0-pod2 -> eks_cluster/eks_cluster-0:
-  kubernetes:target_group_binding/restapi4integration0-pod2 -> target_group/rest-api-4-integbcc77100:
-  kubernetes:target_group_binding/restapi4integration0-pod2 -> kubernetes:helm_chart:eks_cluster-0/aws-load-balancer-controller:
-  kubernetes:target_group_binding/restapi4integration0-pod2 -> kubernetes:service/restapi4integration0-pod2:
+  kubernetes:target_group_binding/restapi4integration0-ekscluster-0:
+  kubernetes:target_group_binding/restapi4integration0-ekscluster-0 -> eks_cluster/eks_cluster-0:
+  kubernetes:target_group_binding/restapi4integration0-ekscluster-0 -> target_group/rest-api-4-integ6897f0b9:
+  kubernetes:target_group_binding/restapi4integration0-ekscluster-0 -> kubernetes:helm_chart:eks_cluster-0/aws-load-balancer-controller:
+  kubernetes:target_group_binding/restapi4integration0-ekscluster-0 -> kubernetes:service/restapi4integration0-pod2:
 
   kubernetes:manifest/fluent-bit:
   kubernetes:manifest/fluent-bit -> eks_cluster/eks_cluster-0:
@@ -191,9 +191,9 @@ resources:
   route_table_association/subnet-0-subnet-0-route_table -> route_table/subnet-0-route_table:
   route_table_association/subnet-0-subnet-0-route_table -> aws:subnet:vpc-0/subnet-0:
 
-  load_balancer_listener/rest_api_4_integration_0-pod2:
-  load_balancer_listener/rest_api_4_integration_0-pod2 -> load_balancer/rest-api-4-integbcc77100:
-  load_balancer_listener/rest_api_4_integration_0-pod2 -> target_group/rest-api-4-integbcc77100:
+  load_balancer_listener/rest_api_4_integration_0-eks_cluster-0:
+  load_balancer_listener/rest_api_4_integration_0-eks_cluster-0 -> load_balancer/rest-api-4-integ6897f0b9:
+  load_balancer_listener/rest_api_4_integration_0-eks_cluster-0 -> target_group/rest-api-4-integ6897f0b9:
 
   iam_role_policy_attachment/aws-load-balancer-controller-iam_policy-0:
   iam_role_policy_attachment/aws-load-balancer-controller-iam_policy-0 -> iam_policy/iam_policy-0:
diff --git a/pkg/knowledge_base2/properties/bool_property_test.go b/pkg/knowledge_base2/properties/bool_property_test.go
index 29a2f143..a903a8c0 100644
--- a/pkg/knowledge_base2/properties/bool_property_test.go
+++ b/pkg/knowledge_base2/properties/bool_property_test.go
@@ -4,8 +4,10 @@ import (
 	"testing"
 
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
 	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 func Test_SetBoolProperty(t *testing.T) {
@@ -268,10 +270,12 @@ func Test_BoolPropertyType(t *testing.T) {
 
 func Test_BoolPropertyValidate(t *testing.T) {
 	tests := []struct {
-		name     string
-		property *BoolProperty
-		value    any
-		wantErr  bool
+		name          string
+		property      *BoolProperty
+		testResources []*construct.Resource
+		mockKBCalls   []mock.Call
+		value         any
+		wantErr       bool
 	}{
 		{
 			name: "bool property",
@@ -298,7 +302,19 @@ func Test_BoolPropertyValidate(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			assert := assert.New(t)
 			resource := &construct.Resource{}
-			err := tt.property.Validate(resource, tt.value)
+			graph := construct.NewGraph()
+			for _, r := range tt.testResources {
+				graph.AddVertex(r)
+			}
+			mockKB := &enginetesting.MockKB{}
+			for _, call := range tt.mockKBCalls {
+				mockKB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			ctx := knowledgebase.DynamicValueContext{
+				Graph:         graph,
+				KnowledgeBase: mockKB,
+			}
+			err := tt.property.Validate(resource, tt.value, ctx)
 			assert.Equal(tt.wantErr, err != nil)
 		})
 	}
diff --git a/pkg/knowledge_base2/properties/float_property_test.go b/pkg/knowledge_base2/properties/float_property_test.go
index ba876fda..82a0a451 100644
--- a/pkg/knowledge_base2/properties/float_property_test.go
+++ b/pkg/knowledge_base2/properties/float_property_test.go
@@ -4,8 +4,11 @@ import (
 	"testing"
 
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
+	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	knowledgebase2 "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 func Test_SetFloatProperty(t *testing.T) {
@@ -196,10 +199,12 @@ func Test_ParseFloatValue(t *testing.T) {
 
 func Test_FloatProperty_Validate(t *testing.T) {
 	tests := []struct {
-		name     string
-		property *FloatProperty
-		value    any
-		wantErr  bool
+		name          string
+		property      *FloatProperty
+		testResources []*construct.Resource
+		mockKBCalls   []mock.Call
+		value         any
+		wantErr       bool
 	}{
 		{
 			name: "float value",
@@ -225,7 +230,19 @@ func Test_FloatProperty_Validate(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			assert := assert.New(t)
 			resource := &construct.Resource{}
-			err := test.property.Validate(resource, test.value)
+			graph := construct.NewGraph()
+			for _, r := range test.testResources {
+				graph.AddVertex(r)
+			}
+			mockKB := &enginetesting.MockKB{}
+			for _, call := range test.mockKBCalls {
+				mockKB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			ctx := knowledgebase.DynamicValueContext{
+				Graph:         graph,
+				KnowledgeBase: mockKB,
+			}
+			err := test.property.Validate(resource, test.value, ctx)
 			if test.wantErr {
 				assert.Error(err)
 				return
diff --git a/pkg/knowledge_base2/properties/int_property_test.go b/pkg/knowledge_base2/properties/int_property_test.go
index 4235e9be..5b1f5f7d 100644
--- a/pkg/knowledge_base2/properties/int_property_test.go
+++ b/pkg/knowledge_base2/properties/int_property_test.go
@@ -4,8 +4,11 @@ import (
 	"testing"
 
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
+	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	knowledgebase2 "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 func Test_SetIntPropertyProperty(t *testing.T) {
@@ -188,10 +191,12 @@ func Test_IntProperty_Validate(t *testing.T) {
 	upperBound := 10
 	lowerBound := 0
 	tests := []struct {
-		name     string
-		property *IntProperty
-		value    any
-		wantErr  bool
+		name          string
+		property      *IntProperty
+		testResources []*construct.Resource
+		mockKBCalls   []mock.Call
+		value         any
+		wantErr       bool
 	}{
 		{
 			name: "int property",
@@ -252,7 +257,19 @@ func Test_IntProperty_Validate(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			assert := assert.New(t)
 			resource := &construct.Resource{}
-			err := test.property.Validate(resource, test.value)
+			graph := construct.NewGraph()
+			for _, r := range test.testResources {
+				graph.AddVertex(r)
+			}
+			mockKB := &enginetesting.MockKB{}
+			for _, call := range test.mockKBCalls {
+				mockKB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			ctx := knowledgebase.DynamicValueContext{
+				Graph:         graph,
+				KnowledgeBase: mockKB,
+			}
+			err := test.property.Validate(resource, test.value, ctx)
 			if test.wantErr {
 				assert.Error(err)
 				return
diff --git a/pkg/knowledge_base2/properties/list_property_test.go b/pkg/knowledge_base2/properties/list_property_test.go
index 9aadcc7a..4833f529 100644
--- a/pkg/knowledge_base2/properties/list_property_test.go
+++ b/pkg/knowledge_base2/properties/list_property_test.go
@@ -4,8 +4,10 @@ import (
 	"testing"
 
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
 	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 func Test_ListProperty_Set(t *testing.T) {
@@ -285,10 +287,12 @@ func Test_ListProperty_Validate(t *testing.T) {
 	minLength := 1
 	maxLength := 2
 	tests := []struct {
-		name     string
-		property *ListProperty
-		value    any
-		wantErr  bool
+		name          string
+		property      *ListProperty
+		testResources []*construct.Resource
+		mockKBCalls   []mock.Call
+		value         any
+		wantErr       bool
 	}{
 		{
 			name: "list property",
@@ -346,7 +350,19 @@ func Test_ListProperty_Validate(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			assert := assert.New(t)
 			resource := &construct.Resource{}
-			err := tt.property.Validate(resource, tt.value)
+			graph := construct.NewGraph()
+			for _, r := range tt.testResources {
+				graph.AddVertex(r)
+			}
+			mockKB := &enginetesting.MockKB{}
+			for _, call := range tt.mockKBCalls {
+				mockKB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			ctx := knowledgebase.DynamicValueContext{
+				Graph:         graph,
+				KnowledgeBase: mockKB,
+			}
+			err := tt.property.Validate(resource, tt.value, ctx)
 			if tt.wantErr {
 				assert.Error(err)
 				return
diff --git a/pkg/knowledge_base2/properties/map_property_test.go b/pkg/knowledge_base2/properties/map_property_test.go
index 9fa59d32..744636d2 100644
--- a/pkg/knowledge_base2/properties/map_property_test.go
+++ b/pkg/knowledge_base2/properties/map_property_test.go
@@ -4,8 +4,11 @@ import (
 	"testing"
 
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
+	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	knowledgebase2 "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 func Test_SetMapProperty(t *testing.T) {
@@ -221,10 +224,12 @@ func Test_MapProperty_Type(t *testing.T) {
 
 func Test_MapProperty_Validate(t *testing.T) {
 	tests := []struct {
-		name     string
-		property *MapProperty
-		value    any
-		wantErr  bool
+		name          string
+		property      *MapProperty
+		testResources []*construct.Resource
+		mockKBCalls   []mock.Call
+		value         any
+		wantErr       bool
 	}{
 		{
 			name: "valid map property",
@@ -273,7 +278,19 @@ func Test_MapProperty_Validate(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			assert := assert.New(t)
 			resource := &construct.Resource{}
-			err := test.property.Validate(resource, test.value)
+			graph := construct.NewGraph()
+			for _, r := range test.testResources {
+				graph.AddVertex(r)
+			}
+			mockKB := &enginetesting.MockKB{}
+			for _, call := range test.mockKBCalls {
+				mockKB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			ctx := knowledgebase.DynamicValueContext{
+				Graph:         graph,
+				KnowledgeBase: mockKB,
+			}
+			err := test.property.Validate(resource, test.value, ctx)
 			if test.wantErr {
 				assert.Error(err)
 				return
diff --git a/pkg/knowledge_base2/properties/property_test.go b/pkg/knowledge_base2/properties/property_test.go
new file mode 100644
index 00000000..d737d76e
--- /dev/null
+++ b/pkg/knowledge_base2/properties/property_test.go
@@ -0,0 +1,173 @@
+package properties
+
+import (
+	"testing"
+
+	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
+	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func Test_ValidatePropertyRef(t *testing.T) {
+	tests := []struct {
+		name          string
+		propertyType  string
+		ref           construct.PropertyRef
+		testResources []*construct.Resource
+		mockKBCalls   []mock.Call
+		expect        any
+		wantErr       bool
+	}{
+		{
+			name:         "string property ref, existing resource with value",
+			propertyType: "string",
+			ref: construct.PropertyRef{
+				Resource: construct.ResourceId{Name: "test"},
+				Property: "test",
+			},
+			testResources: []*construct.Resource{
+				{
+					ID: construct.ResourceId{Name: "test"},
+					Properties: map[string]any{
+						"test": "testval",
+					},
+				},
+			},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetResourceTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Name: "test"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.ResourceTemplate{
+							Properties: knowledgebase.Properties{
+								"test": &StringProperty{PropertyDetails: knowledgebase.PropertyDetails{Path: "test", Name: "test"}},
+							},
+						}, nil,
+					},
+				},
+			},
+			expect: "testval",
+		},
+		{
+			name:         "string property is deploy time returns nil",
+			propertyType: "string",
+			ref: construct.PropertyRef{
+				Resource: construct.ResourceId{Name: "test"},
+				Property: "test",
+			},
+			testResources: []*construct.Resource{
+				{
+					ID: construct.ResourceId{Name: "test"},
+					Properties: map[string]any{
+						"test": "testval",
+					},
+				},
+			},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetResourceTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Name: "test"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.ResourceTemplate{
+							Properties: knowledgebase.Properties{
+								"test": &StringProperty{PropertyDetails: knowledgebase.PropertyDetails{
+									Path:       "test",
+									Name:       "test",
+									DeployTime: true,
+								}},
+							},
+						}, nil,
+					},
+				},
+			},
+		},
+		{
+			name:         "no resource, throws err",
+			propertyType: "string",
+			ref: construct.PropertyRef{
+				Resource: construct.ResourceId{Name: "test"},
+				Property: "test",
+			},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetResourceTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Name: "test"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.ResourceTemplate{
+							Properties: knowledgebase.Properties{
+								"test": &StringProperty{PropertyDetails: knowledgebase.PropertyDetails{
+									Path:       "test",
+									Name:       "test",
+									DeployTime: true,
+								}},
+							},
+						}, nil,
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name:         "no property throws error",
+			propertyType: "string",
+			ref: construct.PropertyRef{
+				Resource: construct.ResourceId{Name: "test"},
+				Property: "test",
+			},
+			testResources: []*construct.Resource{
+				{
+					ID: construct.ResourceId{Name: "test"},
+					Properties: map[string]any{
+						"test": "testval",
+					},
+				},
+			},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetResourceTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Name: "test"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.ResourceTemplate{
+							Properties: knowledgebase.Properties{},
+						}, nil,
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			graph := construct.NewGraph()
+			for _, r := range tt.testResources {
+				graph.AddVertex(r)
+			}
+			mockKB := &enginetesting.MockKB{}
+			for _, call := range tt.mockKBCalls {
+				mockKB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			ctx := knowledgebase.DynamicValueContext{
+				Graph:         graph,
+				KnowledgeBase: mockKB,
+			}
+			val, err := ValidatePropertyRef(tt.ref, tt.propertyType, ctx)
+			if tt.wantErr {
+				assert.Error(err)
+			} else {
+				assert.NoError(err)
+				assert.Equal(tt.expect, val)
+			}
+		})
+	}
+}
diff --git a/pkg/knowledge_base2/properties/set_property_test.go b/pkg/knowledge_base2/properties/set_property_test.go
index 70913833..61fbc91c 100644
--- a/pkg/knowledge_base2/properties/set_property_test.go
+++ b/pkg/knowledge_base2/properties/set_property_test.go
@@ -5,9 +5,11 @@ import (
 	"testing"
 
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
 	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	"github.com/klothoplatform/klotho/pkg/set"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 func Test_SetSetProperty(t *testing.T) {
@@ -465,10 +467,12 @@ func Test_SetValidate(t *testing.T) {
 	minLength := 1
 	maxLength := 2
 	tests := []struct {
-		name     string
-		property *SetProperty
-		value    any
-		expected bool
+		name          string
+		property      *SetProperty
+		testResources []*construct.Resource
+		mockKBCalls   []mock.Call
+		value         any
+		expected      bool
 	}{
 		{
 			name: "set property",
@@ -570,7 +574,19 @@ func Test_SetValidate(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			assert := assert.New(t)
 			resource := &construct.Resource{}
-			actual := tt.property.Validate(resource, tt.value)
+			graph := construct.NewGraph()
+			for _, r := range tt.testResources {
+				graph.AddVertex(r)
+			}
+			mockKB := &enginetesting.MockKB{}
+			for _, call := range tt.mockKBCalls {
+				mockKB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			ctx := knowledgebase.DynamicValueContext{
+				Graph:         graph,
+				KnowledgeBase: mockKB,
+			}
+			actual := tt.property.Validate(resource, tt.value, ctx)
 			if tt.expected {
 				assert.NoError(actual)
 			} else {
diff --git a/pkg/knowledge_base2/properties/string_property_test.go b/pkg/knowledge_base2/properties/string_property_test.go
index 0d130034..c6add3b3 100644
--- a/pkg/knowledge_base2/properties/string_property_test.go
+++ b/pkg/knowledge_base2/properties/string_property_test.go
@@ -4,8 +4,10 @@ import (
 	"testing"
 
 	construct "github.com/klothoplatform/klotho/pkg/construct2"
+	"github.com/klothoplatform/klotho/pkg/engine2/enginetesting"
 	knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 func Test_SetStringProperty(t *testing.T) {
@@ -361,6 +363,8 @@ func Test_StringValidate(t *testing.T) {
 		name             string
 		property         *StringProperty
 		sanitizeTemplate string
+		testResources    []*construct.Resource
+		mockKBCalls      []mock.Call
 		value            any
 		expected         bool
 	}{
@@ -396,6 +400,37 @@ func Test_StringValidate(t *testing.T) {
 			value:            "TEST",
 			expected:         true,
 		},
+		{
+			name: "Valid property ref as string",
+			property: &StringProperty{
+				PropertyDetails: knowledgebase.PropertyDetails{
+					Path: "test",
+				},
+			},
+			testResources: []*construct.Resource{
+				{
+					ID:         construct.ResourceId{Name: "blah"},
+					Properties: construct.Properties{"test": "blah"},
+				},
+			},
+			value: construct.PropertyRef{Resource: construct.ResourceId{Name: "blah"}, Property: "test"},
+			mockKBCalls: []mock.Call{
+				{
+					Method: "GetResourceTemplate",
+					Arguments: mock.Arguments{
+						construct.ResourceId{Name: "blah"},
+					},
+					ReturnArguments: mock.Arguments{
+						&knowledgebase.ResourceTemplate{
+							Properties: knowledgebase.Properties{
+								"test": &StringProperty{PropertyDetails: knowledgebase.PropertyDetails{Path: "test", Name: "test"}},
+							},
+						}, nil,
+					},
+				},
+			},
+			expected: true,
+		},
 		{
 			name: "string not in allowed values",
 			property: &StringProperty{
@@ -440,7 +475,19 @@ func Test_StringValidate(t *testing.T) {
 				tt.property.SanitizeTmpl = tmpl
 			}
 			resource := &construct.Resource{}
-			actual := tt.property.Validate(resource, tt.value)
+			graph := construct.NewGraph()
+			for _, r := range tt.testResources {
+				graph.AddVertex(r)
+			}
+			mockKB := &enginetesting.MockKB{}
+			for _, call := range tt.mockKBCalls {
+				mockKB.On(call.Method, call.Arguments...).Return(call.ReturnArguments...)
+			}
+			ctx := knowledgebase.DynamicValueContext{
+				Graph:         graph,
+				KnowledgeBase: mockKB,
+			}
+			actual := tt.property.Validate(resource, tt.value, ctx)
 			if tt.expected {
 				assert.NoError(actual)
 			} else {