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 {