mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
Add ephemeralasnull() function (#3154)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com> Co-authored-by: Martin Atkins <mart@degeneration.co.uk> Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
@@ -177,6 +177,10 @@ var DescriptionList = map[string]descriptionEntry{
|
||||
Description: "`endswith` takes two values: a string to check and a suffix string. The function returns true if the first string ends with that exact suffix.",
|
||||
ParamDescription: []string{"", ""},
|
||||
},
|
||||
"ephemeralasnull": {
|
||||
Description: "`ephemeralasnull` replaces any ephemeral values in the given value with a null value of the corresponding type.",
|
||||
ParamDescription: []string{""},
|
||||
},
|
||||
"file": {
|
||||
Description: "`file` reads the contents of a file at the given path and returns them as a string.",
|
||||
ParamDescription: []string{""},
|
||||
|
||||
63
internal/lang/funcs/ephemeral.go
Normal file
63
internal/lang/funcs/ephemeral.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package funcs
|
||||
|
||||
import (
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
var EphemeralAsNullFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "value",
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowUnknown: true,
|
||||
AllowNull: true,
|
||||
AllowMarked: true,
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
},
|
||||
Type: func(args []cty.Value) (cty.Type, error) {
|
||||
// The result type is always the same as the argument type.
|
||||
return args[0].Type(), nil
|
||||
},
|
||||
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
|
||||
return cty.Transform(args[0], func(_ cty.Path, val cty.Value) (cty.Value, error) {
|
||||
ty := val.Type()
|
||||
|
||||
// Preserve non-ephemeral marks
|
||||
nonEphemeralMarks := val.Marks()
|
||||
delete(nonEphemeralMarks, marks.Ephemeral)
|
||||
|
||||
switch {
|
||||
case val.IsNull():
|
||||
return cty.NullVal(ty).WithMarks(nonEphemeralMarks), nil
|
||||
case !val.IsKnown():
|
||||
// This mirrors the logic in IsSensitive()
|
||||
//
|
||||
// When a value is unknown its ephemerality is also not yet
|
||||
// finalized, because authors can write expressions where the
|
||||
// ephemerality of the result is decided based on some other
|
||||
// value that isn't yet known itself. As a simple example:
|
||||
// var.unknown_bool ? var.some_ephemeral_value : "b"
|
||||
//
|
||||
// Therefore we must report that we can't predict whether an
|
||||
// unknown value will be ephemeral or not. For more information,
|
||||
// refer to https://github.com/opentofu/opentofu/issues/2415
|
||||
|
||||
return cty.UnknownVal(val.Type()).WithMarks(nonEphemeralMarks), nil
|
||||
case val.HasMark(marks.Ephemeral):
|
||||
// This whole value is marked as ephemeral and should be null
|
||||
return cty.NullVal(val.Type()).WithMarks(nonEphemeralMarks), nil
|
||||
default:
|
||||
// Not marked
|
||||
return val, nil
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
128
internal/lang/funcs/ephemeral_test.go
Normal file
128
internal/lang/funcs/ephemeral_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package funcs
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestEphemeralAsNullFunc(t *testing.T) {
|
||||
// This is a bit overkill, but it is better to be explicit
|
||||
cases := []struct {
|
||||
Name string
|
||||
Input cty.Value
|
||||
Expected cty.Value
|
||||
}{
|
||||
{"null",
|
||||
cty.NullVal(cty.String),
|
||||
cty.NullVal(cty.String)},
|
||||
{"null_sensitive",
|
||||
cty.NullVal(cty.String).Mark(marks.Sensitive),
|
||||
cty.NullVal(cty.String).Mark(marks.Sensitive)},
|
||||
{"null_ephemeral",
|
||||
cty.NullVal(cty.String).Mark(marks.Ephemeral),
|
||||
cty.NullVal(cty.String)},
|
||||
{"null_complex",
|
||||
cty.NullVal(cty.String).Mark(marks.Ephemeral).Mark(marks.Sensitive),
|
||||
cty.NullVal(cty.String).Mark(marks.Sensitive)},
|
||||
{"unknown",
|
||||
cty.UnknownVal(cty.String),
|
||||
cty.UnknownVal(cty.String)},
|
||||
{"unknown_sensitive",
|
||||
cty.UnknownVal(cty.String).Mark(marks.Sensitive),
|
||||
cty.UnknownVal(cty.String).Mark(marks.Sensitive)},
|
||||
{"unknown_ephemeral",
|
||||
cty.UnknownVal(cty.String).Mark(marks.Ephemeral),
|
||||
cty.UnknownVal(cty.String)},
|
||||
{"unknown_complex",
|
||||
cty.UnknownVal(cty.String).Mark(marks.Ephemeral).Mark(marks.Sensitive),
|
||||
cty.UnknownVal(cty.String).Mark(marks.Sensitive)},
|
||||
{"primitive",
|
||||
cty.StringVal("myprimitive"),
|
||||
cty.StringVal("myprimitive")},
|
||||
{"primitive_sensitive",
|
||||
cty.StringVal("mysensitive").Mark(marks.Sensitive),
|
||||
cty.StringVal("mysensitive").Mark(marks.Sensitive)},
|
||||
{"primitive_ephemeral",
|
||||
cty.StringVal("myephemeral").Mark(marks.Ephemeral),
|
||||
cty.NullVal(cty.String)},
|
||||
{"list",
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val")}),
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val")})},
|
||||
{"list_sensitive",
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val")}).Mark(marks.Sensitive),
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val")}).Mark(marks.Sensitive)},
|
||||
{"list_ephemeral",
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val")}).Mark(marks.Ephemeral),
|
||||
cty.NullVal(cty.List(cty.String))},
|
||||
{"list_complex",
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val")}).Mark(marks.Ephemeral).Mark(marks.Sensitive),
|
||||
cty.NullVal(cty.List(cty.String)).Mark(marks.Sensitive)},
|
||||
{"listcontents_sensitive",
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val").Mark(marks.Sensitive)}),
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val").Mark(marks.Sensitive)})},
|
||||
{"listcontents_ephemeral",
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val").Mark(marks.Ephemeral)}),
|
||||
cty.ListVal([]cty.Value{cty.NullVal(cty.String)})},
|
||||
{"listcontents_complex",
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val").Mark(marks.Ephemeral).Mark(marks.Sensitive)}),
|
||||
cty.ListVal([]cty.Value{cty.NullVal(cty.String).Mark(marks.Sensitive)})},
|
||||
{"listcontents_multiple",
|
||||
cty.ListVal([]cty.Value{cty.StringVal("val").Mark(marks.Ephemeral).Mark(marks.Sensitive), cty.StringVal("other")}),
|
||||
cty.ListVal([]cty.Value{cty.NullVal(cty.String).Mark(marks.Sensitive), cty.StringVal("other")})},
|
||||
{"listempty",
|
||||
cty.ListValEmpty(cty.String),
|
||||
cty.ListValEmpty(cty.String)},
|
||||
{"listempty_sensitive",
|
||||
cty.ListValEmpty(cty.String).Mark(marks.Sensitive),
|
||||
cty.ListValEmpty(cty.String).Mark(marks.Sensitive)},
|
||||
{"listempty_ephemeral",
|
||||
cty.ListValEmpty(cty.String).Mark(marks.Ephemeral),
|
||||
cty.NullVal(cty.List(cty.String))},
|
||||
{"listempty_complex",
|
||||
cty.ListValEmpty(cty.String).Mark(marks.Ephemeral).Mark(marks.Sensitive),
|
||||
cty.NullVal(cty.List(cty.String)).Mark(marks.Sensitive)},
|
||||
{"map",
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.StringVal("val")}),
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.StringVal("val")})},
|
||||
{"map_sensitive",
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.StringVal("val")}).Mark(marks.Sensitive),
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.StringVal("val")}).Mark(marks.Sensitive)},
|
||||
{"map_ephemeral",
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.StringVal("val")}).Mark(marks.Ephemeral),
|
||||
cty.NullVal(cty.Map(cty.String))},
|
||||
{"map_complex",
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.StringVal("val")}).Mark(marks.Ephemeral).Mark(marks.Sensitive),
|
||||
cty.NullVal(cty.Map(cty.String)).Mark(marks.Sensitive)},
|
||||
{"mapcontents_sensitive",
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.StringVal("val").Mark(marks.Sensitive)}),
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.StringVal("val").Mark(marks.Sensitive)})},
|
||||
{"mapcontents_ephemeral",
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.StringVal("val").Mark(marks.Ephemeral)}),
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.NullVal(cty.String)})},
|
||||
{"mapcontents_complex",
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.StringVal("val").Mark(marks.Ephemeral).Mark(marks.Sensitive)}),
|
||||
cty.MapVal(map[string]cty.Value{"key": cty.NullVal(cty.String).Mark(marks.Sensitive)})},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
out, err := EphemeralAsNullFunc.Call([]cty.Value{tc.Input})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tc.Expected, out) {
|
||||
t.Fatalf("Expected %#v, Got %#v", tc.Expected, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -146,6 +146,7 @@ func makeBaseFunctionTable(baseDir string) map[string]function.Function {
|
||||
"distinct": stdlib.DistinctFunc,
|
||||
"element": stdlib.ElementFunc,
|
||||
"endswith": funcs.EndsWithFunc,
|
||||
"ephemeralasnull": funcs.EphemeralAsNullFunc,
|
||||
"chunklist": stdlib.ChunklistFunc,
|
||||
"file": funcs.MakeFileFunc(baseDir, false),
|
||||
"fileexists": funcs.MakeFileExistsFunc(baseDir),
|
||||
|
||||
@@ -376,7 +376,13 @@ func TestFunctions(t *testing.T) {
|
||||
cty.False,
|
||||
},
|
||||
},
|
||||
|
||||
"ephemeralasnull": {
|
||||
{
|
||||
// We have more specific tests in the funcs package
|
||||
`ephemeralasnull("foo")`,
|
||||
cty.StringVal("foo"),
|
||||
},
|
||||
},
|
||||
"file": {
|
||||
{
|
||||
`file("hello.txt")`,
|
||||
|
||||
30
website/docs/language/functions/ephemeralasnull.mdx
Normal file
30
website/docs/language/functions/ephemeralasnull.mdx
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
sidebar_label: ephemeralasnull
|
||||
description: The ephemeral as null function replaces all ephemeral values within the given object with null.
|
||||
---
|
||||
|
||||
# `ephemeralasnull` Function
|
||||
|
||||
`ephemeralasnull` takes any value and returns a copy of it with any ephemeral values replaced with null. Ephemeral
|
||||
values can come from variables and outputs marked as ephemeral, as well as ephemeral resources.
|
||||
|
||||
## Examples
|
||||
|
||||
```hcl
|
||||
> ephemeralasnull("Hello World")
|
||||
"Hello World"
|
||||
> ephemeralasnull(var.ephemeral_variable)
|
||||
null
|
||||
> ephemeralasnull(ephemeral.some_resource)
|
||||
null
|
||||
> ephemeralasnull([4, var.ephemeral_variable])
|
||||
[4, null]
|
||||
> ephemeralasnull({
|
||||
"field": "value",
|
||||
"ephemeral": var.ephemeral_variable,
|
||||
})
|
||||
{
|
||||
"field": "value",
|
||||
"ephemeral": null,
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user