Skip to content

Commit

Permalink
Accept many tfplan files in a single submit-plan invocation
Browse files Browse the repository at this point in the history
This is necessary for example when using terragrunt with multiple modules.

Fixes #55
  • Loading branch information
DavidS-ovm committed Aug 25, 2023
1 parent 5119446 commit 1b8f466
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 30 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Upload a terraform plan to overmind for Blast Radius Analysis:

```
terraform show -json ./tfplan > ./tfplan.json
ovm-cli submit-plan --title "example change" --plan-json ./tfplan.json
ovm-cli submit-plan --title "example change" ./tfplan1.json ./tfplan2.json ./tfplan3.json
```

## Terraform ➡ Overmind Mapping
Expand All @@ -66,4 +66,4 @@ output "overmind_mappings" {

Valid mapping values are:

* `cluster_name`: The name of the cluster that was provided to the kubernetes source using the `source.clusterName` option
* `cluster_name`: The name of the cluster that was provided to the kubernetes source using the `source.clusterName` option
8 changes: 8 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,11 @@ func initConfig() {
viper.SetEnvKeyReplacer(replacer)
viper.AutomaticEnv() // read in environment variables that match
}

// must panics if the passed in error is not nil
// use this for init-time error checking of viper/cobra stuff that sometimes errors if the flag does not exist
func must(err error) {
if err != nil {
panic(fmt.Errorf("error initialising: %w", err))
}
}
61 changes: 42 additions & 19 deletions cmd/submitplan.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,20 @@ import (

// submitPlanCmd represents the submit-plan command
var submitPlanCmd = &cobra.Command{
Use: "submit-plan [--title TITLE] [--description DESCRIPTION] [--ticket-link URL] [--plan-json FILE]",
Use: "submit-plan [--title TITLE] [--description DESCRIPTION] [--ticket-link URL] FILE [FILE ...]",
Short: "Creates a new Change from a given terraform plan file",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("no plan files specified")
}
for _, f := range args {
_, err := os.Stat(f)
if err != nil {
return err
}
}
return nil
},
PreRun: func(cmd *cobra.Command, args []string) {
// Bind these to viper
err := viper.BindPFlags(cmd.Flags())
Expand All @@ -42,7 +54,10 @@ var submitPlanCmd = &cobra.Command{

signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

exitcode := SubmitPlan(sigs, nil)
if viper.GetString("plan-json") != "" {
args = append(args, viper.GetString("plan-json"))
}
exitcode := SubmitPlan(sigs, args, nil)
tracing.ShutdownTracer()
os.Exit(exitcode)
},
Expand All @@ -54,14 +69,22 @@ type TfData struct {
Values map[string]any
}

func changingItemQueriesFromPlan(ctx context.Context, planJSON []byte, lf log.Fields) ([]*sdp.Query, error) {
func changingItemQueriesFromPlan(ctx context.Context, fileName string, lf log.Fields) ([]*sdp.Query, error) {
var changing_items []*sdp.Query

// 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 file")
return changing_items, err
}

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

var changing_items []*sdp.Query
// for all managed resources:
for _, resourceChange := range plan.ResourceChanges {
if len(resourceChange.Change.Actions) == 0 || resourceChange.Change.Actions[0] == "no-op" {
Expand Down Expand Up @@ -186,7 +209,7 @@ func changingItemQueriesFromPlan(ctx context.Context, planJSON []byte, lf log.Fi
return changing_items, nil
}

func SubmitPlan(signals chan os.Signal, ready chan bool) int {
func SubmitPlan(signals chan os.Signal, files []string, ready chan bool) int {
timeout, err := time.ParseDuration(viper.GetString("timeout"))
if err != nil {
log.Errorf("invalid --timeout value '%v', error: %v", viper.GetString("timeout"), err)
Expand Down Expand Up @@ -216,19 +239,18 @@ func SubmitPlan(signals chan os.Signal, ready chan bool) int {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

// read results from `terraform show -json ${tfplan file}`
contents, err := os.ReadFile(viper.GetString("plan-json"))
if err != nil {
log.WithContext(ctx).WithError(err).WithFields(lf).Error("failed to read terraform file")
return 1
}

log.WithContext(ctx).WithFields(lf).Info("resolving items from terraform plan")
queries, err := changingItemQueriesFromPlan(ctx, contents, lf)
if err != nil {
log.WithContext(ctx).WithError(err).WithFields(lf).Error("parse terraform plan")
return 1
queries := []*sdp.Query{}
for _, f := range files {
lf["file"] = f
log.WithContext(ctx).WithFields(lf).Info("resolving items from terraform plan")
q, err := changingItemQueriesFromPlan(ctx, f, lf)
if err != nil {
log.WithContext(ctx).WithError(err).WithFields(lf).Error("parse terraform plan")
return 1
}
queries = append(queries, q...)
}
delete(lf, "file")

client := AuthenticatedChangesClient(ctx)
changeUuid, err := getChangeUuid(ctx, sdp.ChangeStatus_CHANGE_STATUS_DEFINING, false)
Expand Down Expand Up @@ -527,6 +549,7 @@ func init() {
submitPlanCmd.PersistentFlags().String("frontend", "https://app.overmind.tech", "The frontend base URL")

submitPlanCmd.PersistentFlags().String("plan-json", "./tfplan.json", "Parse changing items from this terraform plan JSON file. Generate this using 'terraform show -json PLAN_FILE'")
must(submitPlanCmd.PersistentFlags().MarkHidden("plan-json")) // better suited by using `args`

submitPlanCmd.PersistentFlags().String("title", "", "Short title for this change.")
submitPlanCmd.PersistentFlags().String("description", "", "Quick description of the change.")
Expand Down
10 changes: 1 addition & 9 deletions cmd/submitplan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,13 @@ package cmd

import (
"context"
"os"
"testing"

"github.com/sirupsen/logrus"
)

func TestChangingItemQueriesFromPlan(t *testing.T) {
testFile := "testdata/plan.json"
planJSON, err := os.ReadFile(testFile)

if err != nil {
t.Errorf("Error reading %v: %v", testFile, err)
}

queries, err := changingItemQueriesFromPlan(context.Background(), planJSON, logrus.Fields{})
queries, err := changingItemQueriesFromPlan(context.Background(), "testdata/plan.json", logrus.Fields{})

if err != nil {
t.Error(err)
Expand Down

0 comments on commit 1b8f466

Please sign in to comment.