Skip to content
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

Introduce tfjsonpath Package and Sensitive and Unknown Plan Checks #154

Merged
merged 13 commits into from
Jul 24, 2023
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
}
SBGoods marked this conversation as resolved.
Show resolved Hide resolved

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": {
bendbennett marked this conversation as resolved.
Show resolved Hide resolved
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