From df9526be9f598736614c17b4329268c3963a7525 Mon Sep 17 00:00:00 2001 From: Jordan Singer Date: Tue, 9 Apr 2024 14:04:32 -0500 Subject: [PATCH 1/6] allow imported resources to be path expanded and more state reading --- .github/workflows/govulncheck.yaml | 2 +- .github/workflows/lint.yaml | 2 +- .github/workflows/prettier.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/run-integ-tests.yaml | 2 +- .github/workflows/test.yaml | 2 +- cmd/kb/main.go | 2 +- pkg/engine/edge_targets.go | 2 +- pkg/engine/operational_eval/graph.go | 12 +- .../operational_eval/vertex_path_expand.go | 7 +- .../operational_eval/vertex_property.go | 6 - .../operational_rule/operational_action.go | 2 +- .../path_selection/candidate_validity.go | 53 ++++ .../path_selection/candidate_validity_test.go | 80 +++++ pkg/engine/path_selection/path_expansion.go | 25 +- pkg/engine/path_selection/path_selection.go | 7 +- .../path_selection/path_selection_test.go | 1 + pkg/engine/path_selection/paths.go | 26 +- pkg/infra/cli.go | 4 +- .../aws/auto_scaling_group/factory.ts | 4 + .../aws/availability_zone/factory.ts | 8 +- .../aws/ec2_launch_template/factory.ts | 6 +- .../templates/aws/security_group/factory.ts | 11 + .../resource_template_mock_test.go | 299 ++++++++++++++++++ .../state_reader/state_converter/pulumi.go | 7 +- .../state_converter/pulumi_test.go | 9 +- .../state_converter/state_converter.go | 21 -- .../state_converter/state_converter_test.go | 41 --- pkg/infra/state_reader/state_reader.go | 295 ++++++++++++++++- .../state_reader/state_reader_mock_test.go | 110 +++++++ pkg/infra/state_reader/state_reader_test.go | 119 +++++-- .../mappings/pulumi/auto_scaling_group.yaml | 7 + .../mappings/pulumi/ec2_launch_template.yaml | 8 + .../mappings/pulumi/lambda_function.yaml | 3 +- .../mappings/pulumi/security_group.yaml | 7 + pkg/knowledgebase/path_satisfaction.go | 10 +- pkg/knowledgebase/resource_template.go | 1 + .../security_group-ec2_launch_template.yaml | 3 + .../security_group_rule-security_group.yaml | 5 +- .../aws/edges/subnet-security_group_rule.yaml | 30 ++ .../aws/edges/target_group-ecs_service.yaml | 17 +- .../aws/resources/lambda_function.yaml | 4 + .../aws/resources/security_group.yaml | 15 + .../aws/resources/security_group_rule.yaml | 6 + pkg/templates/aws/resources/subnet.yaml | 4 +- 45 files changed, 1125 insertions(+), 164 deletions(-) create mode 100644 pkg/infra/state_reader/resource_template_mock_test.go delete mode 100644 pkg/infra/state_reader/state_converter/state_converter_test.go create mode 100644 pkg/infra/state_reader/state_reader_mock_test.go create mode 100644 pkg/infra/state_reader/state_template/mappings/pulumi/auto_scaling_group.yaml create mode 100644 pkg/infra/state_reader/state_template/mappings/pulumi/ec2_launch_template.yaml create mode 100644 pkg/infra/state_reader/state_template/mappings/pulumi/security_group.yaml create mode 100644 pkg/templates/aws/edges/security_group-ec2_launch_template.yaml create mode 100644 pkg/templates/aws/edges/subnet-security_group_rule.yaml diff --git a/.github/workflows/govulncheck.yaml b/.github/workflows/govulncheck.yaml index a7ff932cf..1e90f6362 100644 --- a/.github/workflows/govulncheck.yaml +++ b/.github/workflows/govulncheck.yaml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: '1.22.1' + go-version: '1.22.2' - uses: actions/cache@v2 with: path: | diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 5bbda774c..1eeb0c403 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: '1.21.5' + go-version: '1.22.2' - uses: actions/checkout@v3 - uses: actions/cache@v2 with: diff --git a/.github/workflows/prettier.yaml b/.github/workflows/prettier.yaml index 23e9660ea..9e0f76790 100644 --- a/.github/workflows/prettier.yaml +++ b/.github/workflows/prettier.yaml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: '1.21.5' + go-version: '1.22.2' - uses: actions/checkout@v3 - name: List files to check run: | diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 95a120a7d..01282a25d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,7 +25,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: '1.21.5' + go-version: '1.22.2' - name: Setup Zig uses: goto-bus-stop/setup-zig@v1 diff --git a/.github/workflows/run-integ-tests.yaml b/.github/workflows/run-integ-tests.yaml index 5bbbf097d..41d1d62c3 100644 --- a/.github/workflows/run-integ-tests.yaml +++ b/.github/workflows/run-integ-tests.yaml @@ -97,7 +97,7 @@ jobs: uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.21.5' + go-version: '1.22.2' - name: pre-build if: ${{ inputs.pre-build-script }} run: ${{ inputs.pre-build-script }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 46e7877ab..339bf3453 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: '1.21.5' + go-version: '1.22.2' - uses: actions/checkout@v3 - uses: actions/cache@v2 with: diff --git a/cmd/kb/main.go b/cmd/kb/main.go index f844f4f85..38ff203fe 100644 --- a/cmd/kb/main.go +++ b/cmd/kb/main.go @@ -73,7 +73,7 @@ func (args Args) Run(ctx *kong.Context) error { return fmt.Errorf("could not parse target: %w", err) } edge.Target.Name = "target" - g, err := path_selection.BuildPathSelectionGraph(edge, kb, args.Classification) + g, err := path_selection.BuildPathSelectionGraph(edge, kb, args.Classification, true) if err != nil { return err } diff --git a/pkg/engine/edge_targets.go b/pkg/engine/edge_targets.go index aef99019b..040d55d9b 100644 --- a/pkg/engine/edge_targets.go +++ b/pkg/engine/edge_targets.go @@ -102,7 +102,7 @@ func (e *Engine) EdgeCanBeExpanded(ctx *solutionContext, source construct.Resour construct.SimpleEdge{ Source: tempSource, Target: tempTarget, - }, ctx.KnowledgeBase(), classification) + }, ctx.KnowledgeBase(), classification, false) if err != nil { return false, cacheable, err } diff --git a/pkg/engine/operational_eval/graph.go b/pkg/engine/operational_eval/graph.go index 2f482de7e..b6660cd76 100644 --- a/pkg/engine/operational_eval/graph.go +++ b/pkg/engine/operational_eval/graph.go @@ -81,6 +81,16 @@ func (eval *Evaluator) AddEdges(es ...construct.Edge) error { func (eval *Evaluator) pathVertices(source, target construct.ResourceId) (graphChanges, error) { changes := newChanges() + src, err := eval.Solution.RawView().Vertex(source) + if err != nil { + return changes, fmt.Errorf("failed to get source vertex for %s: %w", source, err) + } + dst, err := eval.Solution.RawView().Vertex(target) + if err != nil { + return changes, fmt.Errorf("failed to get target vertex for %s: %w", target, err) + } + requireFullBuild := dst.Imported || src.Imported + generateAndAddVertex := func( edge construct.SimpleEdge, kb knowledgebase.TemplateKB, @@ -101,7 +111,7 @@ func (eval *Evaluator) pathVertices(source, target construct.ResourceId) (graphC var tempGraph construct.Graph if buildTempGraph { var err error - tempGraph, err = path_selection.BuildPathSelectionGraph(edge, kb, satisfication.Classification) + tempGraph, err = path_selection.BuildPathSelectionGraph(edge, kb, satisfication.Classification, !requireFullBuild) if err != nil { return fmt.Errorf("could not build temp graph for %s: %w", edge, err) } diff --git a/pkg/engine/operational_eval/vertex_path_expand.go b/pkg/engine/operational_eval/vertex_path_expand.go index e6baa1f1b..305f31329 100644 --- a/pkg/engine/operational_eval/vertex_path_expand.go +++ b/pkg/engine/operational_eval/vertex_path_expand.go @@ -202,7 +202,7 @@ func (v *pathExpandVertex) addDepsFromEdge( return err } - se := construct.SimpleEdge{Source: edge.Source, Target: edge.Target} + se := construct.Edge{Source: edge.Source, Target: edge.Target} se.Source.Name = "" se.Target.Name = "" @@ -238,7 +238,7 @@ func (v *pathExpandVertex) addDepsFromEdge( for i, rule := range tmpl.OperationalRules { for j, cfg := range rule.ConfigurationRules { var err error - data := knowledgebase.DynamicValueData{Edge: &edge} + data := knowledgebase.DynamicValueData{Edge: &se} data.Resource, err = knowledgebase.ExecuteDecodeAsResourceId(dyn, cfg.Resource, data) // We ignore the error because it just means that we cant resolve the resource yet @@ -358,6 +358,7 @@ func (runner *pathExpandVertexRunner) getExpansionsToRun(v *pathExpandVertex) ([ if err != nil { errs = errors.Join(errs, err) } + requireFullBuild := sourceRes.Imported || targetRes.Imported result := make([]path_selection.ExpansionInput, len(expansions)) for i, expansion := range expansions { @@ -369,7 +370,7 @@ func (runner *pathExpandVertexRunner) getExpansionsToRun(v *pathExpandVertex) ([ } if expansion.SatisfactionEdge.Source != edge.Source || expansion.SatisfactionEdge.Target != edge.Target { simple := construct.SimpleEdge{Source: expansion.SatisfactionEdge.Source.ID, Target: expansion.SatisfactionEdge.Target.ID} - tempGraph, err := path_selection.BuildPathSelectionGraph(simple, eval.Solution.KnowledgeBase(), expansion.Classification) + tempGraph, err := path_selection.BuildPathSelectionGraph(simple, eval.Solution.KnowledgeBase(), expansion.Classification, requireFullBuild) if err != nil { errs = errors.Join(errs, fmt.Errorf("error getting expansions to run. could not build path selection graph: %w", err)) continue diff --git a/pkg/engine/operational_eval/vertex_property.go b/pkg/engine/operational_eval/vertex_property.go index 3f35a0401..e149702de 100644 --- a/pkg/engine/operational_eval/vertex_property.go +++ b/pkg/engine/operational_eval/vertex_property.go @@ -163,12 +163,6 @@ func (v *propertyVertex) Evaluate(eval *Evaluator) error { Data: dynData, } - // we know we cannot change properties of imported resources only users through constraints - // we still want to be able to update ids in case they are setting the property of a namespaced resource - // so we just conditionally run the edge operational rules - // - // we still need to run the resource operational rules though, - // to make sure dependencies exist where properties have operational rules set if err := v.evaluateResourceOperational(&opCtx); err != nil { return err } diff --git a/pkg/engine/operational_rule/operational_action.go b/pkg/engine/operational_rule/operational_action.go index c2524953a..cacf1baa1 100644 --- a/pkg/engine/operational_rule/operational_action.go +++ b/pkg/engine/operational_rule/operational_action.go @@ -91,7 +91,7 @@ func (action *operationalResourceAction) createUniqueResources(resource *constru return err } } - if len(uids) == 1 { + if len(uids) == 1 && uids[0].Matches(resource.ID) { res, err := action.ruleCtx.Solution.RawView().Vertex(id) if err != nil { return err diff --git a/pkg/engine/path_selection/candidate_validity.go b/pkg/engine/path_selection/candidate_validity.go index 1fefd447b..1d0b8c555 100644 --- a/pkg/engine/path_selection/candidate_validity.go +++ b/pkg/engine/path_selection/candidate_validity.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/dominikbraun/graph" "github.com/klothoplatform/klotho/pkg/collectionutil" construct "github.com/klothoplatform/klotho/pkg/construct" "github.com/klothoplatform/klotho/pkg/engine/solution_context" @@ -20,6 +21,58 @@ type ( } ) +// checkDoesNotModifyImportedResource checks if there is an imported resource that would be modified due to the edge +// If there is an edge rule modifying the resource then we consider the edge to be invalid +func checkDoesNotModifyImportedResource( + source, target construct.ResourceId, + ctx solution_context.SolutionContext, + et *knowledgebase.EdgeTemplate, +) (bool, error) { + // see if the source resource exists in the graph + sourceResource, srcErr := ctx.RawView().Vertex(source) + // see if the target resource exists in the graph + targetResource, trgtErr := ctx.RawView().Vertex(target) + if errors.Is(srcErr, graph.ErrVertexNotFound) && errors.Is(trgtErr, graph.ErrVertexNotFound) { + return true, nil + } + + if et == nil { + et = ctx.KnowledgeBase().GetEdgeTemplate(source, target) + } + + checkRules := func(resources construct.ResourceList) (bool, error) { + if len(resources) == 0 { + return true, nil + } + for _, rule := range et.OperationalRules { + for _, config := range rule.ConfigurationRules { + dynamicCtx := solution_context.DynamicCtx(ctx) + id := construct.ResourceId{} + // we ignore the error since phantom resources will cause errors in the decoding of templates + _ = dynamicCtx.ExecuteDecode(config.Resource, knowledgebase.DynamicValueData{ + Edge: &construct.Edge{ + Source: source, + Target: target, + }}, &id) + + if resources.MatchesAny(id) { + return false, nil + } + } + } + return true, nil + } + + importedResources := construct.ResourceList{} + if sourceResource != nil && sourceResource.Imported { + importedResources = append(importedResources, source) + } + if targetResource != nil && targetResource.Imported { + importedResources = append(importedResources, target) + } + return checkRules(importedResources) +} + // checkCandidatesValidity checks if the candidate is valid based on the validity of its own path satisfaction rules and namespace func checkCandidatesValidity( ctx solution_context.SolutionContext, diff --git a/pkg/engine/path_selection/candidate_validity_test.go b/pkg/engine/path_selection/candidate_validity_test.go index bade04dc3..3377d19b9 100644 --- a/pkg/engine/path_selection/candidate_validity_test.go +++ b/pkg/engine/path_selection/candidate_validity_test.go @@ -3,6 +3,7 @@ package path_selection import ( "testing" + "github.com/klothoplatform/klotho/pkg/construct" "github.com/klothoplatform/klotho/pkg/construct/graphtest" "github.com/klothoplatform/klotho/pkg/engine/enginetesting" knowledgebase "github.com/klothoplatform/klotho/pkg/knowledgebase" @@ -10,6 +11,85 @@ import ( "github.com/stretchr/testify/require" ) +func Test_checkDoesNotModifyImportedResource(t *testing.T) { + tests := []struct { + name string + source *construct.Resource + target *construct.Resource + et *knowledgebase.EdgeTemplate + mocks func(*enginetesting.MockKB) + want bool + }{ + { + name: "no imported resource returns true", + source: &construct.Resource{ID: graphtest.ParseId(t, "p:a:a")}, + target: &construct.Resource{ID: graphtest.ParseId(t, "p:b:b")}, + et: &knowledgebase.EdgeTemplate{}, + want: true, + }, + { + name: "imported resource with no modifications returns true", + source: &construct.Resource{ID: graphtest.ParseId(t, "p:a:a"), Imported: true}, + target: &construct.Resource{ID: graphtest.ParseId(t, "p:b:b")}, + et: &knowledgebase.EdgeTemplate{ + OperationalRules: []knowledgebase.OperationalRule{ + { + ConfigurationRules: []knowledgebase.ConfigurationRule{ + { + Resource: "{{ .Target }}", + }, + }, + }, + }, + }, + want: true, + }, + { + name: "imported resource with modifications returns false", + source: &construct.Resource{ID: graphtest.ParseId(t, "p:a:a"), Imported: true}, + target: &construct.Resource{ID: graphtest.ParseId(t, "p:b:b")}, + et: &knowledgebase.EdgeTemplate{ + OperationalRules: []knowledgebase.OperationalRule{ + { + ConfigurationRules: []knowledgebase.ConfigurationRule{ + { + Resource: "{{ .Source }}", + }, + }, + }, + }, + }, + }, + { + name: "gets edge template if not provided", + source: &construct.Resource{ID: graphtest.ParseId(t, "p:a:a"), Imported: true}, + target: &construct.Resource{ID: graphtest.ParseId(t, "p:b:b")}, + mocks: func(kb *enginetesting.MockKB) { + kb.On("GetEdgeTemplate", graphtest.ParseId(t, "p:a:a"), graphtest.ParseId(t, "p:b:b")).Return(&knowledgebase.EdgeTemplate{}) + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sol := enginetesting.NewTestSolution() + sol.KB.On("GetResourceTemplate", mock.Anything).Return(&knowledgebase.ResourceTemplate{}, nil) + if tt.mocks != nil { + tt.mocks(&sol.KB) + } + err := sol.RawView().AddVertex(tt.source) + require.NoError(t, err) + err = sol.RawView().AddVertex(tt.target) + require.NoError(t, err) + + got, err := checkDoesNotModifyImportedResource(tt.source.ID, tt.target.ID, sol, tt.et) + require.NoError(t, err) + require.Equal(t, tt.want, got) + sol.KB.AssertExpectations(t) + }) + } +} + func Test_checkAsTargetValidity(t *testing.T) { type testResource struct { id string diff --git a/pkg/engine/path_selection/path_expansion.go b/pkg/engine/path_selection/path_expansion.go index 2193eaacb..7a50e4fe4 100644 --- a/pkg/engine/path_selection/path_expansion.go +++ b/pkg/engine/path_selection/path_expansion.go @@ -308,8 +308,18 @@ func expandPath( path construct.Path, resultGraph construct.Graph, ) error { + if len(path) == 2 { - return nil + doesNotModifyImport, err := checkDoesNotModifyImportedResource(input.SatisfactionEdge.Source.ID, + input.SatisfactionEdge.Target.ID, ctx, nil) + if err != nil { + return err + } + if !doesNotModifyImport { + // Because the direct edge will cause modifications to an imported resource, we need to remove the direct edge + return input.TempGraph.RemoveEdge(input.SatisfactionEdge.Source.ID, + input.SatisfactionEdge.Target.ID) + } } zap.S().Debugf("Resolving path %s", path) @@ -434,10 +444,17 @@ func expandPath( if !tmpl.Unique.CanAdd(edges, source.id, target.id) { return } - + doesNotModifyImport, err := checkDoesNotModifyImportedResource(source.id, target.id, ctx, tmpl) + if err != nil { + errs = errors.Join(errs, err) + return + } + if !doesNotModifyImport { + return + } // if the edge doesnt exist in the actual graph and there is any uniqueness constraint, // then we need to check uniqueness validity - _, err := ctx.RawView().Edge(source.id, target.id) + _, err = ctx.RawView().Edge(source.id, target.id) if errors.Is(err, graph.ErrEdgeNotFound) { if tmpl.Unique.Source || tmpl.Unique.Target { valid, err := checkUniquenessValidity(ctx, source.id, target.id) @@ -527,7 +544,7 @@ func connectThroughNamespace(src, target *construct.Resource, ctx solution_conte continue } // if we have a namespace resource that is not the same as the target namespace resource - tg, err := BuildPathSelectionGraph(construct.SimpleEdge{Source: res, Target: target.ID}, kb, "") + tg, err := BuildPathSelectionGraph(construct.SimpleEdge{Source: res, Target: target.ID}, kb, "", true) if err != nil { continue } diff --git a/pkg/engine/path_selection/path_selection.go b/pkg/engine/path_selection/path_selection.go index 9be9a97b5..084267bf4 100644 --- a/pkg/engine/path_selection/path_selection.go +++ b/pkg/engine/path_selection/path_selection.go @@ -3,6 +3,7 @@ package path_selection import ( "errors" "fmt" + "slices" "github.com/dominikbraun/graph" "github.com/klothoplatform/klotho/pkg/collectionutil" @@ -21,12 +22,14 @@ func BuildPathSelectionGraph( dep construct.SimpleEdge, kb knowledgebase.TemplateKB, classification string, + ignoreDirectEdge bool, ) (construct.Graph, error) { zap.S().Debugf("Building path selection graph for %s", dep) tempGraph := construct.NewAcyclicGraph(graph.Weighted()) // Check to see if there is a direct edge which satisfies the classification and if so short circuit in building the temp graph - if et := kb.GetEdgeTemplate(dep.Source, dep.Target); et != nil && dep.Source.Namespace == dep.Target.Namespace { + et := kb.GetEdgeTemplate(dep.Source, dep.Target) + if !ignoreDirectEdge && et != nil && dep.Source.Namespace == dep.Target.Namespace { directEdgeSatisfies := collectionutil.Contains(et.Classification, classification) if !directEdgeSatisfies { @@ -146,7 +149,7 @@ func PathSatisfiesClassification( } for i, res := range path { resTemplate, err := kb.GetResourceTemplate(res) - if err != nil { + if err != nil || slices.Contains(resTemplate.PathSatisfaction.DenyClassifications, classification) { return false } if collectionutil.Contains(resTemplate.Classification.Is, classification) { diff --git a/pkg/engine/path_selection/path_selection_test.go b/pkg/engine/path_selection/path_selection_test.go index a1228af1b..3da91b0b8 100644 --- a/pkg/engine/path_selection/path_selection_test.go +++ b/pkg/engine/path_selection/path_selection_test.go @@ -92,6 +92,7 @@ func TestBuildPathSelectionGraph(t *testing.T) { construct.SimpleEdge{Source: dep.Source, Target: dep.Target}, kb, tt.args.classification, + true, ) if tt.wantErr { require.Error(t, err) diff --git a/pkg/engine/path_selection/paths.go b/pkg/engine/path_selection/paths.go index dd6f40115..c24d3fefc 100644 --- a/pkg/engine/path_selection/paths.go +++ b/pkg/engine/path_selection/paths.go @@ -101,8 +101,8 @@ func DeterminePathSatisfactionInputs( satisfaction knowledgebase.EdgePathSatisfaction, edge construct.ResourceEdge, ) (expansions []ExpansionInput, errs error) { - srcIds := []construct.ResourceId{edge.Source.ID} - targetIds := []construct.ResourceId{edge.Target.ID} + srcIds := construct.ResourceList{edge.Source.ID} + targetIds := construct.ResourceList{edge.Target.ID} var err error if satisfaction.Source.PropertyReferenceChangesBoundary() { srcIds, err = solution_context.GetResourcesFromPropertyReference(sol, edge.Source.ID, satisfaction.Source.PropertyReference) @@ -123,6 +123,28 @@ func DeterminePathSatisfactionInputs( } } + if satisfaction.Source.Script != "" { + dynamicCtx := solution_context.DynamicCtx(sol) + err = dynamicCtx.ExecuteDecode(satisfaction.Source.Script, + knowledgebase.DynamicValueData{Resource: edge.Source.ID}, &srcIds) + if err != nil { + errs = errors.Join(errs, fmt.Errorf( + "failed to determine path satisfaction source inputs. could not run script for %s: %w", + edge.Source.ID, err, + )) + } + } + if satisfaction.Target.Script != "" { + dynamicCtx := solution_context.DynamicCtx(sol) + err = dynamicCtx.ExecuteDecode(satisfaction.Target.Script, + knowledgebase.DynamicValueData{Resource: edge.Target.ID}, &targetIds) + if err != nil { + errs = errors.Join(errs, fmt.Errorf( + "failed to determine path satisfaction target inputs. could not run script for %s: %w", + edge.Target.ID, err, + )) + } + } for _, srcId := range srcIds { for _, targetId := range targetIds { diff --git a/pkg/infra/cli.go b/pkg/infra/cli.go index c0d8d0da7..f542e1d84 100644 --- a/pkg/infra/cli.go +++ b/pkg/infra/cli.go @@ -104,7 +104,6 @@ func GetLiveState(cmd *cobra.Command, args []string) error { return err } log.Info("Loaded state templates") - reader := statereader.NewPulumiReader(templates, kb) // read in the state file if getImportConstraintsCfg.stateFile == "" { log.Error("State file path is empty") @@ -130,7 +129,8 @@ func GetLiveState(cmd *cobra.Command, args []string) error { } } bytesReader := bytes.NewReader(stateBytes) - result, err := reader.ReadState(bytesReader, input.Graph) + reader := statereader.NewPulumiReader(input.Graph, templates, kb) + result, err := reader.ReadState(bytesReader) if err != nil { return err } diff --git a/pkg/infra/iac/templates/aws/auto_scaling_group/factory.ts b/pkg/infra/iac/templates/aws/auto_scaling_group/factory.ts index 026068e87..89517d8a1 100644 --- a/pkg/infra/iac/templates/aws/auto_scaling_group/factory.ts +++ b/pkg/infra/iac/templates/aws/auto_scaling_group/factory.ts @@ -73,3 +73,7 @@ function properties(object: aws.autoscaling.Group, args: Args) { Id: object.id, } } + +function importResource(args: Args): aws.autoscaling.Group { + return aws.autoscaling.Group.get(args.Name, args.Id) +} diff --git a/pkg/infra/iac/templates/aws/availability_zone/factory.ts b/pkg/infra/iac/templates/aws/availability_zone/factory.ts index 14099110f..49833fb38 100644 --- a/pkg/infra/iac/templates/aws/availability_zone/factory.ts +++ b/pkg/infra/iac/templates/aws/availability_zone/factory.ts @@ -21,6 +21,12 @@ function importResource( return pulumi.output( aws.getAvailabilityZones({ state: 'available', + filters: [ + { + name: 'zone-name', + values: [args.Name], + }, + ], }) - ).names[args.Index] + ).names[0] } diff --git a/pkg/infra/iac/templates/aws/ec2_launch_template/factory.ts b/pkg/infra/iac/templates/aws/ec2_launch_template/factory.ts index ed698bf1a..807e8e001 100644 --- a/pkg/infra/iac/templates/aws/ec2_launch_template/factory.ts +++ b/pkg/infra/iac/templates/aws/ec2_launch_template/factory.ts @@ -34,7 +34,7 @@ function create(args: Args): aws.ec2.LaunchTemplate { //TMPL instanceType: {{ .LaunchTemplateData.instanceType}}, //TMPL {{- end }} //TMPL {{- if .LaunchTemplateData.securityGroupIds }} - //TMPL securityGroupIds: {{ .LaunchTemplateData.securityGroupIds }}, + //TMPL vpcSecurityGroupIds: {{ .LaunchTemplateData.securityGroupIds }}, //TMPL {{- end }} //TMPL {{- if .LaunchTemplateData.userData }} //TMPL userData: {{ .LaunchTemplateData.userData }}, @@ -51,3 +51,7 @@ function properties(object: aws.ec2.LaunchTemplate, args: Args) { Id: object.id, } } + +function importResource(args: Args): aws.ec2.LaunchTemplate { + return aws.ec2.LaunchTemplate.get(args.Name, args.Id) +} diff --git a/pkg/infra/iac/templates/aws/security_group/factory.ts b/pkg/infra/iac/templates/aws/security_group/factory.ts index 415a70604..1822fabd6 100644 --- a/pkg/infra/iac/templates/aws/security_group/factory.ts +++ b/pkg/infra/iac/templates/aws/security_group/factory.ts @@ -20,3 +20,14 @@ function create(args: Args): aws.ec2.SecurityGroup { //TMPL {{- end }} }) } + +function properties(object: aws.ec2.SecurityGroup, args: Args) { + return { + Arn: object.arn, + Id: object.id, + } +} + +function importResource(args: Args): aws.ec2.SecurityGroup { + return aws.ec2.SecurityGroup.get(args.Name, args.Id) +} diff --git a/pkg/infra/state_reader/resource_template_mock_test.go b/pkg/infra/state_reader/resource_template_mock_test.go new file mode 100644 index 000000000..9e7387c2a --- /dev/null +++ b/pkg/infra/state_reader/resource_template_mock_test.go @@ -0,0 +1,299 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./resource_template.go +// +// Generated by this command: +// +// mockgen -source=./resource_template.go --destination=../infra/state_reader/resource_template_mock_test.go --package=statereader +// + +// Package statereader is a generated GoMock package. +package statereader + +import ( + reflect "reflect" + + construct "github.com/klothoplatform/klotho/pkg/construct" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledgebase" + gomock "go.uber.org/mock/gomock" +) + +// MockProperty is a mock of Property interface. +type MockProperty struct { + ctrl *gomock.Controller + recorder *MockPropertyMockRecorder +} + +// MockPropertyMockRecorder is the mock recorder for MockProperty. +type MockPropertyMockRecorder struct { + mock *MockProperty +} + +// NewMockProperty creates a new mock instance. +func NewMockProperty(ctrl *gomock.Controller) *MockProperty { + mock := &MockProperty{ctrl: ctrl} + mock.recorder = &MockPropertyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProperty) EXPECT() *MockPropertyMockRecorder { + return m.recorder +} + +// AppendProperty mocks base method. +func (m *MockProperty) AppendProperty(resource *construct.Resource, value any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AppendProperty", resource, value) + ret0, _ := ret[0].(error) + return ret0 +} + +// AppendProperty indicates an expected call of AppendProperty. +func (mr *MockPropertyMockRecorder) AppendProperty(resource, value any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendProperty", reflect.TypeOf((*MockProperty)(nil).AppendProperty), resource, value) +} + +// Clone mocks base method. +func (m *MockProperty) Clone() knowledgebase.Property { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Clone") + ret0, _ := ret[0].(knowledgebase.Property) + return ret0 +} + +// Clone indicates an expected call of Clone. +func (mr *MockPropertyMockRecorder) Clone() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clone", reflect.TypeOf((*MockProperty)(nil).Clone)) +} + +// Contains mocks base method. +func (m *MockProperty) Contains(value, contains any) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Contains", value, contains) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Contains indicates an expected call of Contains. +func (mr *MockPropertyMockRecorder) Contains(value, contains any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Contains", reflect.TypeOf((*MockProperty)(nil).Contains), value, contains) +} + +// Details mocks base method. +func (m *MockProperty) Details() *knowledgebase.PropertyDetails { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Details") + ret0, _ := ret[0].(*knowledgebase.PropertyDetails) + return ret0 +} + +// Details indicates an expected call of Details. +func (mr *MockPropertyMockRecorder) Details() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Details", reflect.TypeOf((*MockProperty)(nil).Details)) +} + +// GetDefaultValue mocks base method. +func (m *MockProperty) GetDefaultValue(ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDefaultValue", ctx, data) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDefaultValue indicates an expected call of GetDefaultValue. +func (mr *MockPropertyMockRecorder) GetDefaultValue(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultValue", reflect.TypeOf((*MockProperty)(nil).GetDefaultValue), ctx, data) +} + +// Parse mocks base method. +func (m *MockProperty) Parse(value any, ctx knowledgebase.DynamicContext, data knowledgebase.DynamicValueData) (any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Parse", value, ctx, data) + ret0, _ := ret[0].(any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Parse indicates an expected call of Parse. +func (mr *MockPropertyMockRecorder) Parse(value, ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Parse", reflect.TypeOf((*MockProperty)(nil).Parse), value, ctx, data) +} + +// RemoveProperty mocks base method. +func (m *MockProperty) RemoveProperty(resource *construct.Resource, value any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveProperty", resource, value) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveProperty indicates an expected call of RemoveProperty. +func (mr *MockPropertyMockRecorder) RemoveProperty(resource, value any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProperty", reflect.TypeOf((*MockProperty)(nil).RemoveProperty), resource, value) +} + +// SetProperty mocks base method. +func (m *MockProperty) SetProperty(resource *construct.Resource, value any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetProperty", resource, value) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetProperty indicates an expected call of SetProperty. +func (mr *MockPropertyMockRecorder) SetProperty(resource, value any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProperty", reflect.TypeOf((*MockProperty)(nil).SetProperty), resource, value) +} + +// SubProperties mocks base method. +func (m *MockProperty) SubProperties() knowledgebase.Properties { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubProperties") + ret0, _ := ret[0].(knowledgebase.Properties) + return ret0 +} + +// SubProperties indicates an expected call of SubProperties. +func (mr *MockPropertyMockRecorder) SubProperties() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubProperties", reflect.TypeOf((*MockProperty)(nil).SubProperties)) +} + +// Type mocks base method. +func (m *MockProperty) Type() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Type") + ret0, _ := ret[0].(string) + return ret0 +} + +// Type indicates an expected call of Type. +func (mr *MockPropertyMockRecorder) Type() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockProperty)(nil).Type)) +} + +// Validate mocks base method. +func (m *MockProperty) Validate(resource *construct.Resource, value any, ctx knowledgebase.DynamicContext) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validate", resource, value, ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Validate indicates an expected call of Validate. +func (mr *MockPropertyMockRecorder) Validate(resource, value, ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockProperty)(nil).Validate), resource, value, ctx) +} + +// ZeroValue mocks base method. +func (m *MockProperty) ZeroValue() any { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ZeroValue") + ret0, _ := ret[0].(any) + return ret0 +} + +// ZeroValue indicates an expected call of ZeroValue. +func (mr *MockPropertyMockRecorder) ZeroValue() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZeroValue", reflect.TypeOf((*MockProperty)(nil).ZeroValue)) +} + +// MockMapProperty is a mock of MapProperty interface. +type MockMapProperty struct { + ctrl *gomock.Controller + recorder *MockMapPropertyMockRecorder +} + +// MockMapPropertyMockRecorder is the mock recorder for MockMapProperty. +type MockMapPropertyMockRecorder struct { + mock *MockMapProperty +} + +// NewMockMapProperty creates a new mock instance. +func NewMockMapProperty(ctrl *gomock.Controller) *MockMapProperty { + mock := &MockMapProperty{ctrl: ctrl} + mock.recorder = &MockMapPropertyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMapProperty) EXPECT() *MockMapPropertyMockRecorder { + return m.recorder +} + +// Key mocks base method. +func (m *MockMapProperty) Key() knowledgebase.Property { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Key") + ret0, _ := ret[0].(knowledgebase.Property) + return ret0 +} + +// Key indicates an expected call of Key. +func (mr *MockMapPropertyMockRecorder) Key() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockMapProperty)(nil).Key)) +} + +// Value mocks base method. +func (m *MockMapProperty) Value() knowledgebase.Property { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Value") + ret0, _ := ret[0].(knowledgebase.Property) + return ret0 +} + +// Value indicates an expected call of Value. +func (mr *MockMapPropertyMockRecorder) Value() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*MockMapProperty)(nil).Value)) +} + +// MockCollectionProperty is a mock of CollectionProperty interface. +type MockCollectionProperty struct { + ctrl *gomock.Controller + recorder *MockCollectionPropertyMockRecorder +} + +// MockCollectionPropertyMockRecorder is the mock recorder for MockCollectionProperty. +type MockCollectionPropertyMockRecorder struct { + mock *MockCollectionProperty +} + +// NewMockCollectionProperty creates a new mock instance. +func NewMockCollectionProperty(ctrl *gomock.Controller) *MockCollectionProperty { + mock := &MockCollectionProperty{ctrl: ctrl} + mock.recorder = &MockCollectionPropertyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCollectionProperty) EXPECT() *MockCollectionPropertyMockRecorder { + return m.recorder +} + +// Item mocks base method. +func (m *MockCollectionProperty) Item() knowledgebase.Property { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Item") + ret0, _ := ret[0].(knowledgebase.Property) + return ret0 +} + +// Item indicates an expected call of Item. +func (mr *MockCollectionPropertyMockRecorder) Item() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Item", reflect.TypeOf((*MockCollectionProperty)(nil).Item)) +} diff --git a/pkg/infra/state_reader/state_converter/pulumi.go b/pkg/infra/state_reader/state_converter/pulumi.go index f24aaf0da..676f12f4c 100644 --- a/pkg/infra/state_reader/state_converter/pulumi.go +++ b/pkg/infra/state_reader/state_converter/pulumi.go @@ -66,18 +66,13 @@ func (p pulumiStateConverter) convertResource(resource Resource, template statet properties := make(construct.Properties) for k, v := range resource.Outputs { if mapping, ok := template.PropertyMappings[k]; ok { - if strings.Contains(mapping, "#") { - // TODO: Determine how to cross correlate references/resource properties. - // an example of this is the subnets vpcId field (value = vpc-123456789), to where internally its modeld as a "resource". - continue - } properties[mapping] = v } } // Convert the keys to camel case klothoResource := &construct.Resource{ ID: id, - Properties: convertKeysToCamelCase(properties), + Properties: properties, } return klothoResource, nil } diff --git a/pkg/infra/state_reader/state_converter/pulumi_test.go b/pkg/infra/state_reader/state_converter/pulumi_test.go index b8faaf38b..febb91210 100644 --- a/pkg/infra/state_reader/state_converter/pulumi_test.go +++ b/pkg/infra/state_reader/state_converter/pulumi_test.go @@ -90,8 +90,8 @@ func Test_pulumiStateConverter_convertResource(t *testing.T) { QualifiedTypeName: "aws:lambda_function", IaCQualifiedType: "aws:lambda/Function:Function", PropertyMappings: map[string]string{ - "arn": "arn", - "id": "id", + "arn": "Arn", + "id": "Id", }, }, resource: Resource{ @@ -133,8 +133,9 @@ func Test_pulumiStateConverter_convertResource(t *testing.T) { want: construct.Resource{ ID: construct.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_lambda"}, Properties: construct.Properties{ - "Arn": "arn", - "Id": "id", + "Arn": "arn", + "Id": "id", + "Vpc#Id": "vpc-1", }, }, }, diff --git a/pkg/infra/state_reader/state_converter/state_converter.go b/pkg/infra/state_reader/state_converter/state_converter.go index 066eaf45f..11de05562 100644 --- a/pkg/infra/state_reader/state_converter/state_converter.go +++ b/pkg/infra/state_reader/state_converter/state_converter.go @@ -3,7 +3,6 @@ package stateconverter import ( "io" - "github.com/iancoleman/strcase" "github.com/klothoplatform/klotho/pkg/construct" statetemplate "github.com/klothoplatform/klotho/pkg/infra/state_reader/state_template" ) @@ -22,23 +21,3 @@ type ( func NewStateConverter(provider string, templates map[string]statetemplate.StateTemplate) StateConverter { return &pulumiStateConverter{templates: templates} } - -func convertKeysToCamelCase(data construct.Properties) construct.Properties { - result := make(map[string]interface{}) - for key, value := range data { - camelCaseKey := strcase.ToCamel(key) - switch v := value.(type) { - case map[string]interface{}: - resultingProperties := convertKeysToCamelCase(v) - // convert properties to map[string]any - mapResult := make(map[string]interface{}) - for k, v := range resultingProperties { - mapResult[k] = v - } - result[camelCaseKey] = mapResult - default: - result[camelCaseKey] = v - } - } - return result -} diff --git a/pkg/infra/state_reader/state_converter/state_converter_test.go b/pkg/infra/state_reader/state_converter/state_converter_test.go deleted file mode 100644 index 09161cb8f..000000000 --- a/pkg/infra/state_reader/state_converter/state_converter_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package stateconverter - -import ( - "testing" - - "github.com/klothoplatform/klotho/pkg/construct" - "github.com/stretchr/testify/assert" -) - -func Test_convertKeysToCamelCase(t *testing.T) { - tests := []struct { - name string - data construct.Properties - want construct.Properties - }{ - { - name: "converts keys to camel case", - data: construct.Properties{ - "urn": "urn", - "type": "type", - "outputs": map[string]interface{}{ - "output_key": "output_value", - }, - }, - want: construct.Properties{ - "Urn": "urn", - "Type": "type", - "Outputs": map[string]interface{}{ - "OutputKey": "output_value", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert := assert.New(t) - got := convertKeysToCamelCase(tt.data) - assert.Equal(tt.want, got) - }) - } -} diff --git a/pkg/infra/state_reader/state_reader.go b/pkg/infra/state_reader/state_reader.go index 3d4196d2a..008e33f4f 100644 --- a/pkg/infra/state_reader/state_reader.go +++ b/pkg/infra/state_reader/state_reader.go @@ -1,62 +1,323 @@ package statereader import ( + "errors" + "fmt" "io" + "reflect" + "strings" + "github.com/dominikbraun/graph" "github.com/klothoplatform/klotho/pkg/construct" stateconverter "github.com/klothoplatform/klotho/pkg/infra/state_reader/state_converter" statetemplate "github.com/klothoplatform/klotho/pkg/infra/state_reader/state_template" "github.com/klothoplatform/klotho/pkg/knowledgebase" + "github.com/klothoplatform/klotho/pkg/knowledgebase/properties" ) +//go:generate mockgen -source=./state_reader.go --destination=./state_reader_mock_test.go --package=statereader + type ( // StateReader is an interface for reading state from a state store StateReader interface { // ReadState reads the state from the state store - ReadState(io.Reader, construct.Graph) (construct.Graph, error) + ReadState(io.Reader) (construct.Graph, error) + } + + propertyCorrelation interface { + setProperty( + resource *construct.Resource, + property string, + value any, + ) error + checkValue( + step knowledgebase.OperationalStep, + value string, + src construct.ResourceId, + propertyRef string, + ) (*construct.Edge, *construct.PropertyRef, error) } stateReader struct { templates map[string]statetemplate.StateTemplate kb knowledgebase.TemplateKB converter stateconverter.StateConverter + graph construct.Graph + } + + propertyCorrelator struct { + ctx knowledgebase.DynamicValueContext + resources []*construct.Resource } ) -func NewPulumiReader(templates map[string]statetemplate.StateTemplate, kb knowledgebase.TemplateKB) StateReader { - return &stateReader{templates: templates, kb: kb, converter: stateconverter.NewStateConverter("pulumi", templates)} +func NewPulumiReader(g construct.Graph, templates map[string]statetemplate.StateTemplate, kb knowledgebase.TemplateKB) StateReader { + return &stateReader{graph: g, templates: templates, kb: kb, converter: stateconverter.NewStateConverter("pulumi", templates)} } -func (p stateReader) ReadState(reader io.Reader, graph construct.Graph) (construct.Graph, error) { - returnGraph := construct.NewGraph() +func (p stateReader) ReadState(reader io.Reader) (construct.Graph, error) { internalState, err := p.converter.ConvertState(reader) if err != nil { return nil, err } - for id, properties := range internalState { - var resource *construct.Resource - if graph != nil { - resource, err = graph.Vertex(id) - if err != nil { - return nil, err - } + if p.graph == nil { + p.graph = construct.NewGraph() + } + if err = p.loadGraph(internalState); err != nil { + return p.graph, err + } + existingResources := make([]*construct.Resource, 0) + adj, err := p.graph.AdjacencyMap() + if err != nil { + return p.graph, err + } + for id := range adj { + r, err := p.graph.Vertex(id) + if err != nil { + return p.graph, err + } + existingResources = append(existingResources, r) + } + + ctx := knowledgebase.DynamicValueContext{Graph: p.graph, KnowledgeBase: p.kb} + pc := propertyCorrelator{ + ctx: ctx, + resources: existingResources, + } + if err = p.loadProperties(internalState, pc); err != nil { + return p.graph, err + } + + return p.graph, nil +} + +func (p stateReader) loadGraph(state stateconverter.State) error { + var errs error + for id, properties := range state { + resource, err := p.graph.Vertex(id) + if err != nil && !errors.Is(err, graph.ErrVertexNotFound) { + errs = errors.Join(errs, err) + continue } if resource == nil { resource = &construct.Resource{ ID: id, Properties: make(construct.Properties), } + err = p.graph.AddVertex(resource) + if err != nil { + errs = errors.Join(errs, err) + continue + } + } + rt, err := p.kb.GetResourceTemplate(id) + if err != nil { + errs = errors.Join(errs, err) + continue + } else if rt == nil { + errs = errors.Join(errs, fmt.Errorf("resource template not found for resource %s", id)) + continue + } + for key, value := range properties { + if !strings.Contains(key, "#") { + prop := rt.GetProperty(key) + if prop == nil { + errs = errors.Join(errs, fmt.Errorf("property %s not found in resource template %s", key, id)) + continue + } + errs = errors.Join(errs, prop.SetProperty(resource, value)) + } + } + } + return errs +} + +func (p stateReader) loadProperties(state stateconverter.State, pc propertyCorrelation) error { + var errs error + for id, properties := range state { + resource, err := p.graph.Vertex(id) + if err != nil { + errs = errors.Join(errs, err) + continue } for key, value := range properties { - err := resource.SetProperty(key, value) + errs = errors.Join(errs, pc.setProperty(resource, key, value)) + } + } + return errs +} + +func (p propertyCorrelator) setProperty( + resource *construct.Resource, + property string, + value any, +) error { + edges := make([]*construct.Edge, 0) + rt, err := p.ctx.KnowledgeBase.GetResourceTemplate(resource.ID) + if err != nil { + return err + } else if rt == nil { + return fmt.Errorf("resource template not found for resource %s", resource.ID) + } + parts := strings.Split(property, "#") + property = parts[0] + prop := rt.GetProperty(property) + if prop == nil { + return fmt.Errorf("property %s not found in resource template %s", property, resource.ID) + } + opRule := prop.Details().OperationalRule + if opRule == nil || len(opRule.Step.Resources) == 0 { + return resource.SetProperty(property, value) + } + var ref string + if len(parts) > 1 { + ref = parts[1] + } else { + ref = opRule.Step.UsePropertyRef + } + + switch rval := reflect.ValueOf(value); rval.Kind() { + case reflect.String: + edge, pref, err := p.checkValue(opRule.Step, value.(string), resource.ID, ref) + if err != nil { + return err + } + if edge != nil { + edges = append(edges, edge) + } + if pref != nil { + switch prop.(type) { + case *properties.ResourceProperty: + err = prop.SetProperty(resource, pref.Resource) + if err != nil { + return err + } + default: + err = prop.SetProperty(resource, pref) + if err != nil { + return err + } + } + + } + case reflect.Slice, reflect.Array: + var val []any + for i := 0; i < rval.Len(); i++ { + edge, pref, err := p.checkValue(opRule.Step, rval.Index(i).Interface().(string), resource.ID, ref) + if err != nil { + return err + } + if edge != nil { + edges = append(edges, edge) + } + if pref != nil { + val = append(val, *pref) + } + } + collectionProp, ok := prop.(knowledgebase.CollectionProperty) + if !ok { + return fmt.Errorf("property %s is not a collection property", property) + } + switch collectionProp.Item().(type) { + case *properties.ResourceProperty: + resources := make([]construct.ResourceId, 0) + for _, v := range val { + resources = append(resources, v.(construct.PropertyRef).Resource) + } + err = prop.SetProperty(resource, resources) + if err != nil { + return err + } + default: + err = prop.SetProperty(resource, val) if err != nil { - return nil, err + return err + } + } + } + for _, edge := range edges { + err := p.ctx.Graph.AddEdge(edge.Source, edge.Target) + if err != nil { + return err + } + } + return nil +} + +func (p propertyCorrelator) checkValue( + step knowledgebase.OperationalStep, + value string, + src construct.ResourceId, + propertyRef string, +) (*construct.Edge, *construct.PropertyRef, error) { + var possibleIds []construct.ResourceId + data := knowledgebase.DynamicValueData{Resource: src} + for _, selector := range step.Resources { + ids, err := selector.ExtractResourceIds(p.ctx, data) + if err != nil { + return nil, nil, err + } + possibleIds = append(possibleIds, ids...) + for _, id := range ids { + for _, resource := range p.resources { + if id.Matches(resource.ID) { + val, err := p.ctx.FieldValue(propertyRef, resource.ID) + if err != nil { + return nil, nil, err + } + if val == value { + if step.Direction == knowledgebase.DirectionDownstream { + return &construct.Edge{ + Source: src, + Target: resource.ID, + }, &construct.PropertyRef{ + Resource: resource.ID, + Property: propertyRef, + }, nil + } else { + return &construct.Edge{ + Source: resource.ID, + Target: src, + }, &construct.PropertyRef{ + Resource: resource.ID, + Property: propertyRef, + }, nil + } + } + } } } - err = returnGraph.AddVertex(resource) + } + if len(step.Resources) == 1 { + idToUse := possibleIds[0] + newRes := &construct.Resource{ + ID: construct.ResourceId{Provider: idToUse.Provider, Type: idToUse.Type, Name: value}, + } + err := newRes.SetProperty(propertyRef, value) + if err != nil { + return nil, nil, err + } + err = p.ctx.Graph.AddVertex(newRes) if err != nil { - return nil, err + return nil, nil, err + } + if step.Direction == knowledgebase.DirectionDownstream { + return &construct.Edge{ + Source: src, + Target: newRes.ID, + }, &construct.PropertyRef{ + Resource: newRes.ID, + Property: propertyRef, + }, nil + } else { + return &construct.Edge{ + Source: newRes.ID, + Target: src, + }, &construct.PropertyRef{ + Resource: newRes.ID, + Property: propertyRef, + }, nil } } - return returnGraph, nil + return nil, nil, nil } diff --git a/pkg/infra/state_reader/state_reader_mock_test.go b/pkg/infra/state_reader/state_reader_mock_test.go new file mode 100644 index 000000000..8768dc74a --- /dev/null +++ b/pkg/infra/state_reader/state_reader_mock_test.go @@ -0,0 +1,110 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./state_reader.go +// +// Generated by this command: +// +// mockgen -source=./state_reader.go --destination=./state_reader_mock_test.go --package=statereader +// + +// Package statereader is a generated GoMock package. +package statereader + +import ( + io "io" + reflect "reflect" + + construct "github.com/klothoplatform/klotho/pkg/construct" + knowledgebase "github.com/klothoplatform/klotho/pkg/knowledgebase" + gomock "go.uber.org/mock/gomock" +) + +// MockStateReader is a mock of StateReader interface. +type MockStateReader struct { + ctrl *gomock.Controller + recorder *MockStateReaderMockRecorder +} + +// MockStateReaderMockRecorder is the mock recorder for MockStateReader. +type MockStateReaderMockRecorder struct { + mock *MockStateReader +} + +// NewMockStateReader creates a new mock instance. +func NewMockStateReader(ctrl *gomock.Controller) *MockStateReader { + mock := &MockStateReader{ctrl: ctrl} + mock.recorder = &MockStateReaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStateReader) EXPECT() *MockStateReaderMockRecorder { + return m.recorder +} + +// ReadState mocks base method. +func (m *MockStateReader) ReadState(arg0 io.Reader) (construct.Graph, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadState", arg0) + ret0, _ := ret[0].(construct.Graph) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadState indicates an expected call of ReadState. +func (mr *MockStateReaderMockRecorder) ReadState(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadState", reflect.TypeOf((*MockStateReader)(nil).ReadState), arg0) +} + +// MockpropertyCorrelation is a mock of propertyCorrelation interface. +type MockpropertyCorrelation struct { + ctrl *gomock.Controller + recorder *MockpropertyCorrelationMockRecorder +} + +// MockpropertyCorrelationMockRecorder is the mock recorder for MockpropertyCorrelation. +type MockpropertyCorrelationMockRecorder struct { + mock *MockpropertyCorrelation +} + +// NewMockpropertyCorrelation creates a new mock instance. +func NewMockpropertyCorrelation(ctrl *gomock.Controller) *MockpropertyCorrelation { + mock := &MockpropertyCorrelation{ctrl: ctrl} + mock.recorder = &MockpropertyCorrelationMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockpropertyCorrelation) EXPECT() *MockpropertyCorrelationMockRecorder { + return m.recorder +} + +// checkValue mocks base method. +func (m *MockpropertyCorrelation) checkValue(step knowledgebase.OperationalStep, value string, src construct.ResourceId, propertyRef string) (*construct.Edge, *construct.PropertyRef, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "checkValue", step, value, src, propertyRef) + ret0, _ := ret[0].(*construct.Edge) + ret1, _ := ret[1].(*construct.PropertyRef) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// checkValue indicates an expected call of checkValue. +func (mr *MockpropertyCorrelationMockRecorder) checkValue(step, value, src, propertyRef any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "checkValue", reflect.TypeOf((*MockpropertyCorrelation)(nil).checkValue), step, value, src, propertyRef) +} + +// setProperty mocks base method. +func (m *MockpropertyCorrelation) setProperty(resource *construct.Resource, property string, value any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "setProperty", resource, property, value) + ret0, _ := ret[0].(error) + return ret0 +} + +// setProperty indicates an expected call of setProperty. +func (mr *MockpropertyCorrelationMockRecorder) setProperty(resource, property, value any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "setProperty", reflect.TypeOf((*MockpropertyCorrelation)(nil).setProperty), resource, property, value) +} diff --git a/pkg/infra/state_reader/state_reader_test.go b/pkg/infra/state_reader/state_reader_test.go index 65a821099..b54a049e6 100644 --- a/pkg/infra/state_reader/state_reader_test.go +++ b/pkg/infra/state_reader/state_reader_test.go @@ -1,49 +1,51 @@ package statereader import ( - "bytes" "testing" "github.com/klothoplatform/klotho/pkg/construct" "github.com/klothoplatform/klotho/pkg/engine/enginetesting" stateconverter "github.com/klothoplatform/klotho/pkg/infra/state_reader/state_converter" - statetemplate "github.com/klothoplatform/klotho/pkg/infra/state_reader/state_template" + "github.com/klothoplatform/klotho/pkg/knowledgebase" "github.com/stretchr/testify/assert" gomock "go.uber.org/mock/gomock" ) -func Test_stateReader_ReadState(t *testing.T) { +func Test_stateReader_LoadGraph(t *testing.T) { tests := []struct { - name string - templates map[string]statetemplate.StateTemplate - mocks func(mockConverter *MockStateConverter, mockKB *enginetesting.MockKB) - state []byte - graph construct.Graph - want []*construct.Resource - wantErr bool + name string + mocks func(mockKB *enginetesting.MockKB, ctrl *gomock.Controller) + state stateconverter.State + graph construct.Graph + want []*construct.Resource + wantErr bool }{ { name: "ReadState with no input graph", - templates: map[string]statetemplate.StateTemplate{ - "aws:lambda/Function:Function": { - QualifiedTypeName: "aws:lambda_function", - IaCQualifiedType: "aws:lambda/Function:Function", - PropertyMappings: map[string]string{ - "arn": "Arn", - "id": "Id", - }, - }, - }, - mocks: func(mockConverter *MockStateConverter, mockKB *enginetesting.MockKB) { - bytesReader := bytes.NewReader([]byte(`fake state`)) - mockConverter.EXPECT().ConvertState(bytesReader).Return(stateconverter.State{ - construct.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_lambda"}: construct.Properties{ - "Arn": "arn", - "Id": "id", + mocks: func(mockKB *enginetesting.MockKB, ctrl *gomock.Controller) { + mockArnProperty := NewMockProperty(ctrl) + mockIdProperty := NewMockProperty(ctrl) + mockKB.On("GetResourceTemplate", + construct.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_lambda"}, + ).Return(&knowledgebase.ResourceTemplate{ + Properties: knowledgebase.Properties{ + "Arn": mockArnProperty, + "Id": mockIdProperty, }, }, nil) + mockArnProperty.EXPECT().Details().Return(&knowledgebase.PropertyDetails{}) + mockIdProperty.EXPECT().Details().Return(&knowledgebase.PropertyDetails{}) + mockArnProperty.EXPECT().Clone().Return(mockArnProperty) + mockIdProperty.EXPECT().Clone().Return(mockIdProperty) + mockArnProperty.EXPECT().SetProperty(gomock.Any(), "arn").Return(nil).Times(1) + mockIdProperty.EXPECT().SetProperty(gomock.Any(), "id").Return(nil).Times(1) + }, + state: stateconverter.State{ + construct.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_lambda"}: construct.Properties{ + "Arn": "arn", + "Id": "id", + }, }, - state: []byte(`fake state`), want: []*construct.Resource{ { ID: construct.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_lambda"}, @@ -59,26 +61,73 @@ func Test_stateReader_ReadState(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) ctrl := gomock.NewController(t) - mockConverter := NewMockStateConverter(ctrl) mockKB := &enginetesting.MockKB{} - tt.mocks(mockConverter, mockKB) + tt.mocks(mockKB, ctrl) p := stateReader{ - templates: tt.templates, - kb: mockKB, - converter: mockConverter, + kb: mockKB, + graph: construct.NewGraph(), } - byteReader := bytes.NewReader(tt.state) - got, err := p.ReadState(byteReader, tt.graph) + err := p.loadGraph(tt.state) if !assert.NoError(err) { return } for _, resource := range tt.want { - resource, err := got.Vertex(resource.ID) + resource, err := p.graph.Vertex(resource.ID) if !assert.NoError(err) { return } assert.Equal(resource, resource) } + ctrl.Finish() + }) + } +} +func Test_stateReader_checkValue(t *testing.T) { + tests := []struct { + name string + mocks func(mockpc *MockpropertyCorrelation) + state stateconverter.State + graph construct.Graph + wantErr bool + }{ + { + name: "ReadState with no input graph", + mocks: func(mockpc *MockpropertyCorrelation) { + mockpc.EXPECT().setProperty(gomock.Any(), "Arn", "arn").Return(nil).Times(1) + mockpc.EXPECT().setProperty(gomock.Any(), "Id", "id").Return(nil).Times(1) + }, + state: stateconverter.State{ + construct.ResourceId{Provider: "aws", Type: "lambda_function", Name: "my_lambda"}: construct.Properties{ + "Arn": "arn", + "Id": "id", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + ctrl := gomock.NewController(t) + mockCorrelator := NewMockpropertyCorrelation(ctrl) + tt.mocks(mockCorrelator) + p := stateReader{ + graph: construct.NewGraph(), + } + for id := range tt.state { + resource := &construct.Resource{ + ID: id, + Properties: make(construct.Properties), + } + err := p.graph.AddVertex(resource) + if !assert.NoError(err) { + return + } + } + err := p.loadProperties(tt.state, mockCorrelator) + if !assert.NoError(err) { + return + } + ctrl.Finish() }) } } diff --git a/pkg/infra/state_reader/state_template/mappings/pulumi/auto_scaling_group.yaml b/pkg/infra/state_reader/state_template/mappings/pulumi/auto_scaling_group.yaml new file mode 100644 index 000000000..3379c6c18 --- /dev/null +++ b/pkg/infra/state_reader/state_template/mappings/pulumi/auto_scaling_group.yaml @@ -0,0 +1,7 @@ +qualified_type_name: aws:auto_scaling_group +iac_qualified_type: aws:autoscaling/group:Group + +property_mappings: + arn: Arn + id: Id + vpcZoneIdentifiers: VPCZoneIdentifier \ No newline at end of file diff --git a/pkg/infra/state_reader/state_template/mappings/pulumi/ec2_launch_template.yaml b/pkg/infra/state_reader/state_template/mappings/pulumi/ec2_launch_template.yaml new file mode 100644 index 000000000..e198a0f09 --- /dev/null +++ b/pkg/infra/state_reader/state_template/mappings/pulumi/ec2_launch_template.yaml @@ -0,0 +1,8 @@ +qualified_type_name: aws:ec2_launch_template +iac_qualified_type: aws:ec2/launchTemplate:LaunchTemplate + +property_mappings: + arn: Arn + id: Id + tags: Tags + vpcSecurityGroupIds: LaunchTemplateData.SecurityGroupIds \ No newline at end of file diff --git a/pkg/infra/state_reader/state_template/mappings/pulumi/lambda_function.yaml b/pkg/infra/state_reader/state_template/mappings/pulumi/lambda_function.yaml index 5a43808cd..6cd75b384 100644 --- a/pkg/infra/state_reader/state_template/mappings/pulumi/lambda_function.yaml +++ b/pkg/infra/state_reader/state_template/mappings/pulumi/lambda_function.yaml @@ -6,5 +6,4 @@ property_mappings: id: Id tags: Tags memorySize: MemorySize - timeout: Timeout - name: Name \ No newline at end of file + timeout: Timeout \ No newline at end of file diff --git a/pkg/infra/state_reader/state_template/mappings/pulumi/security_group.yaml b/pkg/infra/state_reader/state_template/mappings/pulumi/security_group.yaml new file mode 100644 index 000000000..cd9c93fb7 --- /dev/null +++ b/pkg/infra/state_reader/state_template/mappings/pulumi/security_group.yaml @@ -0,0 +1,7 @@ +qualified_type_name: aws:security_group +iac_qualified_type: aws:ec2/securityGroup:SecurityGroup + +property_mappings: + arn: Arn + id: Id + tags: Tags \ No newline at end of file diff --git a/pkg/knowledgebase/path_satisfaction.go b/pkg/knowledgebase/path_satisfaction.go index a888038e1..dab16f201 100644 --- a/pkg/knowledgebase/path_satisfaction.go +++ b/pkg/knowledgebase/path_satisfaction.go @@ -1,6 +1,7 @@ package knowledgebase import ( + "fmt" "strings" construct "github.com/klothoplatform/klotho/pkg/construct" @@ -9,14 +10,16 @@ import ( type ( PathSatisfaction struct { - AsTarget []PathSatisfactionRoute `json:"as_target" yaml:"as_target"` - AsSource []PathSatisfactionRoute `json:"as_source" yaml:"as_source"` + AsTarget []PathSatisfactionRoute `json:"as_target" yaml:"as_target"` + AsSource []PathSatisfactionRoute `json:"as_source" yaml:"as_source"` + DenyClassifications []string `yaml:"deny_classifications"` } PathSatisfactionRoute struct { Classification string `json:"classification" yaml:"classification"` PropertyReference string `json:"property_reference" yaml:"property_reference"` Validity PathSatisfactionValidityOperation `json:"validity" yaml:"validity"` + Script string `json:"script" yaml:"script"` } PathSatisfactionValidityOperation string @@ -42,6 +45,9 @@ func (p *PathSatisfactionRoute) UnmarshalYAML(n *yaml.Node) error { } p2.Validity = PathSatisfactionValidityOperation(strings.ToLower(string(p2.Validity))) *p = PathSatisfactionRoute(p2) + if p.PropertyReference != "" && p.Script != "" { + return fmt.Errorf("path satisfaction route cannot have both property reference and script") + } return nil } diff --git a/pkg/knowledgebase/resource_template.go b/pkg/knowledgebase/resource_template.go index 41548c007..49bbc2510 100644 --- a/pkg/knowledgebase/resource_template.go +++ b/pkg/knowledgebase/resource_template.go @@ -15,6 +15,7 @@ import ( //go:generate mockgen -source=./resource_template.go --destination=./resource_template_mock_test.go --package=knowledgebase //go:generate mockgen -source=./resource_template.go --destination=../engine2/operational_eval/resource_template_mock_test.go --package=operational_eval +//go:generate mockgen -source=./resource_template.go --destination=../infra/state_reader/resource_template_mock_test.go --package=statereader type ( // ResourceTemplate defines how rules are handled by the engine in terms of making sure they are functional in the graph diff --git a/pkg/templates/aws/edges/security_group-ec2_launch_template.yaml b/pkg/templates/aws/edges/security_group-ec2_launch_template.yaml new file mode 100644 index 000000000..06138c54f --- /dev/null +++ b/pkg/templates/aws/edges/security_group-ec2_launch_template.yaml @@ -0,0 +1,3 @@ +source: aws:security_group +target: aws:ec2_launch_template +deployment_order_reversed: true \ No newline at end of file diff --git a/pkg/templates/aws/edges/security_group_rule-security_group.yaml b/pkg/templates/aws/edges/security_group_rule-security_group.yaml index 3ccf2584d..8fd5c09f2 100644 --- a/pkg/templates/aws/edges/security_group_rule-security_group.yaml +++ b/pkg/templates/aws/edges/security_group_rule-security_group.yaml @@ -1,9 +1,8 @@ source: aws:security_group_rule target: aws:security_group -deployment_order_reversed: true operational_rules: - configuration_rules: - resource: '{{ .Source }}' configuration: - field: SecurityGroup - value: '{{ .Target }}' + field: SecurityGroupId + value: '{{ fieldRef "Id" .Target }}' diff --git a/pkg/templates/aws/edges/subnet-security_group_rule.yaml b/pkg/templates/aws/edges/subnet-security_group_rule.yaml new file mode 100644 index 000000000..c51799533 --- /dev/null +++ b/pkg/templates/aws/edges/subnet-security_group_rule.yaml @@ -0,0 +1,30 @@ +source: aws:subnet +target: aws:security_group_rule +unique: one-to-many +operational_rules: + - configuration_rules: + - resource: '{{ .Target }}' + configuration: + field: CidrBlocks + value: + - '{{ fieldValue "CidrBlock" .Source }}' + - resource: '{{ .Target }}' + configuration: + field: FromPort + value: 0 + - resource: '{{ .Target }}' + configuration: + field: Protocol + value: -1 + - resource: '{{ .Target }}' + configuration: + field: ToPort + value: 0 + - resource: '{{ .Target }}' + configuration: + field: Type + value: ingress + - resource: '{{ .Target }}' + configuration: + field: Description + value: Allow ingress from {{ .Source.Name }} \ No newline at end of file diff --git a/pkg/templates/aws/edges/target_group-ecs_service.yaml b/pkg/templates/aws/edges/target_group-ecs_service.yaml index 9023c9a6f..ad8d96d37 100644 --- a/pkg/templates/aws/edges/target_group-ecs_service.yaml +++ b/pkg/templates/aws/edges/target_group-ecs_service.yaml @@ -2,7 +2,8 @@ source: aws:target_group target: aws:ecs_service deployment_order_reversed: true operational_rules: - - configuration_rules: + - if: '{{ eq (fieldValue "NetworkMode" (fieldValue "TaskDefinition" .Target)) "awsvpc" }}' + configuration_rules: - resource: '{{ .Target }}' configuration: field: LoadBalancers @@ -14,5 +15,19 @@ operational_rules: configuration: field: TargetType value: ip + - if: '{{ eq (fieldValue "NetworkMode" (fieldValue "TaskDefinition" .Target)) "bridge" }}' + configuration_rules: + - resource: '{{ .Target }}' + configuration: + field: LoadBalancers + value: + - TargetGroup: '{{ .Source }}' + ContainerName: '{{ fieldValue "ContainerDefinitions[0].Name" (downstream "aws:ecs_task_definition" .Target) }}' + ContainerPort: '{{ fieldValue "ContainerDefinitions[0].PortMappings[0].ContainerPort" (fieldValue "TaskDefinition" .Target)}}' + - resource: '{{ .Source }}' + configuration: + field: TargetType + value: instance + classification: - service_discovery diff --git a/pkg/templates/aws/resources/lambda_function.yaml b/pkg/templates/aws/resources/lambda_function.yaml index 32bd6bed0..cc2cb9164 100644 --- a/pkg/templates/aws/resources/lambda_function.yaml +++ b/pkg/templates/aws/resources/lambda_function.yaml @@ -83,6 +83,10 @@ properties: type: string configuration_disabled: true deploy_time: true + Id: + type: string + configuration_disabled: true + deploy_time: true path_satisfaction: as_target: diff --git a/pkg/templates/aws/resources/security_group.yaml b/pkg/templates/aws/resources/security_group.yaml index fa40cc840..befad5ebb 100644 --- a/pkg/templates/aws/resources/security_group.yaml +++ b/pkg/templates/aws/resources/security_group.yaml @@ -62,6 +62,21 @@ properties: to itself aws:tags: type: model + Arn: + type: string + description: The Amazon Resource Name (ARN) of the Auto Scaling group. + configuration_disabled: true + deploy_time: true + Id: + type: string + description: The unique identifier for the cluster. + configuration_disabled: true + deploy_time: true + required: true + +path_satisfaction: + deny_classifications: + - permissions classification: is: diff --git a/pkg/templates/aws/resources/security_group_rule.yaml b/pkg/templates/aws/resources/security_group_rule.yaml index a6bcb9641..5ed8bf63b 100644 --- a/pkg/templates/aws/resources/security_group_rule.yaml +++ b/pkg/templates/aws/resources/security_group_rule.yaml @@ -28,7 +28,13 @@ properties: type: string description: Specifies the rule type, either 'ingress' or 'egress', defining the traffic direction + allowed_values: + - ingress + - egress +classification: + is: + - network deployment_permissions: deploy: ["ec2:AuthorizeSecurityGroupIngress", "ec2:AuthorizeSecurityGroupEgress"] diff --git a/pkg/templates/aws/resources/subnet.yaml b/pkg/templates/aws/resources/subnet.yaml index 362697db2..d2c98fe52 100644 --- a/pkg/templates/aws/resources/subnet.yaml +++ b/pkg/templates/aws/resources/subnet.yaml @@ -69,7 +69,9 @@ properties: path_satisfaction: as_source: - network - + deny_classifications: + - permissions + classification: is: - network From 1c605914e9cbd57209d682f9e67a245fe3762f4f Mon Sep 17 00:00:00 2001 From: Jordan Singer Date: Wed, 10 Apr 2024 10:35:53 -0500 Subject: [PATCH 2/6] fix state reading --- pkg/infra/state_reader/state_reader.go | 14 +++++++++++--- .../mappings/pulumi/lambda_function.yaml | 4 +--- .../aws/edges/target_group-ecs_service.yaml | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pkg/infra/state_reader/state_reader.go b/pkg/infra/state_reader/state_reader.go index 008e33f4f..568a38302 100644 --- a/pkg/infra/state_reader/state_reader.go +++ b/pkg/infra/state_reader/state_reader.go @@ -290,10 +290,18 @@ func (p propertyCorrelator) checkValue( } if len(step.Resources) == 1 { idToUse := possibleIds[0] - newRes := &construct.Resource{ - ID: construct.ResourceId{Provider: idToUse.Provider, Type: idToUse.Type, Name: value}, + id := construct.ResourceId{Provider: idToUse.Provider, Type: idToUse.Type, Name: value} + newRes, err := p.ctx.Graph.Vertex(id) + if err != nil && !errors.Is(err, graph.ErrVertexNotFound) { + return nil, nil, err } - err := newRes.SetProperty(propertyRef, value) + if newRes == nil { + newRes = &construct.Resource{ + ID: id, + } + } + + err = newRes.SetProperty(propertyRef, value) if err != nil { return nil, nil, err } diff --git a/pkg/infra/state_reader/state_template/mappings/pulumi/lambda_function.yaml b/pkg/infra/state_reader/state_template/mappings/pulumi/lambda_function.yaml index 6cd75b384..2ee5dfa03 100644 --- a/pkg/infra/state_reader/state_template/mappings/pulumi/lambda_function.yaml +++ b/pkg/infra/state_reader/state_template/mappings/pulumi/lambda_function.yaml @@ -4,6 +4,4 @@ iac_qualified_type: aws:lambda/function:Function property_mappings: arn: Arn id: Id - tags: Tags - memorySize: MemorySize - timeout: Timeout \ No newline at end of file + tags: Tags \ No newline at end of file diff --git a/pkg/templates/aws/edges/target_group-ecs_service.yaml b/pkg/templates/aws/edges/target_group-ecs_service.yaml index ad8d96d37..7358c2ad9 100644 --- a/pkg/templates/aws/edges/target_group-ecs_service.yaml +++ b/pkg/templates/aws/edges/target_group-ecs_service.yaml @@ -15,7 +15,7 @@ operational_rules: configuration: field: TargetType value: ip - - if: '{{ eq (fieldValue "NetworkMode" (fieldValue "TaskDefinition" .Target)) "bridge" }}' + - if: '{{ ne (fieldValue "NetworkMode" (fieldValue "TaskDefinition" .Target)) "awsvpc" }}' configuration_rules: - resource: '{{ .Target }}' configuration: From 5ed9ded0f6aac93d3e4b8ecfb438aa3e8e66e5b7 Mon Sep 17 00:00:00 2001 From: Jordan Singer Date: Wed, 10 Apr 2024 14:15:16 -0500 Subject: [PATCH 3/6] address comments --- .../operational_eval/resource_template_mock_test.go | 2 +- pkg/engine/path_selection/candidate_validity.go | 12 ++++++------ pkg/engine/path_selection/candidate_validity_test.go | 11 ++++++----- pkg/engine/path_selection/path_expansion.go | 8 ++++---- pkg/engine/path_selection/path_selection.go | 10 ++++------ pkg/infra/state_reader/state_reader.go | 11 +++++++---- pkg/infra/state_reader/state_reader_mock_test.go | 12 ++++++------ pkg/knowledgebase/path_satisfaction.go | 7 ++++--- pkg/knowledgebase/resource_template.go | 2 +- 9 files changed, 39 insertions(+), 36 deletions(-) diff --git a/pkg/engine/operational_eval/resource_template_mock_test.go b/pkg/engine/operational_eval/resource_template_mock_test.go index 1cf64adb2..72655e5cc 100644 --- a/pkg/engine/operational_eval/resource_template_mock_test.go +++ b/pkg/engine/operational_eval/resource_template_mock_test.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -source=./resource_template.go --destination=../engine2/operational_eval/resource_template_mock_test.go --package=operational_eval +// mockgen -source=./resource_template.go --destination=../engine/operational_eval/resource_template_mock_test.go --package=operational_eval // // Package operational_eval is a generated GoMock package. diff --git a/pkg/engine/path_selection/candidate_validity.go b/pkg/engine/path_selection/candidate_validity.go index 1d0b8c555..27bf311a5 100644 --- a/pkg/engine/path_selection/candidate_validity.go +++ b/pkg/engine/path_selection/candidate_validity.go @@ -21,9 +21,9 @@ type ( } ) -// checkDoesNotModifyImportedResource checks if there is an imported resource that would be modified due to the edge +// checkModifiesImportedResource checks if there is an imported resource that would be modified due to the edge // If there is an edge rule modifying the resource then we consider the edge to be invalid -func checkDoesNotModifyImportedResource( +func checkModifiesImportedResource( source, target construct.ResourceId, ctx solution_context.SolutionContext, et *knowledgebase.EdgeTemplate, @@ -33,7 +33,7 @@ func checkDoesNotModifyImportedResource( // see if the target resource exists in the graph targetResource, trgtErr := ctx.RawView().Vertex(target) if errors.Is(srcErr, graph.ErrVertexNotFound) && errors.Is(trgtErr, graph.ErrVertexNotFound) { - return true, nil + return false, nil } if et == nil { @@ -42,7 +42,7 @@ func checkDoesNotModifyImportedResource( checkRules := func(resources construct.ResourceList) (bool, error) { if len(resources) == 0 { - return true, nil + return false, nil } for _, rule := range et.OperationalRules { for _, config := range rule.ConfigurationRules { @@ -56,11 +56,11 @@ func checkDoesNotModifyImportedResource( }}, &id) if resources.MatchesAny(id) { - return false, nil + return true, nil } } } - return true, nil + return false, nil } importedResources := construct.ResourceList{} diff --git a/pkg/engine/path_selection/candidate_validity_test.go b/pkg/engine/path_selection/candidate_validity_test.go index 3377d19b9..4b68c053c 100644 --- a/pkg/engine/path_selection/candidate_validity_test.go +++ b/pkg/engine/path_selection/candidate_validity_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_checkDoesNotModifyImportedResource(t *testing.T) { +func Test_checkModifiesImportedResource(t *testing.T) { tests := []struct { name string source *construct.Resource @@ -25,7 +25,7 @@ func Test_checkDoesNotModifyImportedResource(t *testing.T) { source: &construct.Resource{ID: graphtest.ParseId(t, "p:a:a")}, target: &construct.Resource{ID: graphtest.ParseId(t, "p:b:b")}, et: &knowledgebase.EdgeTemplate{}, - want: true, + want: false, }, { name: "imported resource with no modifications returns true", @@ -42,7 +42,7 @@ func Test_checkDoesNotModifyImportedResource(t *testing.T) { }, }, }, - want: true, + want: false, }, { name: "imported resource with modifications returns false", @@ -59,6 +59,7 @@ func Test_checkDoesNotModifyImportedResource(t *testing.T) { }, }, }, + want: true, }, { name: "gets edge template if not provided", @@ -67,7 +68,7 @@ func Test_checkDoesNotModifyImportedResource(t *testing.T) { mocks: func(kb *enginetesting.MockKB) { kb.On("GetEdgeTemplate", graphtest.ParseId(t, "p:a:a"), graphtest.ParseId(t, "p:b:b")).Return(&knowledgebase.EdgeTemplate{}) }, - want: true, + want: false, }, } for _, tt := range tests { @@ -82,7 +83,7 @@ func Test_checkDoesNotModifyImportedResource(t *testing.T) { err = sol.RawView().AddVertex(tt.target) require.NoError(t, err) - got, err := checkDoesNotModifyImportedResource(tt.source.ID, tt.target.ID, sol, tt.et) + got, err := checkModifiesImportedResource(tt.source.ID, tt.target.ID, sol, tt.et) require.NoError(t, err) require.Equal(t, tt.want, got) sol.KB.AssertExpectations(t) diff --git a/pkg/engine/path_selection/path_expansion.go b/pkg/engine/path_selection/path_expansion.go index 7a50e4fe4..66a6d70fd 100644 --- a/pkg/engine/path_selection/path_expansion.go +++ b/pkg/engine/path_selection/path_expansion.go @@ -310,12 +310,12 @@ func expandPath( ) error { if len(path) == 2 { - doesNotModifyImport, err := checkDoesNotModifyImportedResource(input.SatisfactionEdge.Source.ID, + modifiesImport, err := checkModifiesImportedResource(input.SatisfactionEdge.Source.ID, input.SatisfactionEdge.Target.ID, ctx, nil) if err != nil { return err } - if !doesNotModifyImport { + if modifiesImport { // Because the direct edge will cause modifications to an imported resource, we need to remove the direct edge return input.TempGraph.RemoveEdge(input.SatisfactionEdge.Source.ID, input.SatisfactionEdge.Target.ID) @@ -444,12 +444,12 @@ func expandPath( if !tmpl.Unique.CanAdd(edges, source.id, target.id) { return } - doesNotModifyImport, err := checkDoesNotModifyImportedResource(source.id, target.id, ctx, tmpl) + modifiesImport, err := checkModifiesImportedResource(source.id, target.id, ctx, tmpl) if err != nil { errs = errors.Join(errs, err) return } - if !doesNotModifyImport { + if modifiesImport { return } // if the edge doesnt exist in the actual graph and there is any uniqueness constraint, diff --git a/pkg/engine/path_selection/path_selection.go b/pkg/engine/path_selection/path_selection.go index 084267bf4..3f2b0eb37 100644 --- a/pkg/engine/path_selection/path_selection.go +++ b/pkg/engine/path_selection/path_selection.go @@ -147,25 +147,23 @@ func PathSatisfiesClassification( if classification == "" { return true } + metClassification := false for i, res := range path { resTemplate, err := kb.GetResourceTemplate(res) if err != nil || slices.Contains(resTemplate.PathSatisfaction.DenyClassifications, classification) { return false } if collectionutil.Contains(resTemplate.Classification.Is, classification) { - return true + metClassification = true } if i > 0 { et := kb.GetEdgeTemplate(path[i-1], res) if collectionutil.Contains(et.Classification, classification) { - return true + metClassification = true } } - if i == len(path)-1 { - return false - } } - return true + return metClassification } func makePhantom(g construct.Graph, id construct.ResourceId) (construct.ResourceId, error) { diff --git a/pkg/infra/state_reader/state_reader.go b/pkg/infra/state_reader/state_reader.go index 568a38302..873f3c100 100644 --- a/pkg/infra/state_reader/state_reader.go +++ b/pkg/infra/state_reader/state_reader.go @@ -30,7 +30,7 @@ type ( property string, value any, ) error - checkValue( + checkValueForReferences( step knowledgebase.OperationalStep, value string, src construct.ResourceId, @@ -178,7 +178,7 @@ func (p propertyCorrelator) setProperty( switch rval := reflect.ValueOf(value); rval.Kind() { case reflect.String: - edge, pref, err := p.checkValue(opRule.Step, value.(string), resource.ID, ref) + edge, pref, err := p.checkValueForReferences(opRule.Step, value.(string), resource.ID, ref) if err != nil { return err } @@ -203,7 +203,7 @@ func (p propertyCorrelator) setProperty( case reflect.Slice, reflect.Array: var val []any for i := 0; i < rval.Len(); i++ { - edge, pref, err := p.checkValue(opRule.Step, rval.Index(i).Interface().(string), resource.ID, ref) + edge, pref, err := p.checkValueForReferences(opRule.Step, rval.Index(i).Interface().(string), resource.ID, ref) if err != nil { return err } @@ -244,7 +244,10 @@ func (p propertyCorrelator) setProperty( return nil } -func (p propertyCorrelator) checkValue( +// checkValueForReferences checks if the value of a property is a reference to another resource +// If it is a reference then it will substitute the live state value for a property ref or resource id +// if no resource exists in the live state for the reference, then it will try to create a new resource representing the value +func (p propertyCorrelator) checkValueForReferences( step knowledgebase.OperationalStep, value string, src construct.ResourceId, diff --git a/pkg/infra/state_reader/state_reader_mock_test.go b/pkg/infra/state_reader/state_reader_mock_test.go index 8768dc74a..9a24d1ed4 100644 --- a/pkg/infra/state_reader/state_reader_mock_test.go +++ b/pkg/infra/state_reader/state_reader_mock_test.go @@ -79,20 +79,20 @@ func (m *MockpropertyCorrelation) EXPECT() *MockpropertyCorrelationMockRecorder return m.recorder } -// checkValue mocks base method. -func (m *MockpropertyCorrelation) checkValue(step knowledgebase.OperationalStep, value string, src construct.ResourceId, propertyRef string) (*construct.Edge, *construct.PropertyRef, error) { +// checkValueForReferences mocks base method. +func (m *MockpropertyCorrelation) checkValueForReferences(step knowledgebase.OperationalStep, value string, src construct.ResourceId, propertyRef string) (*construct.Edge, *construct.PropertyRef, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "checkValue", step, value, src, propertyRef) + ret := m.ctrl.Call(m, "checkValueForReferences", step, value, src, propertyRef) ret0, _ := ret[0].(*construct.Edge) ret1, _ := ret[1].(*construct.PropertyRef) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } -// checkValue indicates an expected call of checkValue. -func (mr *MockpropertyCorrelationMockRecorder) checkValue(step, value, src, propertyRef any) *gomock.Call { +// checkValueForReferences indicates an expected call of checkValueForReferences. +func (mr *MockpropertyCorrelationMockRecorder) checkValueForReferences(step, value, src, propertyRef any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "checkValue", reflect.TypeOf((*MockpropertyCorrelation)(nil).checkValue), step, value, src, propertyRef) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "checkValueForReferences", reflect.TypeOf((*MockpropertyCorrelation)(nil).checkValueForReferences), step, value, src, propertyRef) } // setProperty mocks base method. diff --git a/pkg/knowledgebase/path_satisfaction.go b/pkg/knowledgebase/path_satisfaction.go index dab16f201..0b1af9b24 100644 --- a/pkg/knowledgebase/path_satisfaction.go +++ b/pkg/knowledgebase/path_satisfaction.go @@ -10,9 +10,10 @@ import ( type ( PathSatisfaction struct { - AsTarget []PathSatisfactionRoute `json:"as_target" yaml:"as_target"` - AsSource []PathSatisfactionRoute `json:"as_source" yaml:"as_source"` - DenyClassifications []string `yaml:"deny_classifications"` + AsTarget []PathSatisfactionRoute `json:"as_target" yaml:"as_target"` + AsSource []PathSatisfactionRoute `json:"as_source" yaml:"as_source"` + // DenyClassifications is a list of classifications that the resource cannot be included in paths during expansion + DenyClassifications []string `yaml:"deny_classifications"` } PathSatisfactionRoute struct { diff --git a/pkg/knowledgebase/resource_template.go b/pkg/knowledgebase/resource_template.go index 49bbc2510..fea2f0c40 100644 --- a/pkg/knowledgebase/resource_template.go +++ b/pkg/knowledgebase/resource_template.go @@ -14,7 +14,7 @@ import ( ) //go:generate mockgen -source=./resource_template.go --destination=./resource_template_mock_test.go --package=knowledgebase -//go:generate mockgen -source=./resource_template.go --destination=../engine2/operational_eval/resource_template_mock_test.go --package=operational_eval +//go:generate mockgen -source=./resource_template.go --destination=../engine/operational_eval/resource_template_mock_test.go --package=operational_eval //go:generate mockgen -source=./resource_template.go --destination=../infra/state_reader/resource_template_mock_test.go --package=statereader type ( From 437fe51d90f421a2f484e1d3e0eddc7254eeed74 Mon Sep 17 00:00:00 2001 From: Jordan Singer Date: Thu, 11 Apr 2024 08:52:43 -0500 Subject: [PATCH 4/6] read RDS from imports and address comments --- pkg/engine/operational_rule/operational_action.go | 2 +- pkg/infra/iac/templates/aws/rds_instance/factory.ts | 4 ++++ .../state_template/mappings/pulumi/rds_instance.yaml | 6 ++++++ pkg/templates/aws/resources/auto_scaling_group.yaml | 1 - pkg/templates/aws/resources/rds_instance.yaml | 5 +++++ 5 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 pkg/infra/state_reader/state_template/mappings/pulumi/rds_instance.yaml diff --git a/pkg/engine/operational_rule/operational_action.go b/pkg/engine/operational_rule/operational_action.go index cacf1baa1..99925817b 100644 --- a/pkg/engine/operational_rule/operational_action.go +++ b/pkg/engine/operational_rule/operational_action.go @@ -91,7 +91,7 @@ func (action *operationalResourceAction) createUniqueResources(resource *constru return err } } - if len(uids) == 1 && uids[0].Matches(resource.ID) { + if len(uids) == 1 && uids[0] == resource.ID { res, err := action.ruleCtx.Solution.RawView().Vertex(id) if err != nil { return err diff --git a/pkg/infra/iac/templates/aws/rds_instance/factory.ts b/pkg/infra/iac/templates/aws/rds_instance/factory.ts index ef071f25e..d15cf4f8f 100644 --- a/pkg/infra/iac/templates/aws/rds_instance/factory.ts +++ b/pkg/infra/iac/templates/aws/rds_instance/factory.ts @@ -70,3 +70,7 @@ function infraExports( Endpoint: object.endpoint, } } + +function importResource(args: Args): aws.rds.Instance { + return aws.rds.Instance.get(args.Name, args.Arn) +} diff --git a/pkg/infra/state_reader/state_template/mappings/pulumi/rds_instance.yaml b/pkg/infra/state_reader/state_template/mappings/pulumi/rds_instance.yaml new file mode 100644 index 000000000..ee0083a0d --- /dev/null +++ b/pkg/infra/state_reader/state_template/mappings/pulumi/rds_instance.yaml @@ -0,0 +1,6 @@ +qualified_type_name: aws:rds_instance +iac_qualified_type: aws:rds/instance:Instance + +property_mappings: + arn: Arn + tags: Tags \ No newline at end of file diff --git a/pkg/templates/aws/resources/auto_scaling_group.yaml b/pkg/templates/aws/resources/auto_scaling_group.yaml index 5b91fe28d..13f530225 100644 --- a/pkg/templates/aws/resources/auto_scaling_group.yaml +++ b/pkg/templates/aws/resources/auto_scaling_group.yaml @@ -43,7 +43,6 @@ properties: direction: downstream resources: - aws:ec2_launch_template - unique: true use_property_ref: Id LaunchTemplateName: type: string diff --git a/pkg/templates/aws/resources/rds_instance.yaml b/pkg/templates/aws/resources/rds_instance.yaml index 0a2248146..dc62981b4 100644 --- a/pkg/templates/aws/resources/rds_instance.yaml +++ b/pkg/templates/aws/resources/rds_instance.yaml @@ -113,6 +113,11 @@ properties: type: string configuration_disabled: true deploy_time: true + Arn: + type: string + configuration_disabled: true + deploy_time: true + required: true consumption: emitted: From c8682c53ecf8127f0147afd49645876109033c01 Mon Sep 17 00:00:00 2001 From: Jordan Singer Date: Fri, 12 Apr 2024 10:00:20 -0500 Subject: [PATCH 5/6] update password and username --- pkg/infra/iac/templates/aws/rds_instance/factory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/infra/iac/templates/aws/rds_instance/factory.ts b/pkg/infra/iac/templates/aws/rds_instance/factory.ts index d15cf4f8f..57039964d 100644 --- a/pkg/infra/iac/templates/aws/rds_instance/factory.ts +++ b/pkg/infra/iac/templates/aws/rds_instance/factory.ts @@ -46,8 +46,8 @@ function create(args: Args): aws.rds.Instance { function properties(object: aws.rds.Instance, args: Args) { return { - Password: kloConfig.requireSecret(`${args.Name}-password`), - Username: kloConfig.requireSecret(`${args.Name}-username`), + Password: object.password.apply((pass) => pass!), + Username: object.username, CredentialsSecretValue: pulumi.jsonStringify({ username: object.username, password: object.password, From 82fc05c0c245ad7a84551c923950a294f606c9d2 Mon Sep 17 00:00:00 2001 From: Jordan Singer Date: Fri, 12 Apr 2024 10:37:44 -0500 Subject: [PATCH 6/6] add unit test --- .../path_selection/path_selection_test.go | 152 ++++++++++++++++++ pkg/templates/aws/resources/iam_role.yaml | 5 + 2 files changed, 157 insertions(+) diff --git a/pkg/engine/path_selection/path_selection_test.go b/pkg/engine/path_selection/path_selection_test.go index 3da91b0b8..3dd0c620b 100644 --- a/pkg/engine/path_selection/path_selection_test.go +++ b/pkg/engine/path_selection/path_selection_test.go @@ -113,3 +113,155 @@ func TestBuildPathSelectionGraph(t *testing.T) { }) } } + +func TestPathSatisfiesClassification(t *testing.T) { + tests := []struct { + name string + resourceTemplates []*knowledgebase.ResourceTemplate + EdgeTemplates []*knowledgebase.EdgeTemplate + path []construct.ResourceId + classification string + want bool + }{ + { + name: "empty classification", + path: []construct.ResourceId{ + graphtest.ParseId(t, "p:a:a"), + graphtest.ParseId(t, "p:b:b"), + }, + resourceTemplates: []*knowledgebase.ResourceTemplate{ + { + QualifiedTypeName: "p:a", + Classification: knowledgebase.Classification{Is: []string{"network"}}, + }, + { + QualifiedTypeName: "p:b", + }, + }, + EdgeTemplates: []*knowledgebase.EdgeTemplate{ + { + Source: graphtest.ParseId(t, "p:a:"), + Target: graphtest.ParseId(t, "p:b:"), + }, + }, + classification: "", + want: true, + }, + { + name: "resource template satisfies classification", + resourceTemplates: []*knowledgebase.ResourceTemplate{ + { + QualifiedTypeName: "p:a", + Classification: knowledgebase.Classification{Is: []string{"network"}}, + }, + { + QualifiedTypeName: "p:b", + }, + }, + EdgeTemplates: []*knowledgebase.EdgeTemplate{ + { + Source: graphtest.ParseId(t, "p:a:"), + Target: graphtest.ParseId(t, "p:b:"), + }, + }, + path: []construct.ResourceId{ + graphtest.ParseId(t, "p:a:a"), + graphtest.ParseId(t, "p:b:b"), + }, + classification: "network", + want: true, + }, + { + name: "resource template does not satisfy classification", + resourceTemplates: []*knowledgebase.ResourceTemplate{ + { + QualifiedTypeName: "p:a", + Classification: knowledgebase.Classification{Is: []string{"network"}}, + }, + { + QualifiedTypeName: "p:b", + }, + }, + EdgeTemplates: []*knowledgebase.EdgeTemplate{ + { + Source: graphtest.ParseId(t, "p:a:"), + Target: graphtest.ParseId(t, "p:b:"), + Classification: []string{"network"}, + }, + }, + path: []construct.ResourceId{ + graphtest.ParseId(t, "p:a:a"), + graphtest.ParseId(t, "p:b:b"), + }, + classification: "storage", + want: false, + }, + { + name: "resource template denies classification", + resourceTemplates: []*knowledgebase.ResourceTemplate{ + { + QualifiedTypeName: "p:a", + Classification: knowledgebase.Classification{Is: []string{"network"}}, + }, + { + QualifiedTypeName: "p:b", + PathSatisfaction: knowledgebase.PathSatisfaction{ + DenyClassifications: []string{"network"}, + }, + }, + }, + EdgeTemplates: []*knowledgebase.EdgeTemplate{ + { + Source: graphtest.ParseId(t, "p:a:"), + Target: graphtest.ParseId(t, "p:b:"), + Classification: []string{"network"}, + }, + }, + path: []construct.ResourceId{ + graphtest.ParseId(t, "p:a:a"), + graphtest.ParseId(t, "p:b:b"), + }, + classification: "network", + want: false, + }, + { + name: "edge template satisfies classification", + resourceTemplates: []*knowledgebase.ResourceTemplate{ + { + QualifiedTypeName: "p:a", + }, + { + QualifiedTypeName: "p:b", + }, + }, + EdgeTemplates: []*knowledgebase.EdgeTemplate{ + { + Source: graphtest.ParseId(t, "p:a:"), + Target: graphtest.ParseId(t, "p:b:"), + Classification: []string{"network"}, + }, + }, + path: []construct.ResourceId{ + graphtest.ParseId(t, "p:a:a"), + graphtest.ParseId(t, "p:b:b"), + }, + classification: "network", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kb := knowledgebase.NewKB() + for _, rt := range tt.resourceTemplates { + err := kb.AddResourceTemplate(rt) + require.NoError(t, err) + } + for _, et := range tt.EdgeTemplates { + err := kb.AddEdgeTemplate(et) + require.NoError(t, err) + } + satisfied := PathSatisfiesClassification(kb, tt.path, tt.classification) + assert.Equal(t, tt.want, satisfied) + }) + } +} diff --git a/pkg/templates/aws/resources/iam_role.yaml b/pkg/templates/aws/resources/iam_role.yaml index 95e787634..954c6e54d 100644 --- a/pkg/templates/aws/resources/iam_role.yaml +++ b/pkg/templates/aws/resources/iam_role.yaml @@ -101,6 +101,11 @@ classification: - permissions - security +path_satisfaction: + deny_classifications: + - network + + delete_context: requires_no_upstream: true views: