diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e6833423..221457913e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ BUG FIXES: - When assigning an empty map to a variable that is declared as a map of an object type with at least one optional attribute, OpenTofu will no longer create a subtly-broken value. ([#2371](https://github.com/opentofu/opentofu/pull/2371)) - The `format` and `formatlist` functions can now accept `null` as one of the arguments without causing problems during the apply phase. Previously these functions would incorrectly return an unknown value when given `null` and so could cause a failure during the apply phase where no unknown values are allowed. ([#2371](https://github.com/opentofu/opentofu/pull/2371)) - Provider used in import is correctly identified. ([#2336](https://github.com/opentofu/opentofu/pull/2336)) +- `plantimestamp()` now returns unknown value during validation ([#2397](https://github.com/opentofu/opentofu/issues/2397)) ## Previous Releases diff --git a/internal/lang/funcs/datetime.go b/internal/lang/funcs/datetime.go index 2f3b44ea3f..51c732999d 100644 --- a/internal/lang/funcs/datetime.go +++ b/internal/lang/funcs/datetime.go @@ -30,6 +30,11 @@ func MakeStaticTimestampFunc(static time.Time) function.Function { Params: []function.Parameter{}, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + // During validation phase, the planTimestamp is zero. By returning unknown value, it's forcing the + // HCL parser to skip any evaluation of other expressions that could use this. + if static.IsZero() { + return cty.UnknownVal(cty.String), nil + } return cty.StringVal(static.Format(time.RFC3339)), nil }, }) diff --git a/internal/lang/funcs/datetime_test.go b/internal/lang/funcs/datetime_test.go index 9f2813eb02..2149ec2a63 100644 --- a/internal/lang/funcs/datetime_test.go +++ b/internal/lang/funcs/datetime_test.go @@ -185,3 +185,49 @@ func TestTimeCmp(t *testing.T) { }) } } + +func TestMakeStaticTimestampFunc(t *testing.T) { + tests := []struct { + Name string + // Setup made like this to bind the generated time value to the wanted value. + Setup func() (time.Time, cty.Value) + }{ + { + Name: "zero", + Setup: func() (time.Time, cty.Value) { + in := time.Time{} + out := cty.UnknownVal(cty.String) + return in, out + }, + }, + { + Name: "now", + Setup: func() (time.Time, cty.Value) { + in := time.Now() + out := cty.StringVal(in.Format(time.RFC3339)) + return in, out + }, + }, + { + Name: "one year later", + Setup: func() (time.Time, cty.Value) { + in := time.Now().Add(8766 * time.Hour) // 1 year later + out := cty.StringVal(in.Format(time.RFC3339)) + return in, out + }, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("MakeStaticTimestampFunc(%s)", test.Name), func(t *testing.T) { + in, want := test.Setup() + got, err := MakeStaticTimestampFunc(in).Call(nil) + if err != nil { + t.Fatalf("MakeStaticTimestampFunc is not meant to return error but got one: %v", err) + } + if !got.RawEquals(want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + } +} diff --git a/internal/tofu/context_validate_test.go b/internal/tofu/context_validate_test.go index 4786d2f830..fdc2067647 100644 --- a/internal/tofu/context_validate_test.go +++ b/internal/tofu/context_validate_test.go @@ -2488,3 +2488,21 @@ locals { t.Fatalf("expected deprecated warning, got: %q\n", warn) } } + +// Ensure that the plantimestamp() call is not affecting the validation step. +func TestContext2Validate_rangeOverZeroPlanTimestamp(t *testing.T) { + p := testProvider("test") + + m := testModule(t, "plan_range_over_plan_timestamp") + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(context.Background(), m) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } +} diff --git a/internal/tofu/testdata/plan_range_over_plan_timestamp/main.tf b/internal/tofu/testdata/plan_range_over_plan_timestamp/main.tf new file mode 100644 index 0000000000..b4aad009bf --- /dev/null +++ b/internal/tofu/testdata/plan_range_over_plan_timestamp/main.tf @@ -0,0 +1,12 @@ +locals { + first_year = 2024 + current_timestamp = plantimestamp() +} + +output "table_years" { + value = toset( + [ + for year in range(local.first_year, tonumber(formatdate("YYYY", local.current_timestamp)) + 2) : tostring(year) + ] + ) +}