9
9
"os"
10
10
"os/exec"
11
11
"path/filepath"
12
+ "regexp"
12
13
"runtime"
13
14
"strings"
14
15
@@ -22,6 +23,11 @@ import (
22
23
"github.com/coder/coder/provisionersdk/proto"
23
24
)
24
25
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
+
25
31
// Provision executes `terraform apply`.
26
32
func (t * terraform ) Provision (stream proto.DRPCProvisioner_ProvisionStream ) error {
27
33
shutdown , shutdownFunc := context .WithCancel (stream .Context ())
@@ -190,6 +196,43 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro
190
196
}
191
197
}()
192
198
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
+
193
236
planfilePath := filepath .Join (start .Directory , "terraform.tfplan" )
194
237
var args []string
195
238
if start .DryRun {
@@ -378,23 +421,11 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state
378
421
_ , err := os .Stat (statefilePath )
379
422
statefileExisted := err == nil
380
423
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 )
395
425
if err != nil {
396
- return nil , xerrors .Errorf ("show terraform state: %w" , err )
426
+ return nil , xerrors .Errorf ("get terraform state: %w" , err )
397
427
}
428
+
398
429
resources := make ([]* proto.Resource , 0 )
399
430
if state .Values != nil {
400
431
rawGraph , err := terraform .Graph (ctx )
@@ -557,6 +588,37 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state
557
588
}, nil
558
589
}
559
590
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
+
560
622
type terraformProvisionLog struct {
561
623
Level string `json:"@level"`
562
624
Message string `json:"@message"`
0 commit comments