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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy