From 2d9cef1f5519c77ed5972643c57e050a6320671f Mon Sep 17 00:00:00 2001 From: Ilia Gogotchuri Date: Wed, 25 Dec 2024 13:21:59 +0400 Subject: [PATCH] Functions: decode_tfvars, encode_tfvars, encode_expr on bult-in provider for compatibility (#2306) Signed-off-by: Ilia Gogotchuri Co-authored-by: Oleksandr Levchenkov --- CHANGELOG.md | 5 + internal/builtin/providers/tf/provider.go | 56 ++- .../providers/tf/provider_functions.go | 175 +++++++++ .../providers/tf/provider_functions_test.go | 332 ++++++++++++++++++ website/data/language-nav-data.json | 8 +- website/docs/language/functions/index.mdx | 3 + website/docs/language/providers/builtin.mdx | 74 ++++ .../docs/language/providers/configuration.mdx | 2 + .../docs/language/providers/requirements.mdx | 21 +- 9 files changed, 651 insertions(+), 25 deletions(-) create mode 100644 internal/builtin/providers/tf/provider_functions.go create mode 100644 internal/builtin/providers/tf/provider_functions_test.go create mode 100644 website/docs/language/providers/builtin.mdx diff --git a/CHANGELOG.md b/CHANGELOG.md index 0005c70524..74040a2d57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ UPGRADE NOTES: 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: BUG FIXES: diff --git a/internal/builtin/providers/tf/provider.go b/internal/builtin/providers/tf/provider.go index 87c519f68c..00a775b27d 100644 --- a/internal/builtin/providers/tf/provider.go +++ b/internal/builtin/providers/tf/provider.go @@ -13,14 +13,27 @@ import ( "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/providers" + "github.com/zclconf/go-cty/cty" ) // Provider is an implementation of providers.Interface -type Provider struct{} +type Provider struct { + funcs map[string]providerFunc +} // NewProvider returns a new tofu provider 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. @@ -32,6 +45,7 @@ func (p *Provider) GetProviderSchema() providers.GetProviderSchemaResponse { ResourceTypes: map[string]providers.Schema{ "terraform_data": dataStoreResourceSchema(), }, + Functions: p.getFunctionSpecs(), } } @@ -161,14 +175,48 @@ func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRe } 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 { - 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. func (p *Provider) Close() error { 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, + } +} diff --git a/internal/builtin/providers/tf/provider_functions.go b/internal/builtin/providers/tf/provider_functions.go new file mode 100644 index 0000000000..ae460a8f98 --- /dev/null +++ b/internal/builtin/providers/tf/provider_functions.go @@ -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 +} diff --git a/internal/builtin/providers/tf/provider_functions_test.go b/internal/builtin/providers/tf/provider_functions_test.go new file mode 100644 index 0000000000..519a5ab62d --- /dev/null +++ b/internal/builtin/providers/tf/provider_functions_test.go @@ -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) + } + }) + } +} diff --git a/website/data/language-nav-data.json b/website/data/language-nav-data.json index 25edc7270b..24245ef2be 100644 --- a/website/data/language-nav-data.json +++ b/website/data/language-nav-data.json @@ -145,6 +145,10 @@ "title": "Provider Requirements", "path": "language/providers/requirements" }, + { + "title": "Built-in Provider", + "path": "language/providers/builtin" + }, { "title": "Dependency Lock File", "path": "language/files/dependency-lock" @@ -717,7 +721,7 @@ }, { "title": "issensitive", - "path": "language/functions/issensitive", + "path": "language/functions/issensitive" }, { "title": "tobool", @@ -1193,7 +1197,7 @@ "title": "urlencode", "path": "language/functions/urlencode", "hidden": true - }, + }, { "title": "urldecode", "path": "language/functions/urldecode", diff --git a/website/docs/language/functions/index.mdx b/website/docs/language/functions/index.mdx index 9d41e4a8fb..0b966da52b 100644 --- a/website/docs/language/functions/index.mdx +++ b/website/docs/language/functions/index.mdx @@ -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: * Support for functions was added in protocol version 5.5 and 6.5. * OpenTofu's provider protocol is compatible with Terraform's provider protocol. diff --git a/website/docs/language/providers/builtin.mdx b/website/docs/language/providers/builtin.mdx new file mode 100644 index 0000000000..1b34765e89 --- /dev/null +++ b/website/docs/language/providers/builtin.mdx @@ -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 +} +``` diff --git a/website/docs/language/providers/configuration.mdx b/website/docs/language/providers/configuration.mdx index 6e51c0db8b..1dac0d1d80 100644 --- a/website/docs/language/providers/configuration.mdx +++ b/website/docs/language/providers/configuration.mdx @@ -1,4 +1,6 @@ --- +sidebar_position: 1 +sidebar_label: Provider Configuration description: >- Learn how to set up providers, including how to use the alias meta-argument to specify multiple configurations for a single provider. diff --git a/website/docs/language/providers/requirements.mdx b/website/docs/language/providers/requirements.mdx index 1d10f00487..887016a531 100644 --- a/website/docs/language/providers/requirements.mdx +++ b/website/docs/language/providers/requirements.mdx @@ -1,4 +1,6 @@ --- +sidebar_position: 2 +sidebar_label: Provider Requirements description: >- Providers are plugins that allow OpenTofu to interact with services, cloud 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 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 Anyone can develop and distribute their own providers.