fix: Use "terraform state pull" instead of "terraform show" (#1262)

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.
This commit is contained in:
Kyle Carberry
2022-05-02 15:02:38 -05:00
committed by GitHub
parent fc642edf51
commit 43c6bff5ae
2 changed files with 37 additions and 19 deletions

View File

@ -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)
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("marshal state: %w", err)
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,
},
},

View File

@ -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 {