-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding State Checks for Known Type and Value, and Sensitive Checks #275
Conversation
* Configuring when state checks are executed. * Testing that state checks are executed.
…or checking of null values (#266)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything looks great to me, good work @bendbennett! Left a couple comments but nothing major.
Love all the docs as well ❤️
|
||
// CheckValue determines whether the passed value is of nil. | ||
func (v nullExact) CheckValue(other any) error { | ||
if other != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I vaguely remember checking an any
/interface{}
for nil and I believe it's not as straightforward as this for things like maps. (I don't know if the underlying tfjson
implementation will expose this problem, so it may not be relevant)
And delve shows it slightly different, but still should be considered null
:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This article explains the problem a bit, may have to dip into some reflection if we need to cover nil cases like maps and slices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for raising this.
I've examined the values returned when using the tfjson
implementation.
With a schema that looks as follows:
func (e *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
/* ... */
"list_attribute": schema.ListAttribute{
Optional: true,
/* ... */
},
"map_attribute": schema.MapAttribute{
Optional: true,
/* ... */
},
"object_attribute": schema.ObjectAttribute{
Optional: true,
AttributeTypes: map[string]attr.Type{
/* ... */
},
},
"set_attribute": schema.SetAttribute{
Optional: true,
/* ... */
},
"list_nested_attribute": schema.ListNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
/* ... */
},
},
},
"map_nested_attribute": schema.MapNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
/* ... */
},
},
},
"set_nested_attribute": schema.SetNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
/* ... */
},
},
},
"single_nested_attribute": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
/* ... */
},
},
},
Blocks: map[string]schema.Block{
"list_nested_block": schema.ListNestedBlock{
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
/* ... */
},
Blocks: map[string]schema.Block{
"list_nested_nested_block": schema.ListNestedBlock{
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
/* ... */
},
},
},
},
},
},
"set_nested_block": schema.SetNestedBlock{
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
/* ... */
},
Blocks: map[string]schema.Block{
"set_nested_nested_block": schema.SetNestedBlock{
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
/* ... */
},
},
},
},
},
},
"single_nested_block": schema.SingleNestedBlock{
Attributes: map[string]schema.Attribute{
/* ... */
},
},
},
}
}
The plan output looks as follows:
{
"format_version": "1.2",
"terraform_version": "1.7.0",
"planned_values": {
"root_module": {
"resources": [
{
"address": "example_resource.example",
"mode": "managed",
"type": "example_resource",
"name": "example",
"provider_name": "registry.terraform.io/bendbennett/playground",
"schema_version": 0,
"values": {
"list_attribute": null,
"list_nested_attribute": null,
"list_nested_block": [],
"map_attribute": null,
"map_nested_attribute": null,
"object_attribute": null,
"set_attribute": null,
"set_nested_attribute": null,
"set_nested_block": [],
"single_nested_attribute": null,
"single_nested_block": null,
},
"sensitive_values": {
"list_nested_block": [],
"set_nested_block": []
}
}
]
}
},
"resource_changes": [
{
"address": "example_resource.example",
"mode": "managed",
"type": "example_resource",
"name": "example",
"provider_name": "registry.terraform.io/bendbennett/playground",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"list_attribute": null,
"list_nested_attribute": null,
"list_nested_block": [],
"map_attribute": null,
"map_nested_attribute": null,
"object_attribute": null,
"set_attribute": null,
"set_nested_attribute": null,
"set_nested_block": [],
"single_nested_attribute": null,
"single_nested_block": null,
},
"after_unknown": {
"id": true,
"list_nested_block": [],
"set_nested_block": []
},
"before_sensitive": false,
"after_sensitive": {
"list_nested_block": [],
"set_nested_block": []
}
}
}
],
"configuration": {
"provider_config": {
"example": {
"name": "example",
"full_name": "registry.terraform.io/bendbennett/playground"
},
"playground": {
"name": "playground",
"full_name": "registry.terraform.io/bendbennett/playground"
}
},
"root_module": {
"resources": [
{
"address": "example_resource.example",
"mode": "managed",
"type": "example_resource",
"name": "example",
"provider_config_key": "example",
"schema_version": 0
}
]
}
},
"timestamp": "2024-01-22T10:12:40Z",
"errored": false
}
The state looks as follows:
{
"version": 4,
"terraform_version": "1.7.0",
"serial": 1,
"lineage": "2c8dadf4-11dd-22b9-c419-5d07aa808890",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "example_resource",
"name": "example",
"provider": "provider[\"registry.terraform.io/bendbennett/playground\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"bool_attribute": null,
"float64_attribute": null,
"id": "example-id",
"int64_attribute": null,
"list_attribute": null,
"list_nested_attribute": null,
"list_nested_block": [],
"map_attribute": null,
"map_nested_attribute": null,
"number_attribute": null,
"object_attribute": null,
"set_attribute": null,
"set_nested_attribute": null,
"set_nested_block": [],
"single_nested_attribute": null,
"single_nested_block": null,
"string_attribute": null
},
"sensitive_attributes": []
}
]
}
],
"check_results": null
}
Debugging during test execution shows the following:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added some additional test coverage to the following:
TestExpectKnownValue_CheckState_AttributeValueNull
TestExpectKnownOutputValueAtPath_CheckState_AttributeValueNull
TestExpectKnownValue_CheckPlan_AttributeValueNull
TestExpectKnownOutputValueAtPath_CheckPlan_AttributeValueNull
TestExpectKnownOutputValue_CheckPlan_AttributeValueNull
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Today I learned! I think we have historically avoided these sorts of issues by not performing (re-)assignment like that article shows. Hopefully we can rely on the static analysis tooling to catch the situation if for some reason it does get introduced and the unit testing does not catch it, because I'm a proponent of simpler == better where possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding the tests, good to know it shouldn't be a problem for us 🙂
I'm a proponent of simpler == better where possible.
Agreed 👍🏻
…d `ExpectNullOutputValueAtPath` plan checks (#266)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall very thrilled with the direction here! Great job. 🚀
knownvalue/null.go
Outdated
|
||
// NullExact returns a Check for asserting equality nil | ||
// and the value passed to the CheckValue method. | ||
func NullExact() nullExact { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bikeshed (I'm so sorry): Are there other checks that could be run against a null value? Maybe this can be just Null
if there is only the possibility for the value being null or not? I guess you could also say the same for boolean checks, but all the other typed checks could have additional checks. 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Agree with the suggested naming change here. I've made the following changes:
NullExact
=>Null
nullExact
=>null
BoolExact
=>bool
boolExact
=>boolValue
The docs have been updated in accordance with these changes.
helper/resource/testing.go
Outdated
@@ -795,6 +803,10 @@ type RefreshPlanChecks struct { | |||
PostRefresh []plancheck.PlanCheck | |||
} | |||
|
|||
// ConfigStateChecks runs all state checks in the slice. This occurs after the apply and refresh of a Config test are run. | |||
// All errors by state checks in this slice are aggregated, reported, and will result in a test failure. | |||
type ConfigStateChecks []statecheck.StateCheck |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Open question: do we need this additional exported type? I'm guessing this was for a little bit of consistency with ConfigPlanChecks
, however that is there because we need a structure to capture the various times plan checks could be ran. In my experience, typically you would introduce something like this if there were plans to attach methods to the type, but since there are not any here right now, I'm curious if there were future plans for that. Another option would also be to put this type in the statecheck
package, so all the implementation details live in one place. I'm just wondering if we should treat this similar to how the plan checks do use []plancheck.PlanCheck
directly once its at that "level". Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're assumption is correct, the exported ConfigStateChecks
type was purely for consistency. I've removed the type and replaced with the usage of []statecheck.StateCheck
throughout.
// Run post-apply, post-refresh state checks | ||
if len(step.ConfigStateChecks) > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Historically we have ran state checks immediately after apply and before the additional plan checks (e.g. line 181 area) -- do we want to also do that here for consistency/ease of migration? In reality, the timing of the checks will determine which errors developers might see first: whether it be unexpectedly non-empty plans after apply or whether their state assertions are incorrect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved the execution of the ConfigStateChecks
so that they run immediately after any Check
(i.e., line 210).
result = append(result, resp.Error) | ||
} | ||
|
||
return errors.Join(result...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No more resource.ComposeAggregateTestCheckFunc
vs resource.ComposeTestCheckFunc
debates!
I'm going to lock this pull request because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active contributions. |
Reference: #266
This PR forms part of the changes outlined in #266, specifically it includes state checks for the following:
ExpectKnownValue
ExpectKnownOutputValue
ExpectKnownOutputValueAtPath
ExpectSensitiveValue
The implementation in this PR differs from the way in which checks for known type and value are currently being handled for plan checks, in that the
ExpectKnown<Value|OutputValue|OutputValueAtPath>
state checks allow for explicitly checking for a null value by using a newly introduced null known value check type. An alternative can be seen in the plan check implementations, where theExpectKnown<Value|OutputValue|OutputValueAtPath>
plan checks return an error if a null value is found, and instead there are explicit checks for null values (e.g.,ExpectNullOutputValue
).In this context, a significant difference between
tfjson.Plan
andtfjson.State
is the handling of null output values. In the case oftfjson.Plan
, an output that specifies an attribute, for instance, which is null appears in the plan, whereas null output values do not appear in state (refer to Allow null value outputs to be present in json output). As a consequence, there is no way for state checks that leveragetfjson.State
to discriminate between output values that are null, and output values that do not exist. However, it is worth noting that output values that reference, for instance, an object that has attributes which are null will appear intfjson.State
.Together this raises the question of whether we want to:
ExpectKnown<Value|OutputValue|OutputValueAtPath>
to handle detection of nulls (to bring them into line with the state checks forExpectKnown<Value|OutputValue|OutputValueAtPath>
in this PR).ExpectKnown<Value|OutputValue|OutputValueAtPath>
in this PR to align their behaviour with the analogous plan checks (i.e., return an error when null is detected).Further Considerations
ExpectEmptyPlan
andExpectNonEmptyPlan
if there is possible value in having these built-in checks made available.