Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit adb7d20

Browse files
authored
feat: skip terraform destroy if there is no state when deleting (#1594)
1 parent a03615a commit adb7d20

File tree

2 files changed

+124
-15
lines changed

2 files changed

+124
-15
lines changed

provisioner/terraform/provision.go

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"os/exec"
1111
"path/filepath"
12+
"regexp"
1213
"runtime"
1314
"strings"
1415

@@ -22,6 +23,11 @@ import (
2223
"github.com/coder/coder/provisionersdk/proto"
2324
)
2425

26+
var (
27+
// noStateRegex is matched against the output from `terraform state show`
28+
noStateRegex = regexp.MustCompile(`(?i)State read error.*no state`)
29+
)
30+
2531
// Provision executes `terraform apply`.
2632
func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
2733
shutdown, shutdownFunc := context.WithCancel(stream.Context())
@@ -190,6 +196,43 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro
190196
}
191197
}()
192198

199+
// If we're destroying, exit early if there's no state. This is necessary to
200+
// avoid any cases where a workspace is "locked out" of terraform due to
201+
// e.g. bad template param values and cannot be deleted. This is just for
202+
// contingency, in the future we will try harder to prevent workspaces being
203+
// broken this hard.
204+
if start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY {
205+
_, err := getTerraformState(shutdown, terraform, statefilePath)
206+
if xerrors.Is(err, os.ErrNotExist) {
207+
_ = stream.Send(&proto.Provision_Response{
208+
Type: &proto.Provision_Response_Log{
209+
Log: &proto.Log{
210+
Level: proto.LogLevel_INFO,
211+
Output: "The terraform state does not exist, there is nothing to do",
212+
},
213+
},
214+
})
215+
216+
return stream.Send(&proto.Provision_Response{
217+
Type: &proto.Provision_Response_Complete{
218+
Complete: &proto.Provision_Complete{},
219+
},
220+
})
221+
}
222+
if err != nil {
223+
err = xerrors.Errorf("get terraform state: %w", err)
224+
_ = stream.Send(&proto.Provision_Response{
225+
Type: &proto.Provision_Response_Complete{
226+
Complete: &proto.Provision_Complete{
227+
Error: err.Error(),
228+
},
229+
},
230+
})
231+
232+
return err
233+
}
234+
}
235+
193236
planfilePath := filepath.Join(start.Directory, "terraform.tfplan")
194237
var args []string
195238
if start.DryRun {
@@ -378,23 +421,11 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state
378421
_, err := os.Stat(statefilePath)
379422
statefileExisted := err == nil
380423

381-
statefile, err := os.OpenFile(statefilePath, os.O_CREATE|os.O_RDWR, 0600)
382-
if err != nil {
383-
return nil, xerrors.Errorf("open statefile %q: %w", statefilePath, err)
384-
}
385-
defer statefile.Close()
386-
// #nosec
387-
cmd := exec.CommandContext(ctx, terraform.ExecPath(), "state", "pull")
388-
cmd.Dir = terraform.WorkingDir()
389-
cmd.Stdout = statefile
390-
err = cmd.Run()
391-
if err != nil {
392-
return nil, xerrors.Errorf("pull terraform state: %w", err)
393-
}
394-
state, err := terraform.ShowStateFile(ctx, statefilePath)
424+
state, err := getTerraformState(ctx, terraform, statefilePath)
395425
if err != nil {
396-
return nil, xerrors.Errorf("show terraform state: %w", err)
426+
return nil, xerrors.Errorf("get terraform state: %w", err)
397427
}
428+
398429
resources := make([]*proto.Resource, 0)
399430
if state.Values != nil {
400431
rawGraph, err := terraform.Graph(ctx)
@@ -557,6 +588,37 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state
557588
}, nil
558589
}
559590

591+
// getTerraformState pulls and merges any remote terraform state into the given
592+
// path and reads the merged state. If there is no state, `os.ErrNotExist` will
593+
// be returned.
594+
func getTerraformState(ctx context.Context, terraform *tfexec.Terraform, statefilePath string) (*tfjson.State, error) {
595+
statefile, err := os.OpenFile(statefilePath, os.O_CREATE|os.O_RDWR, 0600)
596+
if err != nil {
597+
return nil, xerrors.Errorf("open statefile %q: %w", statefilePath, err)
598+
}
599+
defer statefile.Close()
600+
601+
// #nosec
602+
cmd := exec.CommandContext(ctx, terraform.ExecPath(), "state", "pull")
603+
cmd.Dir = terraform.WorkingDir()
604+
cmd.Stdout = statefile
605+
err = cmd.Run()
606+
if err != nil {
607+
return nil, xerrors.Errorf("pull terraform state: %w", err)
608+
}
609+
610+
state, err := terraform.ShowStateFile(ctx, statefilePath)
611+
if err != nil {
612+
if noStateRegex.MatchString(err.Error()) {
613+
return nil, os.ErrNotExist
614+
}
615+
616+
return nil, xerrors.Errorf("show terraform state: %w", err)
617+
}
618+
619+
return state, nil
620+
}
621+
560622
type terraformProvisionLog struct {
561623
Level string `json:"@level"`
562624
Message string `json:"@message"`

provisioner/terraform/provision_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"path/filepath"
1010
"sort"
11+
"strings"
1112
"testing"
1213

1314
"github.com/stretchr/testify/require"
@@ -509,4 +510,50 @@ provider "coder" {
509510
}
510511
})
511512
}
513+
514+
t.Run("DestroyNoState", func(t *testing.T) {
515+
t.Parallel()
516+
517+
const template = `resource "null_resource" "A" {}`
518+
519+
directory := t.TempDir()
520+
err := os.WriteFile(filepath.Join(directory, "main.tf"), []byte(template), 0600)
521+
require.NoError(t, err)
522+
523+
request := &proto.Provision_Request{
524+
Type: &proto.Provision_Request_Start{
525+
Start: &proto.Provision_Start{
526+
State: nil,
527+
Directory: directory,
528+
Metadata: &proto.Provision_Metadata{
529+
WorkspaceTransition: proto.WorkspaceTransition_DESTROY,
530+
},
531+
},
532+
},
533+
}
534+
535+
response, err := api.Provision(ctx)
536+
require.NoError(t, err)
537+
err = response.Send(request)
538+
require.NoError(t, err)
539+
540+
gotLog := false
541+
for {
542+
msg, err := response.Recv()
543+
require.NoError(t, err)
544+
require.NotNil(t, msg)
545+
546+
if msg.GetLog() != nil && strings.Contains(msg.GetLog().Output, "nothing to do") {
547+
gotLog = true
548+
continue
549+
}
550+
if msg.GetComplete() == nil {
551+
continue
552+
}
553+
554+
require.Empty(t, msg.GetComplete().Error)
555+
require.True(t, gotLog, "never received 'nothing to do' log")
556+
break
557+
}
558+
})
512559
}

0 commit comments

Comments
 (0)