Skip to content

Commit

Permalink
Merge pull request #415 from overmindtech/fic-mapping-output
Browse files Browse the repository at this point in the history
Improved the way mapping results are shown
  • Loading branch information
dylanratcliffe authored Jun 20, 2024
2 parents 4d56cc7 + 7f1d20a commit 9dbbe57
Show file tree
Hide file tree
Showing 7 changed files with 379 additions and 341 deletions.
4 changes: 2 additions & 2 deletions cmd/changes_submit_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,15 +343,15 @@ func SubmitPlan(cmd *cobra.Command, args []string) error {
lf := log.Fields{}
for _, f := range args {
lf["file"] = f
_, mappedItemDiffs, _, err := mappedItemDiffsFromPlanFile(ctx, f, lf)
result, err := mappedItemDiffsFromPlanFile(ctx, f, lf)
if err != nil {
return loggedError{
err: err,
fields: lf,
message: "Error parsing terraform plan",
}
}
plannedChanges = append(plannedChanges, mappedItemDiffs...)
plannedChanges = append(plannedChanges, result.GetItemDiffs()...)
}
delete(lf, "file")

Expand Down
2 changes: 1 addition & 1 deletion cmd/changes_submit_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

func TestWithStateFile(t *testing.T) {
_, _, _, err := mappedItemDiffsFromPlanFile(context.Background(), "testdata/state.json", logrus.Fields{})
_, err := mappedItemDiffsFromPlanFile(context.Background(), "testdata/state.json", logrus.Fields{})

if err == nil {
t.Error("Expected error when running with state file, got none")
Expand Down
296 changes: 296 additions & 0 deletions cmd/plan_mapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"os"
"time"

"github.com/google/uuid"
"github.com/overmindtech/cli/cmd/datamaps"
"github.com/overmindtech/sdp-go"
log "github.com/sirupsen/logrus"
"google.golang.org/protobuf/types/known/timestamppb"
)

type MapStatus int

const (
MapStatusSuccess MapStatus = iota
MapStatusNotEnoughInfo
MapStatusUnsupported
)

type PlannedChangeMapResult struct {
// The name of the resource in the Terraform plan
TerraformName string

// The status of the mapping
Status MapStatus

// The message that should be printed next to the status e.g. "mapped" or
// "missing arn"
Message string

*sdp.MappedItemDiff
}

type PlanMappingResult struct {
Results []PlannedChangeMapResult
RemovedSecrets int
}

func (r *PlanMappingResult) NumSuccess() int {
return r.numStatus(MapStatusSuccess)
}

func (r *PlanMappingResult) NumNotEnoughInfo() int {
return r.numStatus(MapStatusNotEnoughInfo)
}

func (r *PlanMappingResult) NumUnsupported() int {
return r.numStatus(MapStatusUnsupported)
}

func (r *PlanMappingResult) NumTotal() int {
return len(r.Results)
}

func (r *PlanMappingResult) GetItemDiffs() []*sdp.MappedItemDiff {
diffs := make([]*sdp.MappedItemDiff, 0)

for _, result := range r.Results {
if result.MappedItemDiff != nil {
diffs = append(diffs, result.MappedItemDiff)
}
}

return diffs
}

func (r *PlanMappingResult) numStatus(status MapStatus) int {
count := 0
for _, result := range r.Results {
if result.Status == status {
count++
}
}
return count
}

func mappedItemDiffsFromPlanFile(ctx context.Context, fileName string, lf log.Fields) (*PlanMappingResult, error) {
// read results from `terraform show -json ${tfplan file}`
planJSON, err := os.ReadFile(fileName)
if err != nil {
log.WithContext(ctx).WithError(err).WithFields(lf).Error("Failed to read terraform plan")
return nil, err
}

return mappedItemDiffsFromPlan(ctx, planJSON, fileName, lf)
}

// mappedItemDiffsFromPlan takes a plan JSON, file name, and log fields as input
// and returns the mapping results and an error. It parses the plan JSON,
// extracts resource changes, and creates mapped item differences for each
// resource change. It also generates mapping queries based on the resource type
// and current resource values. The function categorizes the mapped item
// differences into supported and unsupported changes. Finally, it logs the
// number of supported and unsupported changes and returns the mapped item
// differences.
func mappedItemDiffsFromPlan(ctx context.Context, planJson []byte, fileName string, lf log.Fields) (*PlanMappingResult, error) {
// Check that we haven't been passed a state file
if isStateFile(planJson) {
return nil, fmt.Errorf("'%v' appears to be a state file, not a plan file", fileName)
}

var plan Plan
err := json.Unmarshal(planJson, &plan)
if err != nil {
return nil, fmt.Errorf("failed to parse '%v': %w", fileName, err)
}

results := PlanMappingResult{
Results: make([]PlannedChangeMapResult, 0),
RemovedSecrets: countSensitiveValuesInConfig(plan.Config.RootModule) + countSensitiveValuesInState(plan.PlannedValues.RootModule),
}

// for all managed resources:
for _, resourceChange := range plan.ResourceChanges {
if len(resourceChange.Change.Actions) == 0 || resourceChange.Change.Actions[0] == "no-op" || resourceChange.Mode == "data" {
// skip resources with no changes and data updates
continue
}

itemDiff, err := itemDiffFromResourceChange(resourceChange)
if err != nil {
return nil, fmt.Errorf("failed to create item diff for resource change: %w", err)
}

// Load mappings for this type. These mappings tell us how to create an
// SDP query that will return this resource
awsMappings := datamaps.AwssourceData[resourceChange.Type]
k8sMappings := datamaps.K8ssourceData[resourceChange.Type]
mappings := append(awsMappings, k8sMappings...)

if len(mappings) == 0 {
log.WithContext(ctx).WithFields(lf).WithField("terraform-address", resourceChange.Address).Debug("Skipping unmapped resource")
results.Results = append(results.Results, PlannedChangeMapResult{
TerraformName: resourceChange.Address,
Status: MapStatusUnsupported,
Message: "unsupported",
MappedItemDiff: &sdp.MappedItemDiff{
Item: itemDiff,
MappingQuery: nil, // unmapped item has no mapping query
},
})
continue
}

for _, mapData := range mappings {
var currentResource *Resource

// Look for the resource in the prior values first, since this is
// the *previous* state we're like to be able to find it in the
// actual infra
if plan.PriorState.Values != nil {
currentResource = plan.PriorState.Values.RootModule.DigResource(resourceChange.Address)
}

// If we didn't find it, look in the planned values
if currentResource == nil {
currentResource = plan.PlannedValues.RootModule.DigResource(resourceChange.Address)
}

if currentResource == nil {
log.WithContext(ctx).
WithFields(lf).
WithField("terraform-address", resourceChange.Address).
WithField("terraform-query-field", mapData.QueryField).Warn("Skipping resource without values")
continue
}

query, ok := currentResource.AttributeValues.Dig(mapData.QueryField)
if !ok {
log.WithContext(ctx).
WithFields(lf).
WithField("terraform-address", resourceChange.Address).
WithField("terraform-query-field", mapData.QueryField).Warn("Adding unmapped resource")
results.Results = append(results.Results, PlannedChangeMapResult{
TerraformName: resourceChange.Address,
Status: MapStatusNotEnoughInfo,
Message: fmt.Sprintf("missing %v", mapData.QueryField),
MappedItemDiff: &sdp.MappedItemDiff{
Item: itemDiff,
MappingQuery: nil, // unmapped item has no mapping query
},
})
continue
}

// Create the map that variables will pull data from
dataMap := make(map[string]any)

// Populate resource values
dataMap["values"] = currentResource.AttributeValues

if overmindMappingsOutput, ok := plan.PlannedValues.Outputs["overmind_mappings"]; ok {
configResource := plan.Config.RootModule.DigResource(resourceChange.Address)

if configResource == nil {
log.WithContext(ctx).
WithFields(lf).
WithField("terraform-address", resourceChange.Address).
Debug("Skipping provider mapping for resource without config")
} else {
// Look up the provider config key in the mappings
mappings := make(map[string]map[string]string)

err = json.Unmarshal(overmindMappingsOutput.Value, &mappings)

if err != nil {
log.WithContext(ctx).
WithFields(lf).
WithField("terraform-address", resourceChange.Address).
WithError(err).
Error("Failed to parse overmind_mappings output")
} else {
// We need to split out the module section of the name
// here. If the resource isn't in a module, the
// ProviderConfigKey will be something like
// "kubernetes", however if it's in a module it's be
// something like "module.something:kubernetes"
providerName := extractProviderNameFromConfigKey(configResource.ProviderConfigKey)
currentProviderMappings, ok := mappings[providerName]

if ok {
log.WithContext(ctx).
WithFields(lf).
WithField("terraform-address", resourceChange.Address).
WithField("provider-config-key", configResource.ProviderConfigKey).
Debug("Found provider mappings")

// We have mappings for this provider, so set them
// in the `provider_mapping` value
dataMap["provider_mapping"] = currentProviderMappings
}
}
}
}

// Interpolate variables in the scope
scope, err := InterpolateScope(mapData.Scope, dataMap)

if err != nil {
log.WithContext(ctx).WithError(err).Debugf("Could not find scope mapping variables %v, adding them will result in better results. Error: ", mapData.Scope)
scope = "*"
}

u := uuid.New()
newQuery := &sdp.Query{
Type: mapData.Type,
Method: mapData.Method,
Query: fmt.Sprintf("%v", query),
Scope: scope,
RecursionBehaviour: &sdp.Query_RecursionBehaviour{},
UUID: u[:],
Deadline: timestamppb.New(time.Now().Add(60 * time.Second)),
}

// cleanup item metadata from mapping query
if itemDiff.GetBefore() != nil {
itemDiff.Before.Type = newQuery.GetType()
if newQuery.GetScope() != "*" {
itemDiff.Before.Scope = newQuery.GetScope()
}
}

// cleanup item metadata from mapping query
if itemDiff.GetAfter() != nil {
itemDiff.After.Type = newQuery.GetType()
if newQuery.GetScope() != "*" {
itemDiff.After.Scope = newQuery.GetScope()
}
}

results.Results = append(results.Results, PlannedChangeMapResult{
TerraformName: resourceChange.Address,
Status: MapStatusSuccess,
Message: "mapped",
MappedItemDiff: &sdp.MappedItemDiff{
Item: itemDiff,
MappingQuery: newQuery,
},
})

log.WithContext(ctx).WithFields(log.Fields{
"scope": newQuery.GetScope(),
"type": newQuery.GetType(),
"query": newQuery.GetQuery(),
"method": newQuery.GetMethod().String(),
}).Debug("Mapped resource to query")
}
}

return &results, nil
}
38 changes: 19 additions & 19 deletions cmd/terraform_plan_test.go → cmd/plan_mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import (
)

func TestMappedItemDiffsFromPlan(t *testing.T) {
numSecrets, mappedItemDiffs, _, err := mappedItemDiffsFromPlanFile(context.Background(), "testdata/plan.json", logrus.Fields{})
results, err := mappedItemDiffsFromPlanFile(context.Background(), "testdata/plan.json", logrus.Fields{})
if err != nil {
t.Error(err)
}

if numSecrets != 16 {
t.Errorf("Expected 16 secrets, got %v", numSecrets)
if results.RemovedSecrets != 16 {
t.Errorf("Expected 16 secrets, got %v", results.RemovedSecrets)
}

if len(mappedItemDiffs) != 5 {
t.Errorf("Expected 5 changes, got %v:", len(mappedItemDiffs))
for _, diff := range mappedItemDiffs {
if len(results.Results) != 5 {
t.Errorf("Expected 5 changes, got %v:", len(results.Results))
for _, diff := range results.Results {
t.Errorf(" %v", diff)
}
}
Expand All @@ -30,37 +30,37 @@ func TestMappedItemDiffsFromPlan(t *testing.T) {
var aws_iam_policy *sdp.MappedItemDiff
var secret *sdp.MappedItemDiff

for _, diff := range mappedItemDiffs {
item := diff.GetItem().GetBefore()
if item == nil && diff.GetItem().GetAfter() != nil {
item = diff.GetItem().GetAfter()
for _, result := range results.Results {
item := result.GetItem().GetBefore()
if item == nil && result.GetItem().GetAfter() != nil {
item = result.GetItem().GetAfter()
}
if item == nil {
t.Errorf("Expected any of before/after items to be set, but there's nothing: %v", diff)
t.Errorf("Expected any of before/after items to be set, but there's nothing: %v", result)
continue
}

// t.Logf("item: %v", item.Attributes.AttrStruct.Fields["terraform_address"].GetStringValue())
if item.GetAttributes().GetAttrStruct().GetFields()["terraform_address"].GetStringValue() == "kubernetes_deployment.nats_box" {
if nats_box_deployment != nil {
t.Errorf("Found multiple nats_box_deployment: %v, %v", nats_box_deployment, diff)
t.Errorf("Found multiple nats_box_deployment: %v, %v", nats_box_deployment, result)
}
nats_box_deployment = diff
nats_box_deployment = result.MappedItemDiff
} else if item.GetAttributes().GetAttrStruct().GetFields()["terraform_address"].GetStringValue() == "kubernetes_deployment.api_server" {
if api_server_deployment != nil {
t.Errorf("Found multiple api_server_deployment: %v, %v", api_server_deployment, diff)
t.Errorf("Found multiple api_server_deployment: %v, %v", api_server_deployment, result)
}
api_server_deployment = diff
api_server_deployment = result.MappedItemDiff
} else if item.GetType() == "iam-policy" {
if aws_iam_policy != nil {
t.Errorf("Found multiple aws_iam_policy: %v, %v", aws_iam_policy, diff)
t.Errorf("Found multiple aws_iam_policy: %v, %v", aws_iam_policy, result)
}
aws_iam_policy = diff
aws_iam_policy = result.MappedItemDiff
} else if item.GetType() == "Secret" {
if secret != nil {
t.Errorf("Found multiple secrets: %v, %v", secret, diff)
t.Errorf("Found multiple secrets: %v, %v", secret, result)
}
secret = diff
secret = result.MappedItemDiff
}
}

Expand Down
Loading

0 comments on commit 9dbbe57

Please sign in to comment.