Skip to content

Commit 29da1c4

Browse files
authored
fix: Fixing backend delete safety features (#4089)
* fix: Fixing visible subcommands and flags * fix: Fixing implementation for subcommands, as that was fine * fix: Fixing backend safety feature for `delete` * fix: Adding feature flag to control versioning * feat: Adding integration test for force requirement * fix: Adding GCP check too * fix: Plumbing ctx through for GCS bucket provisioning
1 parent 5bdb539 commit 29da1c4

10 files changed

Lines changed: 114 additions & 8 deletions

File tree

cli/commands/backend/delete/cli.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import (
1111
const (
1212
CommandName = "delete"
1313

14-
BucketFlagName = "bucket"
14+
BucketFlagName = "bucket"
15+
ForceBackendDeleteFlagName = "force"
1516
)
1617

1718
func NewFlags(cmdOpts *Options, prefix flags.Prefix) cli.Flags {
@@ -25,6 +26,12 @@ func NewFlags(cmdOpts *Options, prefix flags.Prefix) cli.Flags {
2526
Hidden: true,
2627
Destination: &cmdOpts.DeleteBucket,
2728
}),
29+
flags.NewFlag(&cli.BoolFlag{
30+
Name: ForceBackendDeleteFlagName,
31+
EnvVars: tgPrefix.EnvVars(ForceBackendDeleteFlagName),
32+
Usage: "Force the backend to be deleted, even if the bucket is not versioned.",
33+
Destination: &cmdOpts.ForceBackendDelete,
34+
}),
2835
}
2936

3037
return append(flags, run.NewFlags(cmdOpts.TerragruntOptions, nil).Filter(run.ConfigFlagName, run.DownloadDirFlagName)...)

internal/remotestate/backend/gcs/backend.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"fmt"
77

8+
"github.com/gruntwork-io/terragrunt/internal/errors"
89
"github.com/gruntwork-io/terragrunt/internal/remotestate/backend"
910
"github.com/gruntwork-io/terragrunt/options"
1011
"github.com/gruntwork-io/terragrunt/shell"
@@ -108,7 +109,7 @@ func (backend *Backend) Init(ctx context.Context, backendConfig backend.Config,
108109
// If bucket is specified and skip_bucket_versioning is false then warn user if versioning is disabled on bucket
109110
if !extGCSCfg.SkipBucketVersioning && bucketName != "" {
110111
// TODO: Remove lint suppression
111-
if err := client.CheckIfGCSVersioningEnabled(bucketName); err != nil { //nolint:contextcheck
112+
if _, err := client.CheckIfGCSVersioningEnabled(ctx, bucketName); err != nil { //nolint:contextcheck
112113
return err
113114
}
114115
}
@@ -130,6 +131,17 @@ func (backend *Backend) Delete(ctx context.Context, backendConfig backend.Config
130131
return err
131132
}
132133

134+
if !opts.ForceBackendDelete {
135+
versioned, err := client.CheckIfGCSVersioningEnabled(ctx, extGCSCfg.RemoteStateConfigGCS.Bucket)
136+
if err != nil {
137+
return err
138+
}
139+
140+
if !versioned {
141+
return errors.New("bucket is not versioned, refusing to delete backend state. If you are sure you want to delete the backend state anyways, use the --force flag")
142+
}
143+
}
144+
133145
var (
134146
bucketName = extGCSCfg.RemoteStateConfigGCS.Bucket
135147
prefix = extGCSCfg.RemoteStateConfigGCS.Prefix

internal/remotestate/backend/gcs/client.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,21 +156,20 @@ func (client *Client) CreateGCSBucketIfNecessary(ctx context.Context, bucketName
156156
}
157157

158158
// CheckIfGCSVersioningEnabled checks if versioning is enabled for the GCS bucket specified in the given config and warn the user if it is not
159-
func (client *Client) CheckIfGCSVersioningEnabled(bucketName string) error {
160-
ctx := context.Background()
159+
func (client *Client) CheckIfGCSVersioningEnabled(ctx context.Context, bucketName string) (bool, error) {
161160
bucket := client.Bucket(bucketName)
162161

163162
attrs, err := bucket.Attrs(ctx)
164163
if err != nil {
165164
// ErrBucketNotExist
166-
return errors.New(err)
165+
return false, errors.New(err)
167166
}
168167

169168
if !attrs.VersioningEnabled {
170-
client.logger.Warnf("Versioning is not enabled for the remote state GCS bucket %s. We recommend enabling versioning so that you can roll back to previous versions of your Terraform state in case of error.", bucketName)
169+
client.logger.Warnf("Versioning is not enabled for the remote state GCS bucket %s. We recommend enabling versioning so that you can roll back to previous versions of your OpenTofu/Terraform state in case of error.", bucketName)
171170
}
172171

173-
return nil
172+
return attrs.VersioningEnabled, nil
174173
}
175174

176175
// CreateGCSBucketWithVersioning creates the given GCS bucket and enables versioning for it.

internal/remotestate/backend/s3/backend.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"path"
88

9+
"github.com/gruntwork-io/terragrunt/internal/errors"
910
"github.com/gruntwork-io/terragrunt/internal/remotestate/backend"
1011
"github.com/gruntwork-io/terragrunt/options"
1112
"github.com/gruntwork-io/terragrunt/shell"
@@ -138,6 +139,17 @@ func (backend *Backend) Delete(ctx context.Context, backendConfig backend.Config
138139
return err
139140
}
140141

142+
if !opts.ForceBackendDelete {
143+
versioned, err := client.CheckIfVersioningEnabled(ctx, extS3Cfg.RemoteStateConfigS3.Bucket)
144+
if err != nil {
145+
return err
146+
}
147+
148+
if !versioned {
149+
return errors.New("bucket is not versioned, refusing to delete backend state. If you are sure you want to delete the backend state anyways, use the --force flag")
150+
}
151+
}
152+
141153
var (
142154
bucketName = extS3Cfg.RemoteStateConfigS3.Bucket
143155
bucketKey = extS3Cfg.RemoteStateConfigS3.Key

internal/remotestate/backend/s3/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ func (client *Client) CheckIfVersioningEnabled(ctx context.Context, bucketName s
416416
// NOTE: There must be a bug in the AWS SDK since res == nil when versioning is not enabled. In the future,
417417
// check the AWS SDK for updates to see if we can remove "res == nil ||".
418418
if res == nil || res.Status == nil || *res.Status != s3.BucketVersioningStatusEnabled {
419-
client.logger.Warnf("Versioning is not enabled for the remote state S3 bucket %s. We recommend enabling versioning so that you can roll back to previous versions of your Terraform state in case of error.", bucketName)
419+
client.logger.Warnf("Versioning is not enabled for the remote state S3 bucket %s. We recommend enabling versioning so that you can roll back to previous versions of your OpenTofu/Terraform state in case of error.", bucketName)
420420
return false, nil
421421
}
422422

options/options.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ type TerragruntOptions struct {
287287
Graph bool
288288
// BackendBootstrap automatically bootstraps backend infrastructure before attempting to use it.
289289
BackendBootstrap bool
290+
// ForceBackendDelete forces the backend to be deleted, even if the bucket is not versioned.
291+
ForceBackendDelete bool
290292
}
291293

292294
// TerragruntOptionsFunc is a functional option type used to pass options in certain integration tests

test/fixtures/bootstrap-gcs-backend/common.hcl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
feature "disable_versioning" {
2+
default = false
3+
}
4+
15
remote_state {
26
backend = "gcs"
37

@@ -11,5 +15,7 @@ remote_state {
1115
location = "__FILL_IN_LOCATION__"
1216
project = "__FILL_IN_PROJECT__"
1317
bucket = "__FILL_IN_BUCKET_NAME__"
18+
19+
skip_bucket_versioning = feature.disable_versioning.value
1420
}
1521
}

test/fixtures/bootstrap-s3-backend/common.hcl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
feature "disable_versioning" {
2+
default = false
3+
}
4+
15
remote_state {
26
backend = "s3"
37
generate = {
@@ -9,5 +13,7 @@ remote_state {
913
bucket = "__FILL_IN_BUCKET_NAME__"
1014
region = "__FILL_IN_REGION__"
1115
dynamodb_table = "__FILL_IN_LOCK_TABLE_NAME__"
16+
17+
skip_bucket_versioning = feature.disable_versioning.value
1218
}
1319
}

test/integration_aws_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,39 @@ func TestAwsBootstrapBackend(t *testing.T) {
125125
}
126126
}
127127

128+
func TestAwsBootstrapBackendWithoutVersioning(t *testing.T) {
129+
t.Parallel()
130+
131+
helpers.CleanupTerraformFolder(t, testFixtureBootstrapS3Backend)
132+
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureBootstrapS3Backend)
133+
rootPath := util.JoinPath(tmpEnvPath, testFixtureBootstrapS3Backend)
134+
135+
testID := strings.ToLower(helpers.UniqueID())
136+
137+
s3BucketName := "terragrunt-test-bucket-" + testID
138+
dynamoDBName := "terragrunt-test-dynamodb-" + testID
139+
140+
defer func() {
141+
deleteS3Bucket(t, s3BucketName, helpers.TerraformRemoteStateS3Region)
142+
cleanupTableForTest(t, dynamoDBName, helpers.TerraformRemoteStateS3Region)
143+
}()
144+
145+
commonConfigPath := util.JoinPath(rootPath, "common.hcl")
146+
helpers.CopyTerragruntConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, s3BucketName, dynamoDBName, helpers.TerraformRemoteStateS3Region)
147+
148+
_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt run --all --non-interactive --log-level debug --strict-control require-explicit-bootstrap --experiment cli-redesign --working-dir "+rootPath+" --feature disable_versioning=true --backend-bootstrap apply")
149+
require.NoError(t, err)
150+
151+
validateS3BucketExistsAndIsTagged(t, helpers.TerraformRemoteStateS3Region, s3BucketName, nil)
152+
validateDynamoDBTableExistsAndIsTagged(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, nil)
153+
154+
_, _, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt --non-interactive --log-level debug --strict-control require-explicit-bootstrap --experiment cli-redesign --working-dir "+rootPath+" --feature disable_versioning=true backend delete --all")
155+
require.Error(t, err)
156+
157+
_, _, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt --non-interactive --log-level debug --strict-control require-explicit-bootstrap --experiment cli-redesign --working-dir "+rootPath+" --feature disable_versioning=true backend delete --all --force")
158+
require.NoError(t, err)
159+
}
160+
128161
func TestAwsDeleteBackend(t *testing.T) {
129162
t.Parallel()
130163

test/integration_gcp_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,35 @@ func TestGcpBootstrapBackend(t *testing.T) {
9696
}
9797
}
9898

99+
func TestGcpBootstrapBackendWithoutVersioning(t *testing.T) {
100+
t.Parallel()
101+
102+
helpers.CleanupTerraformFolder(t, testFixtureBootstrapGCSBackend)
103+
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureBootstrapGCSBackend)
104+
rootPath := util.JoinPath(tmpEnvPath, testFixtureBootstrapGCSBackend)
105+
106+
gcsBucketName := "terragrunt-test-bucket-" + strings.ToLower(helpers.UniqueID())
107+
108+
defer func() {
109+
deleteGCSBucket(t, gcsBucketName)
110+
}()
111+
112+
project := os.Getenv("GOOGLE_CLOUD_PROJECT")
113+
commonConfigPath := util.JoinPath(rootPath, "common.hcl")
114+
copyTerragruntGCSConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, project, terraformRemoteStateGcpRegion, gcsBucketName)
115+
116+
_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt run --all --non-interactive --log-level debug --strict-control require-explicit-bootstrap --experiment cli-redesign --working-dir "+rootPath+" --feature disable_versioning=true --backend-bootstrap apply")
117+
require.NoError(t, err)
118+
119+
validateGCSBucketExistsAndIsLabeled(t, terraformRemoteStateGcpRegion, gcsBucketName, nil)
120+
121+
_, _, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt --non-interactive --log-level debug --strict-control require-explicit-bootstrap --experiment cli-redesign --working-dir "+rootPath+" --feature disable_versioning=true backend delete --all")
122+
require.Error(t, err)
123+
124+
_, _, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt --non-interactive --log-level debug --strict-control require-explicit-bootstrap --experiment cli-redesign --working-dir "+rootPath+" --feature disable_versioning=true backend delete --all --force")
125+
require.NoError(t, err)
126+
}
127+
99128
func TestGcpDeleteBackend(t *testing.T) {
100129
t.Parallel()
101130

0 commit comments

Comments
 (0)