Skip to content

Commit e241e1a

Browse files
authored
actions: return an error if config is omitted but the schema has required attrs (#37626)
* actions: improve handling of null config so we can properly report missing required arguments There are various methods in terraform that will let you know if you are missing required attributes - none of which work with nil inputs. This commit changes the handling, passing in an empty object if the config was null, and adding additional context to the error message when the config block is missing.
1 parent 0dfa115 commit e241e1a

File tree

3 files changed

+135
-66
lines changed

3 files changed

+135
-66
lines changed

internal/terraform/context_validate_test.go

Lines changed: 117 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3575,60 +3575,114 @@ func TestContext2Validate_queryList(t *testing.T) {
35753575
// Action Validation is largely exercised in context_plan_actions_test.go
35763576
func TestContext2Validate_action(t *testing.T) {
35773577
tests := map[string]struct {
3578-
config string
3579-
wantErr string
3578+
config string
3579+
wantErr string
3580+
expectValidateCalled bool
35803581
}{
35813582
"valid config": {
35823583
`
3583-
terraform {
3584-
required_providers {
3585-
test = {
3586-
source = "hashicorp/test"
3587-
version = "1.0.0"
3584+
terraform {
3585+
required_providers {
3586+
test = {
3587+
source = "hashicorp/test"
3588+
version = "1.0.0"
3589+
}
3590+
}
3591+
}
3592+
action "test_register" "foo" {
3593+
config {
3594+
host = "cmdb.snot"
3595+
}
3596+
}
3597+
resource "test_instance" "foo" {
3598+
lifecycle {
3599+
action_trigger {
3600+
events = [after_create]
3601+
actions = [action.test_register.foo]
3602+
}
3603+
}
3604+
}
3605+
`,
3606+
"",
3607+
true,
3608+
},
3609+
"validly null config": { // this doesn't seem likely, but let's make sure nothing panics.
3610+
`
3611+
terraform {
3612+
required_providers {
3613+
test = {
3614+
source = "hashicorp/test"
3615+
version = "1.0.0"
3616+
}
3617+
}
35883618
}
3589-
}
3590-
}
3591-
action "test_register" "foo" {
3592-
config {
3593-
host = "cmdb.snot"
3594-
}
3595-
}
3596-
resource "test_instance" "foo" {
3597-
lifecycle {
3598-
action_trigger {
3599-
events = [after_create]
3600-
actions = [action.test_register.foo]
3601-
}
3602-
}
3603-
}
3604-
`,
3619+
action "test_other" "foo" {
3620+
}
3621+
resource "test_instance" "foo" {
3622+
lifecycle {
3623+
action_trigger {
3624+
events = [after_create]
3625+
actions = [action.test_other.foo]
3626+
}
3627+
}
3628+
}
3629+
`,
36053630
"",
3631+
true,
36063632
},
36073633
"missing required config": {
36083634
`
3609-
terraform {
3610-
required_providers {
3611-
test = {
3612-
source = "hashicorp/test"
3613-
version = "1.0.0"
3635+
terraform {
3636+
required_providers {
3637+
test = {
3638+
source = "hashicorp/test"
3639+
version = "1.0.0"
3640+
}
3641+
}
36143642
}
3615-
}
3616-
}
3617-
action "test_register" "foo" {
3618-
config {}
3619-
}
3620-
resource "test_instance" "foo" {
3621-
lifecycle {
3622-
action_trigger {
3623-
events = [after_create]
3624-
actions = [action.test_register.foo]
3625-
}
3626-
}
3627-
}
3628-
`,
3629-
"host is null",
3643+
action "test_register" "foo" {
3644+
config {}
3645+
}
3646+
resource "test_instance" "foo" {
3647+
lifecycle {
3648+
action_trigger {
3649+
events = [after_create]
3650+
actions = [action.test_register.foo]
3651+
}
3652+
}
3653+
}
3654+
`,
3655+
"Missing required argument: The argument \"host\" is required, but no definition was found.",
3656+
false,
3657+
},
3658+
"invalid required config (provider validation)": {
3659+
`
3660+
terraform {
3661+
required_providers {
3662+
test = {
3663+
source = "hashicorp/test"
3664+
version = "1.0.0"
3665+
}
3666+
}
3667+
}
3668+
action "test_register" "foo" {
3669+
config {
3670+
host = "invalid"
3671+
}
3672+
}
3673+
resource "test_instance" "foo" {
3674+
lifecycle {
3675+
action_trigger {
3676+
events = [after_create]
3677+
actions = [action.test_register.foo]
3678+
}
3679+
}
3680+
}
3681+
`,
3682+
"Invalid value for required argument \"host\" because I said so",
3683+
true,
36303684
},
3631-
"invalid nil config config": {
3685+
"invalid nil config": {
36323686
`
36333687
terraform {
36343688
required_providers {
@@ -3649,7 +3703,8 @@ resource "test_instance" "foo" {
36493703
}
36503704
}
36513705
`,
3652-
"config is null",
3706+
"Missing required argument: The argument \"host\" is required, but was not set.",
3707+
false,
36533708
},
36543709
}
36553710

@@ -3669,6 +3724,14 @@ resource "test_instance" "foo" {
36693724
},
36703725
Actions: map[string]*providers.ActionSchema{
36713726
"test_register": {
3727+
ConfigSchema: &configschema.Block{
3728+
Attributes: map[string]*configschema.Attribute{
3729+
"host": {Type: cty.String, Required: true},
3730+
"output": {Type: cty.String, Computed: true, Optional: true},
3731+
},
3732+
},
3733+
},
3734+
"test_other": {
36723735
ConfigSchema: &configschema.Block{
36733736
Attributes: map[string]*configschema.Attribute{
36743737
"host": {Type: cty.String, Optional: true},
@@ -3678,12 +3741,8 @@ resource "test_instance" "foo" {
36783741
},
36793742
})
36803743
p.ValidateActionConfigFn = func(r providers.ValidateActionConfigRequest) (resp providers.ValidateActionConfigResponse) {
3681-
if r.Config.IsNull() {
3682-
resp.Diagnostics = resp.Diagnostics.Append(errors.New("config is null"))
3683-
return
3684-
}
3685-
if r.Config.GetAttr("host").IsNull() {
3686-
resp.Diagnostics = resp.Diagnostics.Append(errors.New("host is null"))
3744+
if r.Config.GetAttr("host") == cty.StringVal("invalid") {
3745+
resp.Diagnostics = resp.Diagnostics.Append(errors.New("Invalid value for required argument \"host\" because I said so"))
36873746
}
36883747
return
36893748
}
@@ -3695,10 +3754,6 @@ resource "test_instance" "foo" {
36953754
})
36963755

36973756
diags := ctx.Validate(m, nil)
3698-
if !p.ValidateActionConfigCalled {
3699-
t.Fatal("ValidateAction RPC was not called")
3700-
}
3701-
37023757
if test.wantErr != "" {
37033758
if !diags.HasErrors() {
37043759
t.Errorf("unexpected success\nwant errors: %s", test.wantErr)
@@ -3710,6 +3765,13 @@ resource "test_instance" "foo" {
37103765
t.Errorf("unexpected error\ngot: %s", diags.Err().Error())
37113766
}
37123767
}
3768+
if p.ValidateActionConfigCalled != test.expectValidateCalled {
3769+
if test.expectValidateCalled {
3770+
t.Fatal("provider Validate RPC was expected, but not called")
3771+
} else {
3772+
t.Fatal("unexpected Validate RCP call")
3773+
}
3774+
}
37133775
})
37143776
}
37153777
}

internal/terraform/node_action_abstract.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ func (n NodeAbstractAction) Name() string {
4444
// abstract action to a concrete one of some type.
4545
type ConcreteActionNodeFunc func(*NodeAbstractAction) dag.Vertex
4646

47-
// I'm not sure why my ConcreteActionNodeFUnction kept being nil in tests, but
48-
// this is much more robust. If it isn't a validate walk, we need
49-
// nodeExpandActionDeclaration.
47+
// DefaultConcreteActionNodeFunc is the default ConcreteActionNodeFunc used by
48+
// everything except validate.
5049
func DefaultConcreteActionNodeFunc(a *NodeAbstractAction) dag.Vertex {
5150
return &nodeExpandActionDeclaration{
5251
NodeAbstractAction: a,

internal/terraform/node_action_validate.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,24 @@ func (n *NodeValidatableAction) Execute(ctx EvalContext, _ walkOperation) tfdiag
8282
return diags
8383
}
8484

85-
var configVal cty.Value
86-
var valDiags tfdiags.Diagnostics
87-
if n.Config.Config != nil {
88-
configVal, _, valDiags = ctx.EvaluateBlock(n.Config.Config, schema.ConfigSchema, nil, keyData)
89-
diags = diags.Append(valDiags)
90-
if valDiags.HasErrors() {
85+
config := n.Config.Config
86+
if n.Config.Config == nil {
87+
config = hcl.EmptyBody()
88+
}
89+
90+
configVal, _, valDiags := ctx.EvaluateBlock(config, schema.ConfigSchema, nil, keyData)
91+
if valDiags.HasErrors() {
92+
// If there was no config block at all, we'll add a Context range to the returned diagnostic
93+
if n.Config.Config == nil {
94+
for _, diag := range valDiags.ToHCL() {
95+
diag.Context = &n.Config.DeclRange
96+
diags = diags.Append(diag)
97+
}
98+
return diags
99+
} else {
100+
diags = diags.Append(valDiags)
91101
return diags
92102
}
93-
} else {
94-
configVal = cty.NullVal(n.Schema.ConfigSchema.ImpliedType())
95103
}
96104

97105
// Use unmarked value for validate request

0 commit comments

Comments
 (0)