From b2ad1eb3da2d2ff8357706fe15e51d6e2901e898 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Mon, 2 May 2022 19:04:17 +0000 Subject: [PATCH] fix: Use "terraform state pull" instead of "terraform show" Although the terraform-exec docs don't indicate this, the result of "terraform show" isn't actually the state... it's a trimmed version of the state that excludes resource identifiers, essentially removing all state that did exist. Tests will be written to ensure Terraform state reconciliation can occur. This will happen in another PR, as dogfood is currently broken because of this. --- provisioner/terraform/provision.go | 53 +++++++++++++++++-------- provisioner/terraform/provision_test.go | 3 -- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 4217f1e3901e8..4e9865e829344 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -66,6 +66,14 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro return xerrors.Errorf("terraform version %q is too old. required >= %q", version.String(), minimumTerraformVersion.String()) } + statefilePath := filepath.Join(start.Directory, "terraform.tfstate") + if len(start.State) > 0 { + err := os.WriteFile(statefilePath, start.State, 0600) + if err != nil { + return xerrors.Errorf("write statefile %q: %w", statefilePath, err) + } + } + reader, writer := io.Pipe() defer reader.Close() defer writer.Close() @@ -239,14 +247,7 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro errorMessage := err.Error() // Terraform can fail and apply and still need to store it's state. // In this case, we return Complete with an explicit error message. - state, err := terraform.Show(stream.Context()) - if err != nil { - return xerrors.Errorf("show state: %w", err) - } - stateData, err := json.Marshal(state) - if err != nil { - return xerrors.Errorf("marshal state: %w", err) - } + stateData, _ := os.ReadFile(statefilePath) return stream.Send(&proto.Provision_Response{ Type: &proto.Provision_Response_Complete{ Complete: &proto.Provision_Complete{ @@ -263,7 +264,7 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro if start.DryRun { resp, err = parseTerraformPlan(stream.Context(), terraform, planfilePath) } else { - resp, err = parseTerraformApply(stream.Context(), terraform) + resp, err = parseTerraformApply(stream.Context(), terraform, statefilePath) } if err != nil { return err @@ -363,10 +364,26 @@ func parseTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, planfi }, nil } -func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform) (*proto.Provision_Response, error) { - state, err := terraform.Show(ctx) +func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, statefilePath string) (*proto.Provision_Response, error) { + _, err := os.Stat(statefilePath) + statefileExisted := err == nil + + statefile, err := os.OpenFile(statefilePath, os.O_CREATE|os.O_RDWR, 0600) if err != nil { - return nil, xerrors.Errorf("show state file: %w", err) + return nil, xerrors.Errorf("open statefile %q: %w", statefilePath, err) + } + defer statefile.Close() + // #nosec + cmd := exec.CommandContext(ctx, terraform.ExecPath(), "state", "pull") + cmd.Dir = terraform.WorkingDir() + cmd.Stdout = statefile + err = cmd.Run() + if err != nil { + return nil, xerrors.Errorf("pull terraform state: %w", err) + } + state, err := terraform.ShowStateFile(ctx, statefilePath) + if err != nil { + return nil, xerrors.Errorf("show terraform state: %w", err) } resources := make([]*proto.Resource, 0) if state.Values != nil { @@ -501,15 +518,19 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform) (*pro } } - statefileContent, err := json.Marshal(state) - if err != nil { - return nil, xerrors.Errorf("marshal state: %w", err) + var stateContent []byte + // We only want to restore state if it's not hosted remotely. + if statefileExisted { + stateContent, err = os.ReadFile(statefilePath) + if err != nil { + return nil, xerrors.Errorf("read statefile %q: %w", statefilePath, err) + } } return &proto.Provision_Response{ Type: &proto.Provision_Response_Complete{ Complete: &proto.Provision_Complete{ - State: statefileContent, + State: stateContent, Resources: resources, }, }, diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index d3682a932a23e..6169f8e5f0bc4 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -480,9 +480,6 @@ provider "coder" { } require.NoError(t, err) - if !request.GetStart().DryRun { - require.Greater(t, len(msg.GetComplete().State), 0) - } // Remove randomly generated data. for _, resource := range msg.GetComplete().Resources {