mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Functions: decode_tfvars, encode_tfvars, encode_expr on bult-in provider for compatibility (#2306)
Signed-off-by: Ilia Gogotchuri <ilia.gogotchuri0@gmail.com> Co-authored-by: Oleksandr Levchenkov <ollevche@gmail.com>
This commit is contained in:
parent
c5b43b9f1a
commit
2d9cef1f55
@ -4,6 +4,11 @@ UPGRADE NOTES:
|
|||||||
|
|
||||||
NEW FEATURES:
|
NEW FEATURES:
|
||||||
|
|
||||||
|
- New builtin provider functions added ([#2306](https://github.com/opentofu/opentofu/pull/2306)) :
|
||||||
|
- `provider::terraform::decode_tfvars` - Decode a TFVars file content into an object.
|
||||||
|
- `provider::terraform::encode_tfvars` - Encode an object into a string with the same format as a TFVars file.
|
||||||
|
- `provider::terraform::encode_expr` - Encode an arbitrary expression into a string with valid OpenTofu syntax.
|
||||||
|
|
||||||
ENHANCEMENTS:
|
ENHANCEMENTS:
|
||||||
|
|
||||||
BUG FIXES:
|
BUG FIXES:
|
||||||
|
@ -13,14 +13,27 @@ import (
|
|||||||
"github.com/opentofu/opentofu/internal/addrs"
|
"github.com/opentofu/opentofu/internal/addrs"
|
||||||
"github.com/opentofu/opentofu/internal/encryption"
|
"github.com/opentofu/opentofu/internal/encryption"
|
||||||
"github.com/opentofu/opentofu/internal/providers"
|
"github.com/opentofu/opentofu/internal/providers"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider is an implementation of providers.Interface
|
// Provider is an implementation of providers.Interface
|
||||||
type Provider struct{}
|
type Provider struct {
|
||||||
|
funcs map[string]providerFunc
|
||||||
|
}
|
||||||
|
|
||||||
// NewProvider returns a new tofu provider
|
// NewProvider returns a new tofu provider
|
||||||
func NewProvider() providers.Interface {
|
func NewProvider() providers.Interface {
|
||||||
return &Provider{}
|
return &Provider{
|
||||||
|
funcs: getProviderFuncs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) getFunctionSpecs() map[string]providers.FunctionSpec {
|
||||||
|
funcSpecs := make(map[string]providers.FunctionSpec)
|
||||||
|
for name, fn := range p.funcs {
|
||||||
|
funcSpecs[name] = fn.GetFunctionSpec()
|
||||||
|
}
|
||||||
|
return funcSpecs
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSchema returns the complete schema for the provider.
|
// GetSchema returns the complete schema for the provider.
|
||||||
@ -32,6 +45,7 @@ func (p *Provider) GetProviderSchema() providers.GetProviderSchemaResponse {
|
|||||||
ResourceTypes: map[string]providers.Schema{
|
ResourceTypes: map[string]providers.Schema{
|
||||||
"terraform_data": dataStoreResourceSchema(),
|
"terraform_data": dataStoreResourceSchema(),
|
||||||
},
|
},
|
||||||
|
Functions: p.getFunctionSpecs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,14 +175,48 @@ func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) GetFunctions() providers.GetFunctionsResponse {
|
func (p *Provider) GetFunctions() providers.GetFunctionsResponse {
|
||||||
panic("unimplemented - terraform provider has no functions")
|
return providers.GetFunctionsResponse{
|
||||||
|
Functions: p.getFunctionSpecs(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse {
|
func (p *Provider) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse {
|
||||||
panic("unimplemented - terraform provider has no functions")
|
fn, ok := p.funcs[r.Name]
|
||||||
|
if !ok {
|
||||||
|
return providers.CallFunctionResponse{
|
||||||
|
Error: fmt.Errorf("provider function %q not found", r.Name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v, err := fn.Call(r.Arguments)
|
||||||
|
return providers.CallFunctionResponse{
|
||||||
|
Result: v,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close is a noop for this provider, since it's run in-process.
|
// Close is a noop for this provider, since it's run in-process.
|
||||||
func (p *Provider) Close() error {
|
func (p *Provider) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// providerFunc is an interface representing a built-in provider function
|
||||||
|
type providerFunc interface {
|
||||||
|
// Name returns the name of the function which is used to call it
|
||||||
|
Name() string
|
||||||
|
// GetFunctionSpec returns the provider function specification
|
||||||
|
GetFunctionSpec() providers.FunctionSpec
|
||||||
|
// Call is used to invoke the function
|
||||||
|
Call(args []cty.Value) (cty.Value, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProviderFuncs returns a map of functions that are registered in the provider
|
||||||
|
func getProviderFuncs() map[string]providerFunc {
|
||||||
|
decodeTFVars := &decodeTFVarsFunc{}
|
||||||
|
encodeTFVars := &encodeTFVarsFunc{}
|
||||||
|
encodeExpr := &encodeExprFunc{}
|
||||||
|
return map[string]providerFunc{
|
||||||
|
decodeTFVars.Name(): decodeTFVars,
|
||||||
|
encodeTFVars.Name(): encodeTFVars,
|
||||||
|
encodeExpr.Name(): encodeExpr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
175
internal/builtin/providers/tf/provider_functions.go
Normal file
175
internal/builtin/providers/tf/provider_functions.go
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
// Copyright (c) The OpenTofu Authors
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
// Copyright (c) 2023 HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package tf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
|
"github.com/opentofu/opentofu/internal/providers"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// "decode_tfvars"
|
||||||
|
// "encode_tfvars"
|
||||||
|
// "encode_expr"
|
||||||
|
|
||||||
|
// decodeTFVarsFunc decodes a TFVars file content into a cty object
|
||||||
|
type decodeTFVarsFunc struct{}
|
||||||
|
|
||||||
|
func (f *decodeTFVarsFunc) Name() string {
|
||||||
|
return "decode_tfvars"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *decodeTFVarsFunc) GetFunctionSpec() providers.FunctionSpec {
|
||||||
|
params := []providers.FunctionParameterSpec{
|
||||||
|
{
|
||||||
|
Name: "content",
|
||||||
|
Type: cty.String,
|
||||||
|
Description: "TFVars file content to decode",
|
||||||
|
DescriptionFormat: providers.TextFormattingPlain,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return providers.FunctionSpec{
|
||||||
|
Parameters: params,
|
||||||
|
Return: cty.DynamicPseudoType,
|
||||||
|
Summary: "Decode a TFVars file content into an object",
|
||||||
|
Description: "provider::terraform::decode_tfvars decodes a TFVars file content into an object",
|
||||||
|
DescriptionFormat: providers.TextFormattingPlain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var FailedToDecodeError = errors.New("failed to decode tfvars content")
|
||||||
|
|
||||||
|
func wrapDiagErrors(m error, diag hcl.Diagnostics) error {
|
||||||
|
//Prepend the main error
|
||||||
|
errs := append([]error{m}, diag.Errs()...)
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *decodeTFVarsFunc) Call(args []cty.Value) (cty.Value, error) {
|
||||||
|
varsFileContent := args[0].AsString()
|
||||||
|
schema, diag := hclsyntax.ParseConfig([]byte(varsFileContent), "", hcl.Pos{Line: 0, Column: 0})
|
||||||
|
if schema == nil || diag.HasErrors() {
|
||||||
|
return cty.NullVal(cty.DynamicPseudoType), wrapDiagErrors(FailedToDecodeError, diag)
|
||||||
|
}
|
||||||
|
attrs, diag := schema.Body.JustAttributes()
|
||||||
|
// Check if there are any errors.
|
||||||
|
// attrs == nil does not mean that there are no attributes, attrs - is still initialized as an empty map
|
||||||
|
if attrs == nil || diag.HasErrors() {
|
||||||
|
return cty.NullVal(cty.DynamicPseudoType), wrapDiagErrors(FailedToDecodeError, diag)
|
||||||
|
}
|
||||||
|
vals := make(map[string]cty.Value)
|
||||||
|
for name, attr := range attrs {
|
||||||
|
val, diag := attr.Expr.Value(nil)
|
||||||
|
if diag.HasErrors() {
|
||||||
|
return cty.NullVal(cty.DynamicPseudoType), wrapDiagErrors(FailedToDecodeError, diag)
|
||||||
|
}
|
||||||
|
vals[name] = val
|
||||||
|
}
|
||||||
|
return cty.ObjectVal(vals), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeTFVarsFunc encodes an object into a string with the same format as a TFVars file
|
||||||
|
type encodeTFVarsFunc struct{}
|
||||||
|
|
||||||
|
func (f *encodeTFVarsFunc) Name() string {
|
||||||
|
return "encode_tfvars"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *encodeTFVarsFunc) GetFunctionSpec() providers.FunctionSpec {
|
||||||
|
params := []providers.FunctionParameterSpec{
|
||||||
|
{
|
||||||
|
Name: "input",
|
||||||
|
// The input type is determined at runtime
|
||||||
|
Type: cty.DynamicPseudoType,
|
||||||
|
Description: "Input to encode for TFVars file. Must be an object with key that are valid identifiers",
|
||||||
|
DescriptionFormat: providers.TextFormattingPlain,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return providers.FunctionSpec{
|
||||||
|
Parameters: params,
|
||||||
|
Return: cty.String,
|
||||||
|
Summary: "Encode an object into a string with the same format as a TFVars file",
|
||||||
|
Description: "provider::terraform::encode_tfvars encodes an object into a string with the same format as a TFVars file",
|
||||||
|
DescriptionFormat: providers.TextFormattingPlain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var InvalidInputError = errors.New("invalid input")
|
||||||
|
|
||||||
|
func (f *encodeTFVarsFunc) Call(args []cty.Value) (cty.Value, error) {
|
||||||
|
toEncode := args[0]
|
||||||
|
// null is invalid input
|
||||||
|
if toEncode.IsNull() {
|
||||||
|
return cty.NullVal(cty.String), fmt.Errorf("%w: must not be null", InvalidInputError)
|
||||||
|
}
|
||||||
|
if !toEncode.Type().IsObjectType() {
|
||||||
|
return cty.NullVal(cty.String), fmt.Errorf("%w: must be an object", InvalidInputError)
|
||||||
|
}
|
||||||
|
ef := hclwrite.NewEmptyFile()
|
||||||
|
body := ef.Body()
|
||||||
|
|
||||||
|
// Iterate over the elements of the input value
|
||||||
|
it := toEncode.ElementIterator()
|
||||||
|
for it.Next() {
|
||||||
|
key, val := it.Element()
|
||||||
|
// Check if the key is a string, known and not null, otherwise AsString method panics
|
||||||
|
if !key.Type().Equals(cty.String) || !key.IsKnown() || key.IsNull() {
|
||||||
|
return cty.NullVal(cty.String), fmt.Errorf("%w: object key must be a string: %v", InvalidInputError, key)
|
||||||
|
}
|
||||||
|
name := key.AsString()
|
||||||
|
if valid := hclsyntax.ValidIdentifier(name); !valid {
|
||||||
|
return cty.NullVal(cty.String), fmt.Errorf("%w: object key: %s - must be a valid identifier", InvalidInputError, name)
|
||||||
|
}
|
||||||
|
body.SetAttributeValue(key.AsString(), val)
|
||||||
|
}
|
||||||
|
b := ef.Bytes()
|
||||||
|
return cty.StringVal(string(b)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeExprFunc encodes an expression into a string
|
||||||
|
type encodeExprFunc struct{}
|
||||||
|
|
||||||
|
func (f *encodeExprFunc) Name() string {
|
||||||
|
return "encode_expr"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *encodeExprFunc) GetFunctionSpec() providers.FunctionSpec {
|
||||||
|
params := []providers.FunctionParameterSpec{
|
||||||
|
{
|
||||||
|
Name: "expr",
|
||||||
|
Type: cty.DynamicPseudoType,
|
||||||
|
Description: "expression to encode",
|
||||||
|
DescriptionFormat: providers.TextFormattingPlain,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return providers.FunctionSpec{
|
||||||
|
Parameters: params,
|
||||||
|
Return: cty.String,
|
||||||
|
Summary: "Takes an arbitrary expression and converts it into a string with valid OpenTofu syntax",
|
||||||
|
Description: "provider::terraform::encode_expr takes an arbitrary expression and converts it into a string with valid OpenTofu syntax",
|
||||||
|
DescriptionFormat: providers.TextFormattingPlain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var UnknownInputError = errors.New("input is not wholly known")
|
||||||
|
|
||||||
|
func (f *encodeExprFunc) Call(args []cty.Value) (cty.Value, error) {
|
||||||
|
toEncode := args[0]
|
||||||
|
nf := hclwrite.NewEmptyFile()
|
||||||
|
if !toEncode.IsWhollyKnown() {
|
||||||
|
return cty.NullVal(cty.String), UnknownInputError
|
||||||
|
}
|
||||||
|
tokens := hclwrite.TokensForValue(toEncode)
|
||||||
|
body := nf.Body()
|
||||||
|
body.AppendUnstructuredTokens(tokens)
|
||||||
|
return cty.StringVal(string(nf.Bytes())), nil
|
||||||
|
}
|
332
internal/builtin/providers/tf/provider_functions_test.go
Normal file
332
internal/builtin/providers/tf/provider_functions_test.go
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
// Copyright (c) The OpenTofu Authors
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
// Copyright (c) 2023 HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package tf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||||
|
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
)
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
name string
|
||||||
|
arg cty.Value
|
||||||
|
want cty.Value
|
||||||
|
expectedError error
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatHCLWithoutWhitespaces removes all whitespaces from the HCL string
|
||||||
|
// This will not result in a valid HCL string, but it will allow us to compare the result without worrying about whitespaces
|
||||||
|
func formatHCLWithoutWhitespaces(val cty.Value) string {
|
||||||
|
if val.IsNull() || !val.Type().Equals(cty.String) {
|
||||||
|
panic("formatHCLWithoutWhitespaces only works with string values")
|
||||||
|
}
|
||||||
|
f := string(hclwrite.Format([]byte(val.AsString())))
|
||||||
|
f = strings.ReplaceAll(f, " ", "")
|
||||||
|
f = strings.ReplaceAll(f, "\n", "")
|
||||||
|
f = strings.ReplaceAll(f, "\t", "")
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeTFVarsFunc(t *testing.T) {
|
||||||
|
tests := []test{
|
||||||
|
{
|
||||||
|
name: "basic test",
|
||||||
|
arg: cty.StringVal(`
|
||||||
|
test = 2
|
||||||
|
`),
|
||||||
|
want: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"test": cty.NumberIntVal(2),
|
||||||
|
}),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object basic test",
|
||||||
|
arg: cty.StringVal(`
|
||||||
|
test = {
|
||||||
|
k = "v"
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
want: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"test": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"k": cty.StringVal("v"),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list basic test",
|
||||||
|
arg: cty.StringVal(`
|
||||||
|
test = [
|
||||||
|
"i1",
|
||||||
|
"i2",
|
||||||
|
3
|
||||||
|
]
|
||||||
|
`),
|
||||||
|
want: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"test": cty.TupleVal([]cty.Value{
|
||||||
|
cty.StringVal("i1"),
|
||||||
|
cty.StringVal("i2"),
|
||||||
|
cty.NumberIntVal(3),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list of objects",
|
||||||
|
arg: cty.StringVal(`
|
||||||
|
test = [
|
||||||
|
{
|
||||||
|
o1k1 = "o1v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
o2k1 = "o2v1"
|
||||||
|
o2k2 = {
|
||||||
|
o3k1 = "o3v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`),
|
||||||
|
want: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"test": cty.TupleVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"o1k1": cty.StringVal("o1v1"),
|
||||||
|
}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"o2k1": cty.StringVal("o2v1"),
|
||||||
|
"o2k2": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"o3k1": cty.StringVal("o3v1"),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty object",
|
||||||
|
arg: cty.StringVal(""),
|
||||||
|
want: cty.ObjectVal(map[string]cty.Value{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid content",
|
||||||
|
arg: cty.StringVal("test"), // not a valid HCL
|
||||||
|
want: cty.NullVal(cty.DynamicPseudoType),
|
||||||
|
expectedError: FailedToDecodeError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid content 2",
|
||||||
|
arg: cty.StringVal("{}"), // not a valid HCL
|
||||||
|
want: cty.NullVal(cty.DynamicPseudoType),
|
||||||
|
expectedError: FailedToDecodeError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid content 3",
|
||||||
|
arg: cty.StringVal("\"5*5\": 3"), // not a valid HCL
|
||||||
|
want: cty.NullVal(cty.DynamicPseudoType),
|
||||||
|
expectedError: FailedToDecodeError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
decodeTFVars := &decodeTFVarsFunc{}
|
||||||
|
got, err := decodeTFVars.Call([]cty.Value{tt.arg})
|
||||||
|
if !errors.Is(err, tt.expectedError) {
|
||||||
|
t.Errorf("Call() error = %v, expected %v", err, tt.expectedError)
|
||||||
|
}
|
||||||
|
if got.NotEqual(tt.want).True() {
|
||||||
|
t.Errorf("Call() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeTFVarsFunc(t *testing.T) {
|
||||||
|
tests := []test{
|
||||||
|
{
|
||||||
|
name: "empty object",
|
||||||
|
arg: cty.ObjectVal(map[string]cty.Value{}),
|
||||||
|
want: cty.StringVal(""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic test",
|
||||||
|
arg: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"test": cty.NumberIntVal(2),
|
||||||
|
}),
|
||||||
|
want: cty.StringVal(`
|
||||||
|
test = 2
|
||||||
|
`),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object basic test",
|
||||||
|
arg: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"test": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"k": cty.StringVal("v"),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
want: cty.StringVal(`
|
||||||
|
test = {
|
||||||
|
k = "v"
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list basic test",
|
||||||
|
arg: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"test": cty.TupleVal([]cty.Value{
|
||||||
|
cty.StringVal("i1"),
|
||||||
|
cty.StringVal("i2"),
|
||||||
|
cty.NumberIntVal(3),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
want: cty.StringVal(`
|
||||||
|
test = ["i1", "i2", 3]
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list of objects",
|
||||||
|
arg: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"test": cty.TupleVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"o1k1": cty.StringVal("o1v1"),
|
||||||
|
}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"o2k1": cty.StringVal("o2v1"),
|
||||||
|
"o2k2": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"o3k1": cty.StringVal("o3v1"),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
want: cty.StringVal(`
|
||||||
|
test = [
|
||||||
|
{
|
||||||
|
o1k1 = "o1v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
o2k1 = "o2v1"
|
||||||
|
o2k2 = {
|
||||||
|
o3k1 = "o3v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "null input",
|
||||||
|
arg: cty.NullVal(cty.DynamicPseudoType),
|
||||||
|
want: cty.StringVal(""),
|
||||||
|
expectedError: InvalidInputError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid input: not an object",
|
||||||
|
arg: cty.StringVal("test"), // not an object
|
||||||
|
want: cty.StringVal(""),
|
||||||
|
expectedError: InvalidInputError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid input: Object with invalid key",
|
||||||
|
arg: cty.ObjectVal(map[string]cty.Value{"7*7": cty.StringVal("test")}), // invalid key
|
||||||
|
want: cty.StringVal(""),
|
||||||
|
expectedError: InvalidInputError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
encodeTFVars := &encodeTFVarsFunc{}
|
||||||
|
got, err := encodeTFVars.Call([]cty.Value{tt.arg})
|
||||||
|
if err != nil {
|
||||||
|
if tt.expectedError == nil {
|
||||||
|
t.Fatalf("Call() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, tt.expectedError) {
|
||||||
|
t.Fatalf("Call() error = %v, expected %v", err, tt.expectedError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedRequirement := formatHCLWithoutWhitespaces(tt.want)
|
||||||
|
formattedGot := formatHCLWithoutWhitespaces(got)
|
||||||
|
|
||||||
|
if formattedGot != formattedRequirement {
|
||||||
|
t.Errorf("Call() got: %v, want: %v", formattedGot, formattedRequirement)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeExprFunc(t *testing.T) {
|
||||||
|
tests := []test{
|
||||||
|
{
|
||||||
|
name: "string",
|
||||||
|
arg: cty.StringVal("test"),
|
||||||
|
want: cty.StringVal(`"test"`),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number",
|
||||||
|
arg: cty.NumberIntVal(2),
|
||||||
|
want: cty.StringVal("2"),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bool",
|
||||||
|
arg: cty.True,
|
||||||
|
want: cty.StringVal("true"),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "null",
|
||||||
|
arg: cty.NullVal(cty.String),
|
||||||
|
want: cty.StringVal("null"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tuple",
|
||||||
|
arg: cty.TupleVal([]cty.Value{cty.StringVal("test"), cty.StringVal("test2")}),
|
||||||
|
want: cty.StringVal(`["test", "test2"]`),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tuple with objects",
|
||||||
|
arg: cty.TupleVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"test": cty.StringVal("test")}), cty.ObjectVal(map[string]cty.Value{"test2": cty.StringVal("test2")})}),
|
||||||
|
want: cty.StringVal(`[{test = "test"}, {test2 = "test2"}]`),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object",
|
||||||
|
arg: cty.ObjectVal(map[string]cty.Value{"test": cty.StringVal("test")}),
|
||||||
|
want: cty.StringVal(`{test = "test"}`),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested object",
|
||||||
|
arg: cty.ObjectVal(map[string]cty.Value{"test": cty.ObjectVal(map[string]cty.Value{"test": cty.StringVal("test")})}),
|
||||||
|
want: cty.StringVal(`{test = {test = "test"}}`),
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
encodeExpr := &encodeExprFunc{}
|
||||||
|
got, err := encodeExpr.Call([]cty.Value{tt.arg})
|
||||||
|
if !errors.Is(err, tt.expectedError) {
|
||||||
|
t.Errorf("Call() error = %v, expected %v", err, tt.expectedError)
|
||||||
|
}
|
||||||
|
formattedRequirement := formatHCLWithoutWhitespaces(tt.want)
|
||||||
|
formattedGot := formatHCLWithoutWhitespaces(got)
|
||||||
|
|
||||||
|
if formattedGot != formattedRequirement {
|
||||||
|
t.Errorf("Call() got: %v, want: %v", formattedGot, formattedRequirement)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -145,6 +145,10 @@
|
|||||||
"title": "Provider Requirements",
|
"title": "Provider Requirements",
|
||||||
"path": "language/providers/requirements"
|
"path": "language/providers/requirements"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Built-in Provider",
|
||||||
|
"path": "language/providers/builtin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Dependency Lock File",
|
"title": "Dependency Lock File",
|
||||||
"path": "language/files/dependency-lock"
|
"path": "language/files/dependency-lock"
|
||||||
@ -717,7 +721,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "<code>issensitive</code>",
|
"title": "<code>issensitive</code>",
|
||||||
"path": "language/functions/issensitive",
|
"path": "language/functions/issensitive"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "<code>tobool</code>",
|
"title": "<code>tobool</code>",
|
||||||
@ -1193,7 +1197,7 @@
|
|||||||
"title": "urlencode",
|
"title": "urlencode",
|
||||||
"path": "language/functions/urlencode",
|
"path": "language/functions/urlencode",
|
||||||
"hidden": true
|
"hidden": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "urldecode",
|
"title": "urldecode",
|
||||||
"path": "language/functions/urldecode",
|
"path": "language/functions/urldecode",
|
||||||
|
@ -61,6 +61,9 @@ locals {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Built-in Provider Functions:
|
||||||
|
OpenTofu has a built-in provider `terraform.io/builtin/terraform` which provides [additional functions](../providers/builtin.mdx#functions) that can be used in OpenTofu configurations.
|
||||||
|
|
||||||
### Notes for Provider Authors:
|
### Notes for Provider Authors:
|
||||||
* Support for functions was added in protocol version 5.5 and 6.5.
|
* Support for functions was added in protocol version 5.5 and 6.5.
|
||||||
* OpenTofu's provider protocol is compatible with Terraform's provider protocol.
|
* OpenTofu's provider protocol is compatible with Terraform's provider protocol.
|
||||||
|
74
website/docs/language/providers/builtin.mdx
Normal file
74
website/docs/language/providers/builtin.mdx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
sidebar_label: Built-in Provider
|
||||||
|
---
|
||||||
|
# Built-in Provider
|
||||||
|
|
||||||
|
Most providers are distributed separately as plugins, but there
|
||||||
|
is one provider that is built into OpenTofu itself. This provider enables the
|
||||||
|
[the `terraform_remote_state` data source](../state/remote-state-data.mdx).
|
||||||
|
|
||||||
|
Because this provider is built in to OpenTofu, you don't need to declare it
|
||||||
|
in the `required_providers` block in order to use its features (except provider functions).
|
||||||
|
It has a special provider source address, which is
|
||||||
|
`terraform.io/builtin/terraform`. This address may sometimes appear in
|
||||||
|
OpenTofu's error messages and other output in order to unambiguously refer
|
||||||
|
to the built-in provider, as opposed to a hypothetical third-party provider
|
||||||
|
with the type name "terraform".
|
||||||
|
|
||||||
|
There is also an existing provider with the source address
|
||||||
|
`hashicorp/terraform`, which is an older version of the now-built-in provider.
|
||||||
|
`hashicorp/terraform` is not compatible with OpenTofu and should never be declared in a
|
||||||
|
`required_providers` block.
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
The built-in provider has additional functions, which can be called after declaring the provider in the `required_providers` block.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
terraform = {
|
||||||
|
source = "terraform.io/builtin/terraform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### decode_tfvars
|
||||||
|
|
||||||
|
`decode_tfvars` takes the content of the .tfvars file as a string input and returns a decoded object.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
locals {
|
||||||
|
content = file("./example_file.tfvars")
|
||||||
|
decoded = provider::terraform::decode_tfvars(local.content) # Returns object
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### encode_tfvars
|
||||||
|
|
||||||
|
`encode_tfvars` takes an object and returns the string representation of the object that can be used as the content of the .tfvars file.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
locals {
|
||||||
|
object = {
|
||||||
|
key1 = "value1"
|
||||||
|
key2 = "value2"
|
||||||
|
}
|
||||||
|
encoded = provider::terraform::encode_tfvars(local.object) # Returns string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The keys in the object need to be [valid identifiers](../syntax/configuration.mdx#identifiers).
|
||||||
|
|
||||||
|
### encode_expr
|
||||||
|
|
||||||
|
`encode_expr` takes an arbitrary [expression](../expressions/index.mdx) and converts it into a string with valid OpenTofu syntax.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
locals {
|
||||||
|
expression = {
|
||||||
|
key1 = "value1"
|
||||||
|
key2 = "value2"
|
||||||
|
}
|
||||||
|
encoded = provider::terraform::encode_expr(local.expression) # Returns string
|
||||||
|
}
|
||||||
|
```
|
@ -1,4 +1,6 @@
|
|||||||
---
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
sidebar_label: Provider Configuration
|
||||||
description: >-
|
description: >-
|
||||||
Learn how to set up providers, including how to use the alias meta-argument to
|
Learn how to set up providers, including how to use the alias meta-argument to
|
||||||
specify multiple configurations for a single provider.
|
specify multiple configurations for a single provider.
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
---
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
sidebar_label: Provider Requirements
|
||||||
description: >-
|
description: >-
|
||||||
Providers are plugins that allow OpenTofu to interact with services, cloud
|
Providers are plugins that allow OpenTofu to interact with services, cloud
|
||||||
providers, and other APIs. Learn how to declare providers in a configuration.
|
providers, and other APIs. Learn how to declare providers in a configuration.
|
||||||
@ -248,25 +250,6 @@ often it forces users of the module to update many modules simultaneously when
|
|||||||
performing routine upgrades. Specify a minimum version, document any known
|
performing routine upgrades. Specify a minimum version, document any known
|
||||||
incompatibilities, and let the root module manage the maximum version.
|
incompatibilities, and let the root module manage the maximum version.
|
||||||
|
|
||||||
## Built-in Providers
|
|
||||||
|
|
||||||
Most providers are distributed separately as plugins, but there
|
|
||||||
is one provider that is built into OpenTofu itself. This provider enables the
|
|
||||||
[the `terraform_remote_state` data source](../../language/state/remote-state-data.mdx).
|
|
||||||
|
|
||||||
Because this provider is built in to OpenTofu, you don't need to declare it
|
|
||||||
in the `required_providers` block in order to use its features. However, for
|
|
||||||
consistency it _does_ have a special provider source address, which is
|
|
||||||
`terraform.io/builtin/terraform`. This address may sometimes appear in
|
|
||||||
OpenTofu's error messages and other output in order to unambiguously refer
|
|
||||||
to the built-in provider, as opposed to a hypothetical third-party provider
|
|
||||||
with the type name "tofu".
|
|
||||||
|
|
||||||
There is also an existing provider with the source address
|
|
||||||
`hashicorp/terraform`, which is an older version of the now-built-in provider.
|
|
||||||
`hashicorp/terraform` is not compatible with OpenTofu and should never be declared in a
|
|
||||||
`required_providers` block.
|
|
||||||
|
|
||||||
## In-house Providers
|
## In-house Providers
|
||||||
|
|
||||||
Anyone can develop and distribute their own providers.
|
Anyone can develop and distribute their own providers.
|
||||||
|
Loading…
Reference in New Issue
Block a user