Skip to content

Commit

Permalink
Implement plan parsing and uploading
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidS-ovm committed Feb 29, 2024
1 parent 2392ffb commit 09898a8
Show file tree
Hide file tree
Showing 8 changed files with 373 additions and 219 deletions.
2 changes: 1 addition & 1 deletion cmd/changes_end_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func EndChange(ctx context.Context, ready chan bool) int {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_HAPPENING, true)
changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_HAPPENING, viper.GetString("ticket-link"), true)
if err != nil {
log.WithError(err).WithFields(lf).Error("failed to identify change")
return 1
Expand Down
2 changes: 1 addition & 1 deletion cmd/changes_get_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func GetChange(ctx context.Context, ready chan bool) int {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus(sdp.ChangeStatus_value[viper.GetString("status")]), true)
changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus(sdp.ChangeStatus_value[viper.GetString("status")]), viper.GetString("ticket-link"), true)
if err != nil {
log.WithError(err).WithFields(lf).Error("failed to identify change")
return 1
Expand Down
2 changes: 1 addition & 1 deletion cmd/changes_manual_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func ManualChange(ctx context.Context, ready chan bool) int {
defer cancel()

client := AuthenticatedChangesClient(ctx, oi)
changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, false)
changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, viper.GetString("ticket-link"),false)
if err != nil {
log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to searching for existing changes")
return 1
Expand Down
2 changes: 1 addition & 1 deletion cmd/changes_start_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func StartChange(ctx context.Context, ready chan bool) int {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, true)
changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING,viper.GetString("ticket-link"), true)
if err != nil {
log.WithError(err).WithFields(lf).Error("failed to identify change")
return 1
Expand Down
214 changes: 4 additions & 210 deletions cmd/changes_submit_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@ import (
"connectrpc.com/connect"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/overmindtech/cli/cmd/datamaps"
"github.com/overmindtech/cli/tracing"
"github.com/overmindtech/sdp-go"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"google.golang.org/protobuf/types/known/timestamppb"
)

// submitPlanCmd represents the submit-plan command
Expand Down Expand Up @@ -300,219 +298,15 @@ func isStateFile(bytes []byte) bool {
return false
}

func mappedItemDiffsFromPlan(ctx context.Context, fileName string, lf log.Fields) ([]*sdp.MappedItemDiff, error) {
func mappedItemDiffsFromPlanFile(ctx context.Context, fileName string, lf log.Fields) ([]*sdp.MappedItemDiff, 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
}

// 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)
}

plannedChangeGroupsVar := plannedChangeGroups{
supported: map[string][]*sdp.MappedItemDiff{},
unsupported: map[string][]*sdp.MappedItemDiff{},
}

// 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)
}

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")
plannedChangeGroupsVar.Add(resourceChange.Type, &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("Skipping resource without query field")
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()
}
}

plannedChangeGroupsVar.Add(resourceChange.Type, &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")
}
}

supported := ""
numSupported := plannedChangeGroupsVar.NumSupportedChanges()
if numSupported > 0 {
supported = Green.Color(fmt.Sprintf("%v supported", numSupported))
}

unsupported := ""
numUnsupported := plannedChangeGroupsVar.NumUnsupportedChanges()
if numUnsupported > 0 {
unsupported = Yellow.Color(fmt.Sprintf("%v unsupported", numUnsupported))
}

numTotalChanges := numSupported + numUnsupported

switch numTotalChanges {
case 0:
log.WithContext(ctx).Infof("Plan (%v) contained no changing resources.", fileName)
case 1:
log.WithContext(ctx).Infof("Plan (%v) contained one changing resource: %v %v", fileName, supported, unsupported)
default:
log.WithContext(ctx).Infof("Plan (%v) contained %v changing resources: %v %v", fileName, numTotalChanges, supported, unsupported)
}

// Log the types
for typ, plannedChanges := range plannedChangeGroupsVar.supported {
log.WithContext(ctx).Infof(Green.Color(" ✓ %v (%v)"), typ, len(plannedChanges))
}
for typ, plannedChanges := range plannedChangeGroupsVar.unsupported {
log.WithContext(ctx).Infof(Yellow.Color(" ✗ %v (%v)"), typ, len(plannedChanges))
}

return plannedChangeGroupsVar.MappedItemDiffs(), nil
return mappedItemDiffsFromPlan(ctx, planJSON, fileName, lf)
}

// Returns the name of the provider from the config key. If the resource isn't
Expand Down Expand Up @@ -610,7 +404,7 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int {

for _, f := range files {
lf["file"] = f
mappedItemDiffs, err := mappedItemDiffsFromPlan(ctx, f, lf)
mappedItemDiffs, err := mappedItemDiffsFromPlanFile(ctx, f, lf)
if err != nil {
log.WithContext(ctx).WithError(err).WithFields(lf).Error("Error parsing terraform plan")
return 1
Expand All @@ -620,7 +414,7 @@ func SubmitPlan(ctx context.Context, files []string, ready chan bool) int {
delete(lf, "file")

client := AuthenticatedChangesClient(ctx, oi)
changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, false)
changeUuid, err := getChangeUuid(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, viper.GetString("ticket-link"), false)
if err != nil {
log.WithContext(ctx).WithError(err).WithFields(lf).Error("Failed searching for existing changes")
return 1
Expand Down
4 changes: 2 additions & 2 deletions cmd/changes_submit_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import (
)

func TestWithStateFile(t *testing.T) {
_, err := mappedItemDiffsFromPlan(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")
}
}

func TestMappedItemDiffsFromPlan(t *testing.T) {
mappedItemDiffs, err := mappedItemDiffsFromPlan(context.Background(), "testdata/plan.json", logrus.Fields{})
mappedItemDiffs, err := mappedItemDiffsFromPlanFile(context.Background(), "testdata/plan.json", logrus.Fields{})

if err != nil {
t.Error(err)
Expand Down
3 changes: 1 addition & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,13 +462,12 @@ func HasScopesFlexible(claims *sdp.CustomClaims, requiredScopes []string) (bool,
}

// getChangeUuid returns the UUID of a change, as selected by --uuid or --change, or a state with the specified status and having --ticket-link
func getChangeUuid(ctx context.Context, oi OvermindInstance, expectedStatus sdp.ChangeStatus, errNotFound bool) (uuid.UUID, error) {
func getChangeUuid(ctx context.Context, oi OvermindInstance, expectedStatus sdp.ChangeStatus, ticketLink string, errNotFound bool) (uuid.UUID, error) {
var changeUuid uuid.UUID
var err error

uuidString := viper.GetString("uuid")
changeUrlString := viper.GetString("change")
ticketLink := viper.GetString("ticket-link")

// If no arguments are specified then return an error
if uuidString == "" && changeUrlString == "" && ticketLink == "" {
Expand Down
Loading

0 comments on commit 09898a8

Please sign in to comment.