From 85e3d06fc8f4a468c069c332a2cfaeb2c5e963cd Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 19 May 2022 16:53:15 +0000 Subject: [PATCH 1/2] feat: skip terraform destroy if there is no state when deleting --- provisioner/terraform/provision.go | 92 +++++++++++++++++++++---- provisioner/terraform/provision_test.go | 47 +++++++++++++ 2 files changed, 124 insertions(+), 15 deletions(-) diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index ecc493b991c56..4cb7523bcfc23 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "runtime" "strings" @@ -22,6 +23,13 @@ import ( "github.com/coder/coder/provisionersdk/proto" ) +var ( + // noStateRegex is matched against the output from `terraform state show` + noStateRegex = regexp.MustCompile(`(?i)State read error.*no state`) + + noStateError = xerrors.New("no state") +) + // Provision executes `terraform apply`. func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) error { shutdown, shutdownFunc := context.WithCancel(stream.Context()) @@ -190,6 +198,41 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro } }() + // If we're destroying, exit early if there's no state. + if start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY { + _, err := getTerraformState(shutdown, terraform, statefilePath) + if xerrors.Is(err, noStateError) { + _ = stream.Send(&proto.Provision_Response{ + Type: &proto.Provision_Response_Log{ + Log: &proto.Log{ + Level: proto.LogLevel_INFO, + Output: "The terraform state does not exist, there is nothing to do", + }, + }, + }) + + return stream.Send(&proto.Provision_Response{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Error: "", + }, + }, + }) + } + if err != nil { + err = xerrors.Errorf("get terraform state: %w", err) + _ = stream.Send(&proto.Provision_Response{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Error: err.Error(), + }, + }, + }) + + return err + } + } + planfilePath := filepath.Join(start.Directory, "terraform.tfplan") var args []string if start.DryRun { @@ -378,23 +421,11 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state _, 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("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) + state, err := getTerraformState(ctx, terraform, statefilePath) if err != nil { - return nil, xerrors.Errorf("show terraform state: %w", err) + return nil, xerrors.Errorf("get terraform state: %w", err) } + resources := make([]*proto.Resource, 0) if state.Values != nil { rawGraph, err := terraform.Graph(ctx) @@ -557,6 +588,37 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state }, nil } +// getTerraformState pulls and merges any remote terraform state into the given +// path and reads the merged state. If there is no state, `noStateError` will be +// returned. +func getTerraformState(ctx context.Context, terraform *tfexec.Terraform, statefilePath string) (*tfjson.State, error) { + statefile, err := os.OpenFile(statefilePath, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + 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 { + if noStateRegex.MatchString(err.Error()) { + return nil, noStateError + } + + return nil, xerrors.Errorf("show terraform state: %w", err) + } + + return state, nil +} + type terraformProvisionLog struct { Level string `json:"@level"` Message string `json:"@message"` diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index 6169f8e5f0bc4..af5995cd68fda 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "sort" + "strings" "testing" "github.com/stretchr/testify/require" @@ -509,4 +510,50 @@ provider "coder" { } }) } + + t.Run("DestroyNoState", func(t *testing.T) { + t.Parallel() + + const template = `resource "null_resource" "A" {}` + + directory := t.TempDir() + err := os.WriteFile(filepath.Join(directory, "main.tf"), []byte(template), 0600) + require.NoError(t, err) + + request := &proto.Provision_Request{ + Type: &proto.Provision_Request_Start{ + Start: &proto.Provision_Start{ + State: nil, + Directory: directory, + Metadata: &proto.Provision_Metadata{ + WorkspaceTransition: proto.WorkspaceTransition_DESTROY, + }, + }, + }, + } + + response, err := api.Provision(ctx) + require.NoError(t, err) + err = response.Send(request) + require.NoError(t, err) + + gotLog := false + for { + msg, err := response.Recv() + require.NoError(t, err) + require.NotNil(t, msg) + + if msg.GetLog() != nil && strings.Contains(msg.GetLog().Output, "nothing to do") { + gotLog = true + continue + } + if msg.GetComplete() == nil { + continue + } + + require.Empty(t, msg.GetComplete().Error) + require.True(t, gotLog, "never received 'nothing to do' log") + break + } + }) } From aecb3bedf7189f212eed35e587950dcf997c2a9f Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 19 May 2022 18:30:59 +0000 Subject: [PATCH 2/2] chore: code review --- provisioner/terraform/provision.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 4cb7523bcfc23..8c1c96d55e57a 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -26,8 +26,6 @@ import ( var ( // noStateRegex is matched against the output from `terraform state show` noStateRegex = regexp.MustCompile(`(?i)State read error.*no state`) - - noStateError = xerrors.New("no state") ) // Provision executes `terraform apply`. @@ -198,10 +196,14 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro } }() - // If we're destroying, exit early if there's no state. + // If we're destroying, exit early if there's no state. This is necessary to + // avoid any cases where a workspace is "locked out" of terraform due to + // e.g. bad template param values and cannot be deleted. This is just for + // contingency, in the future we will try harder to prevent workspaces being + // broken this hard. if start.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY { _, err := getTerraformState(shutdown, terraform, statefilePath) - if xerrors.Is(err, noStateError) { + if xerrors.Is(err, os.ErrNotExist) { _ = stream.Send(&proto.Provision_Response{ Type: &proto.Provision_Response_Log{ Log: &proto.Log{ @@ -213,9 +215,7 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro return stream.Send(&proto.Provision_Response{ Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Error: "", - }, + Complete: &proto.Provision_Complete{}, }, }) } @@ -589,8 +589,8 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state } // getTerraformState pulls and merges any remote terraform state into the given -// path and reads the merged state. If there is no state, `noStateError` will be -// returned. +// path and reads the merged state. If there is no state, `os.ErrNotExist` will +// be returned. func getTerraformState(ctx context.Context, terraform *tfexec.Terraform, statefilePath string) (*tfjson.State, error) { statefile, err := os.OpenFile(statefilePath, os.O_CREATE|os.O_RDWR, 0600) if err != nil { @@ -610,7 +610,7 @@ func getTerraformState(ctx context.Context, terraform *tfexec.Terraform, statefi state, err := terraform.ShowStateFile(ctx, statefilePath) if err != nil { if noStateRegex.MatchString(err.Error()) { - return nil, noStateError + return nil, os.ErrNotExist } return nil, xerrors.Errorf("show terraform state: %w", err) 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