Skip to content

Commit a9c9728

Browse files
authored
tfexec: Add -allow-deferral experimental options to Plan and Apply commands (#447)
1 parent c07c678 commit a9c9728

File tree

11 files changed

+223
-19
lines changed

11 files changed

+223
-19
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
- resolve-versions
8181
- static-checks
8282
runs-on: ${{ matrix.os }}
83-
timeout-minutes: 10
83+
timeout-minutes: 20
8484
strategy:
8585
fail-fast: false
8686
matrix:

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 0.21.0 (Unreleased)
2+
3+
ENHANCEMENTS:
4+
- tfexec: Add `-allow-deferral` to `(Terraform).Apply()` and `(Terraform).Plan()` methods ([#447](https://github.com/hashicorp/terraform-exec/pull/447))
5+
16
# 0.20.0 (December 20, 2023)
27

38
ENHANCEMENTS:

tfexec/apply.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import (
1212
)
1313

1414
type applyConfig struct {
15-
backup string
16-
destroy bool
17-
dirOrPlan string
18-
lock bool
15+
allowDeferral bool
16+
backup string
17+
destroy bool
18+
dirOrPlan string
19+
lock bool
1920

2021
// LockTimeout must be a string with time unit, e.g. '10s'
2122
lockTimeout string
@@ -105,6 +106,10 @@ func (opt *DestroyFlagOption) configureApply(conf *applyConfig) {
105106
conf.destroy = opt.destroy
106107
}
107108

109+
func (opt *AllowDeferralOption) configureApply(conf *applyConfig) {
110+
conf.allowDeferral = opt.allowDeferral
111+
}
112+
108113
// Apply represents the terraform apply subcommand.
109114
func (tf *Terraform) Apply(ctx context.Context, opts ...ApplyOption) error {
110115
cmd, err := tf.applyCmd(ctx, opts...)
@@ -232,6 +237,22 @@ func (tf *Terraform) buildApplyArgs(ctx context.Context, c applyConfig) ([]strin
232237
}
233238
}
234239

240+
if c.allowDeferral {
241+
// Ensure the version is later than 1.9.0
242+
err := tf.compatible(ctx, tf1_9_0, nil)
243+
if err != nil {
244+
return nil, fmt.Errorf("-allow-deferral is an experimental option introduced in Terraform 1.9.0: %w", err)
245+
}
246+
247+
// Ensure the version has experiments enabled (alpha or dev builds)
248+
err = tf.experimentsEnabled(ctx)
249+
if err != nil {
250+
return nil, fmt.Errorf("-allow-deferral is only available in experimental Terraform builds: %w", err)
251+
}
252+
253+
args = append(args, "-allow-deferral")
254+
}
255+
235256
return args, nil
236257
}
237258

tfexec/apply_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,35 @@ func TestApplyJSONCmd(t *testing.T) {
150150
}, nil, applyCmd)
151151
})
152152
}
153+
154+
func TestApplyCmd_AllowDeferral(t *testing.T) {
155+
td := t.TempDir()
156+
157+
tf, err := NewTerraform(td, tfVersion(t, testutil.Alpha_v1_9))
158+
if err != nil {
159+
t.Fatal(err)
160+
}
161+
162+
// empty env, to avoid environ mismatch in testing
163+
tf.SetEnv(map[string]string{})
164+
165+
t.Run("allow deferrals during apply", func(t *testing.T) {
166+
applyCmd, err := tf.applyCmd(context.Background(),
167+
AllowDeferral(true),
168+
)
169+
if err != nil {
170+
t.Fatal(err)
171+
}
172+
173+
assertCmd(t, []string{
174+
"apply",
175+
"-no-color",
176+
"-auto-approve",
177+
"-input=false",
178+
"-lock=true",
179+
"-parallelism=10",
180+
"-refresh=true",
181+
"-allow-deferral",
182+
}, nil, applyCmd)
183+
})
184+
}

tfexec/force_unlock_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package tfexec
55

66
import (
77
"context"
8+
"runtime"
89
"testing"
910

1011
"github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
@@ -39,6 +40,10 @@ func TestForceUnlockCmd(t *testing.T) {
3940
// The optional final positional [DIR] argument is available
4041
// until v0.15.0.
4142
func TestForceUnlockCmd_pre015(t *testing.T) {
43+
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
44+
t.Skip("Terraform for darwin/arm64 is not available until v1")
45+
}
46+
4247
td := t.TempDir()
4348

4449
tf, err := NewTerraform(td, tfVersion(t, testutil.Latest014))

tfexec/internal/testutil/tfcache.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const (
2424
Latest_v1_1 = "1.1.9"
2525
Latest_v1_5 = "1.5.3"
2626
Latest_v1_6 = "1.6.0-alpha20230719"
27+
28+
Beta_v1_8 = "1.8.0-beta1"
29+
Alpha_v1_9 = "1.9.0-alpha20240404"
2730
)
2831

2932
const appendUserAgent = "tfexec-testutil"

tfexec/options.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ import (
77
"encoding/json"
88
)
99

10+
// AllowDeferralOption represents the -allow-deferral flag. This flag is only enabled in
11+
// experimental builds of Terraform. (alpha or built via source with experiments enabled)
12+
type AllowDeferralOption struct {
13+
allowDeferral bool
14+
}
15+
16+
// AllowDeferral represents the -allow-deferral flag. This flag is only enabled in
17+
// experimental builds of Terraform. (alpha or built via source with experiments enabled)
18+
func AllowDeferral(allowDeferral bool) *AllowDeferralOption {
19+
return &AllowDeferralOption{allowDeferral}
20+
}
21+
1022
// AllowMissingConfigOption represents the -allow-missing-config flag.
1123
type AllowMissingConfigOption struct {
1224
allowMissingConfig bool

tfexec/plan.go

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,21 @@ import (
1212
)
1313

1414
type planConfig struct {
15-
destroy bool
16-
dir string
17-
lock bool
18-
lockTimeout string
19-
out string
20-
parallelism int
21-
reattachInfo ReattachInfo
22-
refresh bool
23-
refreshOnly bool
24-
replaceAddrs []string
25-
state string
26-
targets []string
27-
vars []string
28-
varFiles []string
15+
allowDeferral bool
16+
destroy bool
17+
dir string
18+
lock bool
19+
lockTimeout string
20+
out string
21+
parallelism int
22+
reattachInfo ReattachInfo
23+
refresh bool
24+
refreshOnly bool
25+
replaceAddrs []string
26+
state string
27+
targets []string
28+
vars []string
29+
varFiles []string
2930
}
3031

3132
var defaultPlanOptions = planConfig{
@@ -97,6 +98,10 @@ func (opt *DestroyFlagOption) configurePlan(conf *planConfig) {
9798
conf.destroy = opt.destroy
9899
}
99100

101+
func (opt *AllowDeferralOption) configurePlan(conf *planConfig) {
102+
conf.allowDeferral = opt.allowDeferral
103+
}
104+
100105
// Plan executes `terraform plan` with the specified options and waits for it
101106
// to complete.
102107
//
@@ -243,6 +248,21 @@ func (tf *Terraform) buildPlanArgs(ctx context.Context, c planConfig) ([]string,
243248
args = append(args, "-var", v)
244249
}
245250
}
251+
if c.allowDeferral {
252+
// Ensure the version is later than 1.9.0
253+
err := tf.compatible(ctx, tf1_9_0, nil)
254+
if err != nil {
255+
return nil, fmt.Errorf("-allow-deferral is an experimental option introduced in Terraform 1.9.0: %w", err)
256+
}
257+
258+
// Ensure the version has experiments enabled (alpha or dev builds)
259+
err = tf.experimentsEnabled(ctx)
260+
if err != nil {
261+
return nil, fmt.Errorf("-allow-deferral is only available in experimental Terraform builds: %w", err)
262+
}
263+
264+
args = append(args, "-allow-deferral")
265+
}
246266

247267
return args, nil
248268
}

tfexec/plan_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,34 @@ func TestPlanJSONCmd(t *testing.T) {
178178
}, nil, planCmd)
179179
})
180180
}
181+
182+
func TestPlanCmd_AllowDeferral(t *testing.T) {
183+
td := t.TempDir()
184+
185+
tf, err := NewTerraform(td, tfVersion(t, testutil.Alpha_v1_9))
186+
if err != nil {
187+
t.Fatal(err)
188+
}
189+
190+
// empty env, to avoid environ mismatch in testing
191+
tf.SetEnv(map[string]string{})
192+
193+
t.Run("allow deferrals during plan", func(t *testing.T) {
194+
planCmd, err := tf.planCmd(context.Background(), AllowDeferral(true))
195+
if err != nil {
196+
t.Fatal(err)
197+
}
198+
199+
assertCmd(t, []string{
200+
"plan",
201+
"-no-color",
202+
"-input=false",
203+
"-detailed-exitcode",
204+
"-lock-timeout=0s",
205+
"-lock=true",
206+
"-parallelism=10",
207+
"-refresh=true",
208+
"-allow-deferral",
209+
}, nil, planCmd)
210+
})
211+
}

tfexec/version.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var (
3333
tf1_1_0 = version.Must(version.NewVersion("1.1.0"))
3434
tf1_4_0 = version.Must(version.NewVersion("1.4.0"))
3535
tf1_6_0 = version.Must(version.NewVersion("1.6.0"))
36+
tf1_9_0 = version.Must(version.NewVersion("1.9.0"))
3637
)
3738

3839
// Version returns structured output from the terraform version command including both the Terraform CLI version
@@ -180,6 +181,22 @@ func (tf *Terraform) compatible(ctx context.Context, minInclusive *version.Versi
180181
return nil
181182
}
182183

184+
// experimentsEnabled asserts the cached terraform version has experiments enabled in the executable,
185+
// and returns a well known error if not. Experiments are enabled in alpha and (potentially) dev builds of Terraform.
186+
func (tf *Terraform) experimentsEnabled(ctx context.Context) error {
187+
tfv, _, err := tf.Version(ctx, false)
188+
if err != nil {
189+
return err
190+
}
191+
192+
preRelease := tfv.Prerelease()
193+
if preRelease == "dev" || strings.Contains(preRelease, "alpha") {
194+
return nil
195+
}
196+
197+
return fmt.Errorf("experiments are not enabled in version %s, as it's not an alpha or dev build", errorVersionString(tfv))
198+
}
199+
183200
func stripPrereleaseAndMeta(v *version.Version) *version.Version {
184201
if v == nil {
185202
return nil

0 commit comments

Comments
 (0)