mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
jsonconfig: Additional details about input variables
The JSON object describing an input variable can now include two additional properties: - "type" provides a JSON representation of the variable's type constraint, if one is set. Omitted if either there is no constraint declared at all or if it's set to "any", which are equivalent and both mean that the type is completely unconstrained. This uses the standard cty representation of a type constraint, which matches how OpenTofu already describes types in the provider protocol, in state snapshots, and in saved plan files. - "required" directly represents whether callers are required to provide a value for the variable. This is technically redundant since it is set to true unless "default" is also set, but this avoids the need for consuming software to reimplement this rule and potentially allows us to make this rule more complicated/subtle in future if needed. For some reason the documentation about the JSON configuration representation did not previously mention the "variables" property at all, so this adds documentation for both the new properties and the pre-existing properties. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
@@ -64,8 +64,10 @@ type moduleCall struct {
|
||||
type variables map[string]*variable
|
||||
|
||||
type variable struct {
|
||||
Type json.RawMessage `json:"type,omitempty"`
|
||||
Default json.RawMessage `json:"default,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Sensitive bool `json:"sensitive,omitempty"`
|
||||
Deprecated string `json:"deprecated,omitempty"`
|
||||
}
|
||||
@@ -379,17 +381,47 @@ func marshalModule(c *configs.Config, schemas *tofu.Schemas, addr string) (modul
|
||||
if len(c.Module.Variables) > 0 {
|
||||
vars := make(variables, len(c.Module.Variables))
|
||||
for k, v := range c.Module.Variables {
|
||||
typeConstraint := cty.DynamicPseudoType
|
||||
if v.ConstraintType != cty.NilType {
|
||||
typeConstraint = v.ConstraintType
|
||||
}
|
||||
|
||||
var typeJSON []byte
|
||||
// We leave the "type" property unset in output when it
|
||||
// would be DynamicPseudoType, because the most typical way to
|
||||
// represent this situation in our source language is to
|
||||
// omit the type argument from the declaration -- it essentially
|
||||
// represents "no type constrant at all" -- and because this
|
||||
// avoids exposing a potentially-confusing detail that cty
|
||||
// describes DynamicPseudoType as "dynamic" in JSON, while HCL
|
||||
// prefers to call it "any".
|
||||
if !typeConstraint.Equals(cty.DynamicPseudoType) {
|
||||
typeJSON, err = typeConstraint.MarshalJSON()
|
||||
if err != nil {
|
||||
// Should not get here, because v.ConstraintType should always
|
||||
// be a valid cty type when it isn't NilType, so this uses
|
||||
// the internal type stringification to get the most detailed
|
||||
// error message in a potential bug report.
|
||||
return module, fmt.Errorf("failed to marshal %#v as JSON: %w", typeConstraint, err)
|
||||
}
|
||||
}
|
||||
|
||||
var defaultValJSON []byte
|
||||
var required bool
|
||||
if v.Default == cty.NilVal {
|
||||
defaultValJSON = nil
|
||||
required = true
|
||||
} else {
|
||||
defaultValJSON, err = ctyjson.Marshal(v.Default, v.Default.Type())
|
||||
required = false
|
||||
if err != nil {
|
||||
return module, err
|
||||
}
|
||||
}
|
||||
vars[k] = &variable{
|
||||
Type: typeJSON,
|
||||
Default: defaultValJSON,
|
||||
Required: required,
|
||||
Description: v.Description,
|
||||
Sensitive: v.Sensitive,
|
||||
Deprecated: v.Deprecated,
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
package jsonconfig
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/tofu"
|
||||
@@ -110,38 +113,165 @@ func TestFindSourceProviderConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalModule(t *testing.T) {
|
||||
t.Run("validate variables marshalling with all the required fields", func(t *testing.T) {
|
||||
varCfg := &configs.Variable{
|
||||
Name: "myvar",
|
||||
Description: "myvar description",
|
||||
Deprecated: "myvar deprecated message",
|
||||
}
|
||||
modCfg := configs.Config{
|
||||
Module: &configs.Module{
|
||||
Variables: map[string]*configs.Variable{
|
||||
"myvar": varCfg,
|
||||
},
|
||||
},
|
||||
}
|
||||
modCfg.Root = &modCfg
|
||||
emptySchemas := &tofu.Schemas{}
|
||||
|
||||
out, err := marshalModule(&modCfg, &tofu.Schemas{}, addrs.RootModule.String())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error during marshalling module: %s", err)
|
||||
}
|
||||
|
||||
expected := module{
|
||||
tests := map[string]struct {
|
||||
Input *configs.Config
|
||||
Schemas *tofu.Schemas
|
||||
Want module
|
||||
}{
|
||||
"empty": {
|
||||
Input: &configs.Config{
|
||||
Module: &configs.Module{},
|
||||
},
|
||||
Schemas: emptySchemas,
|
||||
Want: module{
|
||||
Outputs: map[string]output{},
|
||||
ModuleCalls: map[string]moduleCall{},
|
||||
Variables: map[string]*variable{
|
||||
"myvar": {
|
||||
Description: varCfg.Description,
|
||||
Deprecated: varCfg.Deprecated,
|
||||
},
|
||||
},
|
||||
"variable, minimal": {
|
||||
Input: &configs.Config{
|
||||
Module: &configs.Module{
|
||||
Variables: map[string]*configs.Variable{
|
||||
"example": &configs.Variable{
|
||||
Name: "example",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Schemas: emptySchemas,
|
||||
Want: module{
|
||||
Outputs: map[string]output{},
|
||||
ModuleCalls: map[string]moduleCall{},
|
||||
Variables: variables{
|
||||
"example": {
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"variable, elaborate": {
|
||||
Input: &configs.Config{
|
||||
Module: &configs.Module{
|
||||
Variables: map[string]*configs.Variable{
|
||||
"example": {
|
||||
Name: "example",
|
||||
Description: "description",
|
||||
Deprecated: "deprecation message",
|
||||
Sensitive: true,
|
||||
ConstraintType: cty.String,
|
||||
Type: cty.String, // similar to ConstraintType; unfortunate historical quirk
|
||||
Default: cty.StringVal("hello"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Schemas: emptySchemas,
|
||||
Want: module{
|
||||
Outputs: map[string]output{},
|
||||
ModuleCalls: map[string]moduleCall{},
|
||||
Variables: variables{
|
||||
"example": {
|
||||
Type: json.RawMessage(`"string"`),
|
||||
Default: json.RawMessage(`"hello"`),
|
||||
Required: false,
|
||||
Description: "description",
|
||||
Deprecated: "deprecation message",
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"variable, collection type": {
|
||||
Input: &configs.Config{
|
||||
Module: &configs.Module{
|
||||
Variables: map[string]*configs.Variable{
|
||||
"example": {
|
||||
Name: "example",
|
||||
ConstraintType: cty.List(cty.String),
|
||||
Type: cty.List(cty.String), // similar to ConstraintType; unfortunate historical quirk
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Schemas: emptySchemas,
|
||||
Want: module{
|
||||
Outputs: map[string]output{},
|
||||
ModuleCalls: map[string]moduleCall{},
|
||||
Variables: variables{
|
||||
"example": {
|
||||
// The following is how cty serializes collection types
|
||||
// as JSON: a two-element array where the first is
|
||||
// the kind of collection and the second is the
|
||||
// element type.
|
||||
Type: json.RawMessage(`["list","string"]`),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"variable, object type": {
|
||||
Input: &configs.Config{
|
||||
Module: &configs.Module{
|
||||
Variables: map[string]*configs.Variable{
|
||||
"example": {
|
||||
Name: "example",
|
||||
ConstraintType: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"foo": cty.String,
|
||||
"bar": cty.String,
|
||||
}, []string{"bar"}),
|
||||
Type: cty.Object(map[string]cty.Type{
|
||||
"foo": cty.String,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Schemas: emptySchemas,
|
||||
Want: module{
|
||||
Outputs: map[string]output{},
|
||||
ModuleCalls: map[string]moduleCall{},
|
||||
Variables: variables{
|
||||
"example": {
|
||||
// The following is how cty serializes structural types
|
||||
// as JSON: a two- or three-element array where the
|
||||
// first is the kind of structure and the second is the
|
||||
// kind-specific structure description, which in
|
||||
// this case is a JSON object mapping attribute names
|
||||
// to their types. For object types in particular,
|
||||
// when at least one optional attribute is included
|
||||
// the array has a third element listing the names
|
||||
// of the optional attributes.
|
||||
Type: json.RawMessage(`["object",{"bar":"string","foo":"string"},["bar"]]`),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO: More test cases covering things other than input variables.
|
||||
// (For now the other details are mainly tested in package command,
|
||||
// as part of the tests for "tofu show".)
|
||||
}
|
||||
if diff := cmp.Diff(expected, out); diff != "" {
|
||||
t.Errorf("unexpected diff: \n%s", diff)
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
schemas := test.Schemas
|
||||
|
||||
// We'll make the input a little more realistic by including some
|
||||
// of the cyclic pointers that would normally be inserted by the
|
||||
// config loader.
|
||||
input := *test.Input
|
||||
input.Root = &input
|
||||
input.Parent = &input
|
||||
|
||||
got, err := marshalModule(&input, schemas, addrs.RootModule.String())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(test.Want, got); diff != "" {
|
||||
t.Error("wrong result\n" + diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,8 +660,8 @@ func TestShow_json_output(t *testing.T) {
|
||||
// Disregard format version to reduce needless test fixture churn
|
||||
want.FormatVersion = got.FormatVersion
|
||||
|
||||
if !cmp.Equal(got, want) {
|
||||
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatal("wrong result:\n" + diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -857,8 +857,8 @@ func TestShow_json_output_conditions_refresh_only(t *testing.T) {
|
||||
// Disregard format version to reduce needless test fixture churn
|
||||
want.FormatVersion = got.FormatVersion
|
||||
|
||||
if !cmp.Equal(got, want) {
|
||||
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatal("wrong result:\n" + diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1651,6 +1651,8 @@ func TestShow_module(t *testing.T) {
|
||||
},
|
||||
"variables": map[string]any{
|
||||
"foo": map[string]any{
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -130,9 +130,11 @@
|
||||
],
|
||||
"variables": {
|
||||
"ami": {
|
||||
"type": "string",
|
||||
"default": "ami-test"
|
||||
},
|
||||
"id_minimum_length": {
|
||||
"type": "number",
|
||||
"default": 10
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,9 +149,11 @@
|
||||
],
|
||||
"variables": {
|
||||
"ami": {
|
||||
"type": "string",
|
||||
"default": "ami-test"
|
||||
},
|
||||
"id_minimum_length": {
|
||||
"type": "number",
|
||||
"default": 10
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,10 @@
|
||||
}
|
||||
],
|
||||
"variables": {
|
||||
"contents": {}
|
||||
"contents": {
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"source": "./create"
|
||||
|
||||
@@ -40,27 +40,110 @@
|
||||
},
|
||||
"variables": {
|
||||
"list_empty_default": {
|
||||
"default": []
|
||||
"default": [],
|
||||
"type": [
|
||||
"list",
|
||||
[
|
||||
"object",
|
||||
{
|
||||
"optional_attribute": "string",
|
||||
"optional_attribute_with_default": "string",
|
||||
"required_attribute": "string"
|
||||
},
|
||||
[
|
||||
"optional_attribute",
|
||||
"optional_attribute_with_default"
|
||||
]
|
||||
]
|
||||
]
|
||||
},
|
||||
"list_no_default": {
|
||||
"required": true,
|
||||
"type": [
|
||||
"list",
|
||||
[
|
||||
"object",
|
||||
{
|
||||
"optional_attribute": "string",
|
||||
"optional_attribute_with_default": "string",
|
||||
"required_attribute": "string"
|
||||
},
|
||||
[
|
||||
"optional_attribute",
|
||||
"optional_attribute_with_default"
|
||||
]
|
||||
]
|
||||
]
|
||||
},
|
||||
"list_no_default": {},
|
||||
"nested_optional_object": {
|
||||
"default": {
|
||||
"nested_object": null
|
||||
}
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
{
|
||||
"nested_object": [
|
||||
"object",
|
||||
{
|
||||
"flag": "bool"
|
||||
},
|
||||
[
|
||||
"flag"
|
||||
]
|
||||
]
|
||||
},
|
||||
[
|
||||
"nested_object"
|
||||
]
|
||||
]
|
||||
},
|
||||
"nested_optional_object_with_default": {
|
||||
"default": {
|
||||
"nested_object": {
|
||||
"flag": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
{
|
||||
"nested_object": [
|
||||
"object",
|
||||
{
|
||||
"flag": "bool"
|
||||
},
|
||||
[
|
||||
"flag"
|
||||
]
|
||||
]
|
||||
},
|
||||
[
|
||||
"nested_object"
|
||||
]
|
||||
]
|
||||
},
|
||||
"nested_optional_object_with_embedded_default": {
|
||||
"default": {
|
||||
"nested_object": {
|
||||
"flag": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
{
|
||||
"nested_object": [
|
||||
"object",
|
||||
{
|
||||
"flag": "bool"
|
||||
},
|
||||
[
|
||||
"flag"
|
||||
]
|
||||
]
|
||||
},
|
||||
[
|
||||
"nested_object"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,6 +422,54 @@ Because the configuration models are produced at a stage prior to expression eva
|
||||
// as the root of a tree of similar objects describing descendent modules.
|
||||
"root_module": {
|
||||
|
||||
// "variables" describes the input variable configurations in the module.
|
||||
"variables": {
|
||||
|
||||
// Property names here are the input variable names
|
||||
"example": {
|
||||
// "type" describes the type constraint of the input variable, if any.
|
||||
// This property is omitted for an unconstrained input variable.
|
||||
//
|
||||
// When present, its value is either a single string representing a
|
||||
// primitive type, or an array with two or three elements describing a
|
||||
// complex type:
|
||||
// - "string", "number", or "bool" for the primitive types.
|
||||
// - ["list", <type>] for a list type, where the second array element
|
||||
// is the list element type described in the same notation. The
|
||||
// collection type kinds are "list", "map", and "set".
|
||||
// - ["object", <attributes>] for an object type, where the second
|
||||
// array element is a JSON object describing the object attributes
|
||||
// and their associated types. For an object type with optional
|
||||
// attributes, the array has a third element that is a JSON array
|
||||
// listing the attributes that are optional.
|
||||
// - ["tuple", <elements>] for a tuple type, where the second array
|
||||
// element is a JSON array describing the tuple element types.
|
||||
"type": "string",
|
||||
|
||||
// "default" is the default value of the input variable, serialized
|
||||
// as JSON using the same mappings as OpenTofu's built-in "jsonencode"
|
||||
// function.
|
||||
"default": "Example",
|
||||
|
||||
// "required" is included and set to true if callers are required to
|
||||
// provide a value for this variable, or omitted if it is optional.
|
||||
"required": true,
|
||||
|
||||
// "description" is the author-provided description associated with
|
||||
// this input variable, if any.
|
||||
"description": "Example",
|
||||
|
||||
// "sensitive" is included and set to true if the input variable is
|
||||
// declared as being "sensitive", or omitted if not.
|
||||
"sensitive": true,
|
||||
|
||||
// "deprecated" is included and set to a deprecation message for
|
||||
// any input variable that is declared as deprecated, or omitted for
|
||||
// non-deprecated input variables.
|
||||
"deprecated": "Example",
|
||||
}
|
||||
},
|
||||
|
||||
// "outputs" describes the output value configurations in the module.
|
||||
"outputs": {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user