Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Terraform plan result handling #5177

Merged
merged 1 commit into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions pkg/app/piped/platformprovider/terraform/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,20 +154,24 @@ func (t *Terraform) SelectWorkspace(ctx context.Context, workspace string) error
}

type PlanResult struct {
Adds int
Changes int
Destroys int
Imports int
Adds int
Changes int
Destroys int
Imports int
HasStateChanges bool

PlanOutput string
}

func (r PlanResult) NoChanges() bool {
return r.Adds == 0 && r.Changes == 0 && r.Destroys == 0 && r.Imports == 0
return r.Adds == 0 && r.Changes == 0 && r.Destroys == 0 && r.Imports == 0 && !r.HasStateChanges
}

func (r PlanResult) Render() (string, error) {
terraformDiffStart := "Terraform will perform the following actions:"
if !strings.Contains(r.PlanOutput, terraformDiffStart) {
return "", nil
}
startIndex := strings.Index(r.PlanOutput, terraformDiffStart) + len(terraformDiffStart)

terraformDiffEnd := fmt.Sprintf("Plan: %d to import, %d to add, %d to change, %d to destroy.", r.Imports, r.Adds, r.Changes, r.Destroys)
Expand Down Expand Up @@ -319,8 +323,8 @@ func (t *Terraform) makeCommonCommandArgs() (args []string) {
var (
// Import block was introduced from Terraform v1.5.0.
// Keep this regex for backward compatibility.
planHasChangeRegex = regexp.MustCompile(`(?m)^Plan:(?: (\d+) to import,)?? (\d+) to add, (\d+) to change, (\d+) to destroy.$`)
planNoChangesRegex = regexp.MustCompile(`(?m)^No changes. Infrastructure is up-to-date.$`)
planHasChangeRegex = regexp.MustCompile(`(?m)^Plan:(?: (\d+) to import,)?? (\d+) to add, (\d+) to change, (\d+) to destroy\.$`)
planHasOutputsRegex = regexp.MustCompile(`(?m)^Changes to Outputs:$`)
)

// Borrowed from https://github.com/acarl005/stripansi
Expand Down Expand Up @@ -369,17 +373,17 @@ func parsePlanResult(out string, ansiIncluded bool) (PlanResult, error) {
imports, adds, changes, destroys, err := parseNums(s[1:]...)
if err == nil {
return PlanResult{
Adds: adds,
Changes: changes,
Destroys: destroys,
Imports: imports,
PlanOutput: out,
Adds: adds,
Changes: changes,
Destroys: destroys,
Imports: imports,
HasStateChanges: true,
PlanOutput: out,
}, nil
}
}

if s := planNoChangesRegex.FindStringSubmatch(out); len(s) > 0 {
return PlanResult{}, nil
if planHasOutputsRegex.MatchString(out) {
return PlanResult{HasStateChanges: true, PlanOutput: out}, nil
}

return PlanResult{}, fmt.Errorf("unable to parse plan output")
Expand Down
154 changes: 150 additions & 4 deletions pkg/app/piped/platformprovider/terraform/terraform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ func TestParsePlanResult(t *testing.T) {
{
name: "older than v1.5.0",
input: `Plan: 1 to add, 2 to change, 3 to destroy.`,
expected: PlanResult{Adds: 1, Changes: 2, Destroys: 3, PlanOutput: "Plan: 1 to add, 2 to change, 3 to destroy."},
expected: PlanResult{Adds: 1, Changes: 2, Destroys: 3, HasStateChanges: true},
expectedErr: false,
},
{
name: "later than v1.5.0",
input: `Plan: 1 to import, 1 to add, 2 to change, 3 to destroy.`,
expected: PlanResult{Imports: 1, Adds: 1, Changes: 2, Destroys: 3, PlanOutput: "Plan: 1 to import, 1 to add, 2 to change, 3 to destroy."},
expected: PlanResult{Imports: 1, Adds: 1, Changes: 2, Destroys: 3, HasStateChanges: true},
expectedErr: false,
},
{
Expand All @@ -81,8 +81,106 @@ func TestParsePlanResult(t *testing.T) {
expectedErr: true,
},
{
name: "No changes",
input: `No changes. Infrastructure is up-to-date.`,
name: "Changes to outputs",
input: `terraform init -no-color
Initializing the backend...

Successfully configured the backend "gcs"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Finding hashicorp/google versions matching "x.xx.x"...
- Installing hashicorp/google vx.xx.x...
- Installed hashicorp/google vx.xx.x (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform plan -lock=false -detailed-exitcode -no-colorgoogle_compute_global_address.xxx: Refreshing state... [id=projects/xxxx/global/addresses/xxxx]
google_service_account.xxxxx: Refreshing state... [id=projects/xxxx/serviceAccounts/xxxxx@xxxxx.iam.gserviceaccount.com]
google_compute_global_address.xxxx: Refreshing state... [id=projects/xxxx/global/addresses/xxxxxx]
google_dns_record_set.xxxxx: Refreshing state... [id=xxxxx/A]

Changes to Outputs:
+ global_address = xxxx

You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.`,
expected: PlanResult{HasStateChanges: true},
expectedErr: false,
},
{
name: "Refactor", // when using moved blocks or removed blocks
input: `terraform init -no-color
Initializing the backend...

Successfully configured the backend "gcs"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Finding hashicorp/google versions matching "x.xx.x"...
- Installing hashicorp/google vx.xx.x...
- Installed hashicorp/google vx.xx.x (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform plan -lock=false -detailed-exitcode -no-colorgoogle_compute_global_address.xxx: Refreshing state... [id=projects/xxxx/global/addresses/xxxx]
google_service_account.xxxxx: Refreshing state... [id=projects/xxxx/serviceAccounts/xxxxx@xxxxx.iam.gserviceaccount.com]
google_compute_global_address.xxxx: Refreshing state... [id=projects/xxxx/global/addresses/xxxxxx]
google_dns_record_set.xxxxx: Refreshing state... [id=xxxxx/A]

Terraform will perform the following actions:

# google_dns_record_set.xxx has moved to google_dns_record_set.xxx
resource "google_compute_global_forwarding_rule" "xxx" {
id = "xxxx"
managed_zone = "xxxx"
name = "xxxx.xxxx.xxxx."
# (4 unchanged attributes hidden)
}

# google_compute_global_address.xxx will no longer be managed by Terraform, but will not be destroyed
# (destroy = false is set in the configuration)
. resource "google_compute_global_address" "xxx" {
id = "xxxx"
name = "xxxx"
# (5 unchanged attributes hidden)
}

Plan: 0 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.`,
expected: PlanResult{HasStateChanges: true},
expectedErr: false,
},
}
Expand All @@ -93,6 +191,7 @@ func TestParsePlanResult(t *testing.T) {
t.Parallel()
result, err := parsePlanResult(tc.input, false)
assert.Equal(t, tc.expectedErr, err != nil)
result.PlanOutput = ""
assert.Equal(t, tc.expected, result)
})
}
Expand Down Expand Up @@ -139,6 +238,53 @@ Plan: 1 to import, 2 to add, 3 to change, 4 to destroy.
`,
expectedErr: false,
},
{
name: "New outputs",
planResult: &PlanResult{
HasStateChanges: true,
PlanOutput: `terraform init -no-color
Initializing the backend...

Successfully configured the backend "gcs"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Finding hashicorp/google versions matching "x.xx.x"...
- Installing hashicorp/google vx.xx.x...
- Installed hashicorp/google vx.xx.x (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform plan -lock=false -detailed-exitcode -no-colorgoogle_compute_global_address.xxx: Refreshing state... [id=projects/xxxx/global/addresses/xxxx]
google_service_account.xxxxx: Refreshing state... [id=projects/xxxx/serviceAccounts/xxxxx@xxxxx.iam.gserviceaccount.com]
google_compute_global_address.xxxx: Refreshing state... [id=projects/xxxx/global/addresses/xxxxxx]
google_dns_record_set.xxxxx: Refreshing state... [id=xxxxx/A]

Changes to Outputs:
+ global_address = xxxx

You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.`,
},
expected: "",
},
}

for _, tc := range testcases {
Expand Down
4 changes: 3 additions & 1 deletion tool/actions-plan-preview/planpreview.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ const (

// Terraform plan format
prefixTerraformPlan = "Terraform will perform the following actions:"
// Terraform changes to outputs format
prefixTerraformChangesToOutput = "Changes to Outputs:"
)

func makeCommentBody(event *githubEvent, r *PlanPreviewResult) string {
Expand Down Expand Up @@ -320,7 +322,7 @@ func generateTerraformShortPlanDetails(details string) (string, error) {
// NOTE: scanner.Scan() return false if the buffer size of one line exceed bufio.MaxScanTokenSize(65536 byte).
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, prefixTerraformPlan) {
if strings.Contains(line, prefixTerraformPlan) || strings.Contains(line, prefixTerraformChangesToOutput) {
start = length
break
}
Expand Down
118 changes: 118 additions & 0 deletions tool/actions-plan-preview/planpreview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,124 @@ guarantee to take exactly these actions if you run "terraform apply" now.
2 to add, 1 to change, 0 to destroy`,
wantErr: false,
},
{
name: "moved resources",
planDetails: `terraform init -no-color
Initializing the backend...

Successfully configured the backend "gcs"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Finding hashicorp/google versions matching "x.xx.x"...
- Installing hashicorp/google vx.xx.x...
- Installed hashicorp/google vx.xx.x (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform plan -lock=false -detailed-exitcode -no-colorgoogle_compute_global_address.xxx: Refreshing state... [id=projects/xxxx/global/addresses/xxxx]
google_service_account.xxxxx: Refreshing state... [id=projects/xxxx/serviceAccounts/xxxxx@xxxxx.iam.gserviceaccount.com]
google_compute_global_address.xxxx: Refreshing state... [id=projects/xxxx/global/addresses/xxxxxx]
google_dns_record_set.xxxxx: Refreshing state... [id=xxxxx/A]

Terraform will perform the following actions:

# google_dns_record_set.xxx has moved to google_dns_record_set.xxx
resource "google_compute_global_forwarding_rule" "xxx" {
id = "xxxx"
managed_zone = "xxxx"
name = "xxxx.xxxx.xxxx."
# (4 unchanged attributes hidden)
}

Plan: 0 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.`,
want: `Terraform will perform the following actions:

# google_dns_record_set.xxx has moved to google_dns_record_set.xxx
resource "google_compute_global_forwarding_rule" "xxx" {
id = "xxxx"
managed_zone = "xxxx"
name = "xxxx.xxxx.xxxx."
# (4 unchanged attributes hidden)
}

Plan: 0 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.`,
wantErr: false,
},
{
name: "Only new outputs",
planDetails: `terraform init -no-color
Initializing the backend...

Successfully configured the backend "gcs"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Finding hashicorp/google versions matching "x.xx.x"...
- Installing hashicorp/google vx.xx.x...
- Installed hashicorp/google vx.xx.x (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform plan -lock=false -detailed-exitcode -no-colorgoogle_compute_global_address.xxx: Refreshing state... [id=projects/xxxx/global/addresses/xxxx]
google_service_account.xxxxx: Refreshing state... [id=projects/xxxx/serviceAccounts/xxxxx@xxxxx.iam.gserviceaccount.com]
google_compute_global_address.xxxx: Refreshing state... [id=projects/xxxx/global/addresses/xxxxxx]
google_dns_record_set.xxxxx: Refreshing state... [id=xxxxx/A]

Changes to Outputs:
+ global_address = xxxx

You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.`,
want: `Changes to Outputs:
+ global_address = xxxx

You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.`,
wantErr: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
Loading