Merge pull request #29832 from hashicorp/jbardin/nullable-variable

configs: explicitly nullable variable values
This commit is contained in:
James Bardin 2021-11-01 12:46:31 -04:00 committed by GitHub
commit b91d9435ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 209 additions and 7 deletions

View File

@ -56,6 +56,10 @@ func (v *Variable) merge(ov *Variable) hcl.Diagnostics {
if ov.ParsingMode != 0 {
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
// 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 {
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

View File

@ -24,6 +24,8 @@ func TestModuleOverrideVariable(t *testing.T) {
Description: "b_override description",
DescriptionSet: true,
Default: cty.StringVal("b_override"),
Nullable: false,
NullableSet: true,
Type: cty.String,
ConstraintType: cty.String,
ParsingMode: VariableParseLiteral,
@ -46,6 +48,8 @@ func TestModuleOverrideVariable(t *testing.T) {
Description: "base description",
DescriptionSet: true,
Default: cty.StringVal("b_override partial"),
Nullable: true,
NullableSet: false,
Type: cty.String,
ConstraintType: cty.String,
ParsingMode: VariableParseLiteral,

View File

@ -36,6 +36,12 @@ type Variable struct {
DescriptionSet 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
}
@ -110,6 +116,16 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
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 {
val, valDiags := attr.Expr.Value(nil)
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
}
@ -556,6 +581,9 @@ var variableBlockSchema = &hcl.BodySchema{
{
Name: "sensitive",
},
{
Name: "nullable",
},
},
Blocks: []hcl.BlockHeaderSchema{
{

View File

@ -0,0 +1,5 @@
variable "in" {
type = number
nullable = false
default = null
}

View File

@ -30,3 +30,15 @@ variable "sensitive_value" {
}
sensitive = true
}
variable "nullable" {
type = string
nullable = true
default = "ok"
}
variable "nullable_default_null" {
type = map(string)
nullable = true
default = null
}

View File

@ -1,4 +1,5 @@
variable "fully_overridden" {
nullable = false
default = "b_override"
description = "b_override description"
type = string

View File

@ -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)
}
}

View File

@ -268,11 +268,15 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
}
val, isSet := vals[addr.Name]
if !isSet {
if config.Default != cty.NilVal {
return config.Default, diags
}
return cty.UnknownVal(config.Type), diags
switch {
case !isSet:
// The config loader will ensure there is a default if the value is not
// set at all.
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
@ -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),
Subject: &config.DeclRange,
})
// Stub out our return value so that the semantic checker doesn't
// produce redundant downstream errors.
val = cty.UnknownVal(config.Type)
}

View File

@ -253,7 +253,23 @@ func (n *nodeModuleVariable) evalModuleCallArgument(ctx EvalContext, validateOnl
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[name] = val
return vals, diags.ErrWithWarnings()
}

View 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
}

View 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
}