mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge pull request #29832 from hashicorp/jbardin/nullable-variable
configs: explicitly nullable variable values
This commit is contained in:
commit
b91d9435ea
@ -56,6 +56,10 @@ func (v *Variable) merge(ov *Variable) hcl.Diagnostics {
|
|||||||
if ov.ParsingMode != 0 {
|
if ov.ParsingMode != 0 {
|
||||||
v.ParsingMode = ov.ParsingMode
|
v.ParsingMode = ov.ParsingMode
|
||||||
}
|
}
|
||||||
|
if ov.NullableSet {
|
||||||
|
v.Nullable = ov.Nullable
|
||||||
|
v.NullableSet = ov.NullableSet
|
||||||
|
}
|
||||||
|
|
||||||
// If the override file overrode type without default or vice-versa then
|
// If the override file overrode type without default or vice-versa then
|
||||||
// it may have created an invalid situation, which we'll catch now by
|
// it may have created an invalid situation, which we'll catch now by
|
||||||
@ -100,6 +104,16 @@ func (v *Variable) merge(ov *Variable) hcl.Diagnostics {
|
|||||||
} else {
|
} else {
|
||||||
v.Default = val
|
v.Default = val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure a null default wasn't merged in when it is not allowed
|
||||||
|
if !v.Nullable && v.Default.IsNull() {
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid default value for variable",
|
||||||
|
Detail: "A null default value is not valid when nullable=false.",
|
||||||
|
Subject: &ov.DeclRange,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return diags
|
return diags
|
||||||
|
@ -24,6 +24,8 @@ func TestModuleOverrideVariable(t *testing.T) {
|
|||||||
Description: "b_override description",
|
Description: "b_override description",
|
||||||
DescriptionSet: true,
|
DescriptionSet: true,
|
||||||
Default: cty.StringVal("b_override"),
|
Default: cty.StringVal("b_override"),
|
||||||
|
Nullable: false,
|
||||||
|
NullableSet: true,
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
ConstraintType: cty.String,
|
ConstraintType: cty.String,
|
||||||
ParsingMode: VariableParseLiteral,
|
ParsingMode: VariableParseLiteral,
|
||||||
@ -46,6 +48,8 @@ func TestModuleOverrideVariable(t *testing.T) {
|
|||||||
Description: "base description",
|
Description: "base description",
|
||||||
DescriptionSet: true,
|
DescriptionSet: true,
|
||||||
Default: cty.StringVal("b_override partial"),
|
Default: cty.StringVal("b_override partial"),
|
||||||
|
Nullable: true,
|
||||||
|
NullableSet: false,
|
||||||
Type: cty.String,
|
Type: cty.String,
|
||||||
ConstraintType: cty.String,
|
ConstraintType: cty.String,
|
||||||
ParsingMode: VariableParseLiteral,
|
ParsingMode: VariableParseLiteral,
|
||||||
|
@ -36,6 +36,12 @@ type Variable struct {
|
|||||||
DescriptionSet bool
|
DescriptionSet bool
|
||||||
SensitiveSet bool
|
SensitiveSet bool
|
||||||
|
|
||||||
|
// Nullable indicates that null is a valid value for this variable. Setting
|
||||||
|
// Nullable to false means that the module can expect this variable to
|
||||||
|
// never be null.
|
||||||
|
Nullable bool
|
||||||
|
NullableSet bool
|
||||||
|
|
||||||
DeclRange hcl.Range
|
DeclRange hcl.Range
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +116,16 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
|
|||||||
v.SensitiveSet = true
|
v.SensitiveSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if attr, exists := content.Attributes["nullable"]; exists {
|
||||||
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable)
|
||||||
|
diags = append(diags, valDiags...)
|
||||||
|
v.NullableSet = true
|
||||||
|
} else {
|
||||||
|
// The current default is true, which is subject to change in a future
|
||||||
|
// language edition.
|
||||||
|
v.Nullable = true
|
||||||
|
}
|
||||||
|
|
||||||
if attr, exists := content.Attributes["default"]; exists {
|
if attr, exists := content.Attributes["default"]; exists {
|
||||||
val, valDiags := attr.Expr.Value(nil)
|
val, valDiags := attr.Expr.Value(nil)
|
||||||
diags = append(diags, valDiags...)
|
diags = append(diags, valDiags...)
|
||||||
@ -134,6 +150,15 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !v.Nullable && val.IsNull() {
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid default value for variable",
|
||||||
|
Detail: "A null default value is not valid when nullable=false.",
|
||||||
|
Subject: attr.Expr.Range().Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
v.Default = val
|
v.Default = val
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -556,6 +581,9 @@ var variableBlockSchema = &hcl.BodySchema{
|
|||||||
{
|
{
|
||||||
Name: "sensitive",
|
Name: "sensitive",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "nullable",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Blocks: []hcl.BlockHeaderSchema{
|
Blocks: []hcl.BlockHeaderSchema{
|
||||||
{
|
{
|
||||||
|
5
internal/configs/testdata/invalid-modules/nullable-with-default-null/main.tf
vendored
Normal file
5
internal/configs/testdata/invalid-modules/nullable-with-default-null/main.tf
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
variable "in" {
|
||||||
|
type = number
|
||||||
|
nullable = false
|
||||||
|
default = null
|
||||||
|
}
|
@ -30,3 +30,15 @@ variable "sensitive_value" {
|
|||||||
}
|
}
|
||||||
sensitive = true
|
sensitive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "nullable" {
|
||||||
|
type = string
|
||||||
|
nullable = true
|
||||||
|
default = "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nullable_default_null" {
|
||||||
|
type = map(string)
|
||||||
|
nullable = true
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
variable "fully_overridden" {
|
variable "fully_overridden" {
|
||||||
|
nullable = false
|
||||||
default = "b_override"
|
default = "b_override"
|
||||||
description = "b_override description"
|
description = "b_override description"
|
||||||
type = string
|
type = string
|
||||||
|
@ -596,3 +596,36 @@ resource "test_object" "x" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Apply_nullableVariables(t *testing.T) {
|
||||||
|
m := testModule(t, "apply-nullable-variables")
|
||||||
|
state := states.NewState()
|
||||||
|
ctx := testContext2(t, &ContextOpts{})
|
||||||
|
plan, diags := ctx.Plan(m, state, &PlanOpts{})
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("plan: %s", diags.Err())
|
||||||
|
}
|
||||||
|
state, diags = ctx.Apply(plan, m)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("apply: %s", diags.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs := state.Module(addrs.RootModuleInstance).OutputValues
|
||||||
|
// we check for null outputs be seeing that they don't exists
|
||||||
|
if _, ok := outputs["nullable_null_default"]; ok {
|
||||||
|
t.Error("nullable_null_default: expected no output value")
|
||||||
|
}
|
||||||
|
if _, ok := outputs["nullable_non_null_default"]; ok {
|
||||||
|
t.Error("nullable_non_null_default: expected no output value")
|
||||||
|
}
|
||||||
|
if _, ok := outputs["nullable_no_default"]; ok {
|
||||||
|
t.Error("nullable_no_default: expected no output value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := outputs["non_nullable_default"].Value; v.AsString() != "ok" {
|
||||||
|
t.Fatalf("incorrect 'non_nullable_default' output value: %#v\n", v)
|
||||||
|
}
|
||||||
|
if v := outputs["non_nullable_no_default"].Value; v.AsString() != "ok" {
|
||||||
|
t.Fatalf("incorrect 'non_nullable_no_default' output value: %#v\n", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -268,11 +268,15 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
|
|||||||
}
|
}
|
||||||
|
|
||||||
val, isSet := vals[addr.Name]
|
val, isSet := vals[addr.Name]
|
||||||
if !isSet {
|
switch {
|
||||||
if config.Default != cty.NilVal {
|
case !isSet:
|
||||||
return config.Default, diags
|
// The config loader will ensure there is a default if the value is not
|
||||||
}
|
// set at all.
|
||||||
return cty.UnknownVal(config.Type), diags
|
val = config.Default
|
||||||
|
|
||||||
|
case val.IsNull() && !config.Nullable && config.Default != cty.NilVal:
|
||||||
|
// If nullable=false a null value will use the configured default.
|
||||||
|
val = config.Default
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@ -286,8 +290,6 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
|
|||||||
Detail: fmt.Sprintf(`The resolved value of variable %q is not appropriate: %s.`, addr.Name, err),
|
Detail: fmt.Sprintf(`The resolved value of variable %q is not appropriate: %s.`, addr.Name, err),
|
||||||
Subject: &config.DeclRange,
|
Subject: &config.DeclRange,
|
||||||
})
|
})
|
||||||
// Stub out our return value so that the semantic checker doesn't
|
|
||||||
// produce redundant downstream errors.
|
|
||||||
val = cty.UnknownVal(config.Type)
|
val = cty.UnknownVal(config.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +253,23 @@ func (n *nodeModuleVariable) evalModuleCallArgument(ctx EvalContext, validateOnl
|
|||||||
val = cty.UnknownVal(n.Config.Type)
|
val = cty.UnknownVal(n.Config.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there is no default, we have to ensure that a null value is allowed
|
||||||
|
// for this variable.
|
||||||
|
if n.Config.Default == cty.NilVal && !n.Config.Nullable && val.IsNull() {
|
||||||
|
// The value cannot be null, and there is no configured default.
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: `Invalid variable value`,
|
||||||
|
Detail: fmt.Sprintf(`The variable %q is required, but the given value is null.`, n.Addr),
|
||||||
|
Subject: &n.Config.DeclRange,
|
||||||
|
})
|
||||||
|
// Stub out our return value so that the semantic checker doesn't
|
||||||
|
// produce redundant downstream errors.
|
||||||
|
val = cty.UnknownVal(n.Config.Type)
|
||||||
|
}
|
||||||
|
|
||||||
vals := make(map[string]cty.Value)
|
vals := make(map[string]cty.Value)
|
||||||
vals[name] = val
|
vals[name] = val
|
||||||
|
|
||||||
return vals, diags.ErrWithWarnings()
|
return vals, diags.ErrWithWarnings()
|
||||||
}
|
}
|
||||||
|
28
internal/terraform/testdata/apply-nullable-variables/main.tf
vendored
Normal file
28
internal/terraform/testdata/apply-nullable-variables/main.tf
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
module "mod" {
|
||||||
|
source = "./mod"
|
||||||
|
nullable_null_default = null
|
||||||
|
nullable_non_null_default = null
|
||||||
|
nullable_no_default = null
|
||||||
|
non_nullable_default = null
|
||||||
|
non_nullable_no_default = "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nullable_null_default" {
|
||||||
|
value = module.mod.nullable_null_default
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nullable_non_null_default" {
|
||||||
|
value = module.mod.nullable_non_null_default
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nullable_no_default" {
|
||||||
|
value = module.mod.nullable_no_default
|
||||||
|
}
|
||||||
|
|
||||||
|
output "non_nullable_default" {
|
||||||
|
value = module.mod.non_nullable_default
|
||||||
|
}
|
||||||
|
|
||||||
|
output "non_nullable_no_default" {
|
||||||
|
value = module.mod.non_nullable_no_default
|
||||||
|
}
|
59
internal/terraform/testdata/apply-nullable-variables/mod/main.tf
vendored
Normal file
59
internal/terraform/testdata/apply-nullable-variables/mod/main.tf
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// optional, and this can take null as an input
|
||||||
|
variable "nullable_null_default" {
|
||||||
|
// This is implied now as the default, and probably should be implied even
|
||||||
|
// when nullable=false is the default, so we're leaving this unset for the test.
|
||||||
|
// nullable = true
|
||||||
|
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// assigning null can still override the default.
|
||||||
|
variable "nullable_non_null_default" {
|
||||||
|
nullable = true
|
||||||
|
default = "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
// required, and assigning null is valid.
|
||||||
|
variable "nullable_no_default" {
|
||||||
|
nullable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// this combination is invalid
|
||||||
|
//variable "non_nullable_null_default" {
|
||||||
|
// nullable = false
|
||||||
|
// default = null
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
// assigning null will take the default
|
||||||
|
variable "non_nullable_default" {
|
||||||
|
nullable = false
|
||||||
|
default = "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
// required, but null is not a valid value
|
||||||
|
variable "non_nullable_no_default" {
|
||||||
|
nullable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nullable_null_default" {
|
||||||
|
value = var.nullable_null_default
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nullable_non_null_default" {
|
||||||
|
value = var.nullable_non_null_default
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nullable_no_default" {
|
||||||
|
value = var.nullable_no_default
|
||||||
|
}
|
||||||
|
|
||||||
|
output "non_nullable_default" {
|
||||||
|
value = var.non_nullable_default
|
||||||
|
}
|
||||||
|
|
||||||
|
output "non_nullable_no_default" {
|
||||||
|
value = var.non_nullable_no_default
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user