Skip to content

Commit

Permalink
Introduce tfjsonpath Package and Sensitive and Unknown Plan Checks (#…
Browse files Browse the repository at this point in the history
…154)

* Implement `tfjsonpath` package

* Implement `ExpectSensitiveValue` plan check

* Implement `ExpectUnknownValue` plan check

* Update website documentation

* Resolve linting errors

* Add copyright headers

* Add test for `null` values

* Reword bool assertion error

* Add Changie Entries

* Correct documentation typos

Co-authored-by: Brian Flad <[email protected]>

* Add error for when resource is not found

* Add additional wording to attribute path subsections

* Resolve linting errors

---------

Co-authored-by: Brian Flad <[email protected]>
  • Loading branch information
SBGoods and bflad authored Jul 24, 2023
1 parent 462c64f commit ebffbe1
Show file tree
Hide file tree
Showing 14 changed files with 1,907 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20230720-163447.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'tfjsonpath: Introduced new `tfjsonpath` package which contains methods that
allow traversal of Terraform JSON data'
time: 2023-07-20T16:34:47.373683-04:00
custom:
Issue: "154"
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20230720-163627.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'plancheck: Added `ExpectUnknownValue` built-in plan check, which asserts that
a given attribute has an unknown value'
time: 2023-07-20T16:36:27.361538-04:00
custom:
Issue: "154"
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20230720-163828.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'plancheck: Added `ExpectSensitiveValue` built-in plan check, which asserts
that a given attribute has a sensitive value'
time: 2023-07-20T16:38:28.94511-04:00
custom:
Issue: "154"
61 changes: 61 additions & 0 deletions plancheck/expect_sensitive_value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package plancheck

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)

var _ PlanCheck = expectSensitiveValue{}

type expectSensitiveValue struct {
resourceAddress string
attributePath tfjsonpath.Path
}

// CheckPlan implements the plan check logic.
func (e expectSensitiveValue) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) {

for _, rc := range req.Plan.ResourceChanges {
if e.resourceAddress != rc.Address {
continue
}

result, err := tfjsonpath.Traverse(rc.Change.AfterSensitive, e.attributePath)
if err != nil {
resp.Error = err
return
}

isSensitive, ok := result.(bool)
if !ok {
resp.Error = fmt.Errorf("invalid path: the path value cannot be asserted as bool")
return
}

if !isSensitive {
resp.Error = fmt.Errorf("attribute at path is not sensitive")
return
}

return
}

resp.Error = fmt.Errorf("%s - Resource not found in plan ResourceChanges", e.resourceAddress)
}

// ExpectSensitiveValue returns a plan check that asserts that the specified attribute at the given resource has a sensitive value.
//
// Due to implementation differences between the terraform-plugin-sdk and the terraform-plugin-framework, representation of sensitive
// values may differ. For example, terraform-plugin-sdk based providers may have less precise representations of sensitive values, such
// as marking whole maps as sensitive rather than individual element values.
func ExpectSensitiveValue(resourceAddress string, attributePath tfjsonpath.Path) PlanCheck {
return expectSensitiveValue{
resourceAddress: resourceAddress,
attributePath: attributePath,
}
}
300 changes: 300 additions & 0 deletions plancheck/expect_sensitive_value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package plancheck_test

import (
"context"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)

func Test_ExpectSensitiveValue_SensitiveStringAttribute(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
ProviderFactories: map[string]func() (*schema.Provider, error){
"test": func() (*schema.Provider, error) { //nolint:unparam // required signature
return testProviderSensitive(), nil
},
},
Steps: []r.TestStep{
{
Config: `
resource "test_resource" "one" {
sensitive_string_attribute = "test"
}
`,
ConfigPlanChecks: r.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectSensitiveValue("test_resource.one",
tfjsonpath.New("sensitive_string_attribute")),
},
},
},
},
})
}

func Test_ExpectSensitiveValue_SensitiveListAttribute(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
ProviderFactories: map[string]func() (*schema.Provider, error){
"test": func() (*schema.Provider, error) { //nolint:unparam // required signature
return testProviderSensitive(), nil
},
},
Steps: []r.TestStep{
{
Config: `
resource "test_resource" "one" {
sensitive_list_attribute = ["value1"]
}
`,
ConfigPlanChecks: r.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectSensitiveValue("test_resource.one",
tfjsonpath.New("sensitive_list_attribute")),
},
},
},
},
})
}

func Test_ExpectSensitiveValue_SensitiveSetAttribute(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
ProviderFactories: map[string]func() (*schema.Provider, error){
"test": func() (*schema.Provider, error) { //nolint:unparam // required signature
return testProviderSensitive(), nil
},
},
Steps: []r.TestStep{
{
Config: `
resource "test_resource" "one" {
sensitive_set_attribute = ["value1"]
}
`,
ConfigPlanChecks: r.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectSensitiveValue("test_resource.one",
tfjsonpath.New("sensitive_set_attribute")),
},
},
},
},
})
}

func Test_ExpectSensitiveValue_SensitiveMapAttribute(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
ProviderFactories: map[string]func() (*schema.Provider, error){
"test": func() (*schema.Provider, error) { //nolint:unparam // required signature
return testProviderSensitive(), nil
},
},
Steps: []r.TestStep{
{
Config: `
resource "test_resource" "one" {
sensitive_map_attribute = {
key1 = "value1",
}
}
`,
ConfigPlanChecks: r.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectSensitiveValue("test_resource.one",
tfjsonpath.New("sensitive_map_attribute")),
},
},
},
},
})
}

func Test_ExpectSensitiveValue_ListNestedBlock_SensitiveAttribute(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
ProviderFactories: map[string]func() (*schema.Provider, error){
"test": func() (*schema.Provider, error) { //nolint:unparam // required signature
return testProviderSensitive(), nil
},
},
Steps: []r.TestStep{
{
Config: `
resource "test_resource" "one" {
list_nested_block_sensitive_attribute {
sensitive_list_nested_block_attribute = "sensitive-test"
list_nested_block_attribute = "test"
}
}
`,
ConfigPlanChecks: r.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectSensitiveValue("test_resource.one",
tfjsonpath.New("list_nested_block_sensitive_attribute").AtSliceIndex(0).
AtMapKey("sensitive_list_nested_block_attribute")),
},
},
},
},
})
}

func Test_ExpectSensitiveValue_SetNestedBlock_SensitiveAttribute(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
ProviderFactories: map[string]func() (*schema.Provider, error){
"test": func() (*schema.Provider, error) { //nolint:unparam // required signature
return testProviderSensitive(), nil
},
},
Steps: []r.TestStep{
{
Config: `
resource "test_resource" "one" {
set_nested_block_sensitive_attribute {
sensitive_set_nested_block_attribute = "sensitive-test"
set_nested_block_attribute = "test"
}
}
`,
ConfigPlanChecks: r.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectSensitiveValue("test_resource.one",
tfjsonpath.New("set_nested_block_sensitive_attribute")),
},
},
},
},
})
}

func Test_ExpectSensitiveValue_ExpectError_ResourceNotFound(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
ProviderFactories: map[string]func() (*schema.Provider, error){
"test": func() (*schema.Provider, error) { //nolint:unparam // required signature
return testProviderSensitive(), nil
},
},
Steps: []r.TestStep{
{
Config: `
resource "test_resource" "one" {}
`,
ConfigPlanChecks: r.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectSensitiveValue("test_resource.two", tfjsonpath.New("set_attribute")),
},
},
ExpectError: regexp.MustCompile(`test_resource.two - Resource not found in plan ResourceChanges`),
},
},
})
}

func testProviderSensitive() *schema.Provider {
return &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"test_resource": {
CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics {
d.SetId("test")
return nil
},
UpdateContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
return nil
},
DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
return nil
},
ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
return nil
},
Schema: map[string]*schema.Schema{
"sensitive_string_attribute": {
Sensitive: true,
Optional: true,
Type: schema.TypeString,
},
"sensitive_list_attribute": {
Sensitive: true,
Type: schema.TypeList,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Optional: true,
},
"sensitive_set_attribute": {
Sensitive: true,
Type: schema.TypeSet,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Optional: true,
},
"sensitive_map_attribute": {
Sensitive: true,
Type: schema.TypeMap,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Optional: true,
},
"list_nested_block_sensitive_attribute": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"list_nested_block_attribute": {
Type: schema.TypeString,
Optional: true,
},
"sensitive_list_nested_block_attribute": {
Sensitive: true,
Type: schema.TypeString,
Optional: true,
},
},
},
},
"set_nested_block_sensitive_attribute": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"set_nested_block_attribute": {
Type: schema.TypeString,
Optional: true,
},
"sensitive_set_nested_block_attribute": {
Sensitive: true,
Type: schema.TypeString,
Optional: true,
},
},
},
},
},
},
},
}
}
Loading

0 comments on commit ebffbe1

Please sign in to comment.