Skip to content

Commit 2084154

Browse files
committed
actions: return an error if config is null but the schema has required attributes
There are various methods in terraform that will let you know if you are missing required attributes - none of which work with hcl.EmptyBody or nil inputs. This adds some helper methods so terraform can check if an action schema has required arguments and returns an error if the config is null but arguments are required. I did not bother with an exhaustive list of missing args, but I'm open to making that change. I'd like to get eyes on this section before I move on to modifying what we send to the provider.
1 parent 719aefd commit 2084154

File tree

3 files changed

+130
-67
lines changed

3 files changed

+130
-67
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: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,18 @@ 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() {
91-
return diags
92-
}
85+
// If the action has no "config" attribute at all, we'll send the provider
86+
// an empty body - config = {} and omitted config entirely are the same, internally.
87+
var config hcl.Body
88+
if n.Config.Config == nil {
89+
config = hcl.EmptyBody()
9390
} else {
94-
configVal = cty.NullVal(n.Schema.ConfigSchema.ImpliedType())
91+
config = n.Config.Config
92+
}
93+
configVal, _, valDiags := ctx.EvaluateBlock(config, schema.ConfigSchema, nil, keyData)
94+
diags = diags.Append(valDiags)
95+
if valDiags.HasErrors() {
96+
return diags
9597
}
9698

9799
// Use unmarked value for validate request

0 commit comments

Comments
 (0)