Skip to content

Commit

Permalink
Merge pull request #30752 from hashicorp/alisdair/condition-blocks-re…
Browse files Browse the repository at this point in the history
…sults-in-plan

core: Store condition block results in plan
  • Loading branch information
alisdair committed Apr 4, 2022
2 parents 1e56e1f + c5d10bd commit 3fbedf2
Show file tree
Hide file tree
Showing 32 changed files with 1,682 additions and 309 deletions.
86 changes: 86 additions & 0 deletions internal/addrs/check.go
@@ -0,0 +1,86 @@
package addrs

import "fmt"

// Check is the address of a check rule within a checkable object.
//
// This represents the check rule globally within a configuration, and is used
// during graph evaluation to identify a condition result object to update with
// the result of check rule evaluation.
//
// The check address is not distinct from resource traversals, and check rule
// values are not intended to be available to the language, so the address is
// not Referenceable.
//
// Note also that the check address is only relevant within the scope of a run,
// as reordering check blocks between runs will result in their addresses
// changing.
type Check struct {
Container Checkable
Type CheckType
Index int
}

func (c Check) String() string {
container := c.Container.String()
switch c.Type {
case ResourcePrecondition:
return fmt.Sprintf("%s.preconditions[%d]", container, c.Index)
case ResourcePostcondition:
return fmt.Sprintf("%s.postconditions[%d]", container, c.Index)
case OutputPrecondition:
return fmt.Sprintf("%s.preconditions[%d]", container, c.Index)
default:
// This should not happen
return fmt.Sprintf("%s.conditions[%d]", container, c.Index)
}
}

// Checkable is an interface implemented by all address types that can contain
// condition blocks.
type Checkable interface {
checkableSigil()

// Check returns the address of an individual check rule of a specified
// type and index within this checkable container.
Check(CheckType, int) Check
String() string
}

var (
_ Checkable = AbsResourceInstance{}
_ Checkable = AbsOutputValue{}
)

type checkable struct {
}

func (c checkable) checkableSigil() {
}

// CheckType describes the category of check.
//go:generate go run golang.org/x/tools/cmd/stringer -type=CheckType check.go
type CheckType int

const (
InvalidCondition CheckType = 0
ResourcePrecondition CheckType = 1
ResourcePostcondition CheckType = 2
OutputPrecondition CheckType = 3
)

// Description returns a human-readable description of the check type. This is
// presented in the user interface through a diagnostic summary.
func (c CheckType) Description() string {
switch c {
case ResourcePrecondition:
return "Resource precondition"
case ResourcePostcondition:
return "Resource postcondition"
case OutputPrecondition:
return "Module output value precondition"
default:
// This should not happen
return "Condition"
}
}
26 changes: 26 additions & 0 deletions internal/addrs/checktype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 75 additions & 0 deletions internal/addrs/output_value.go
Expand Up @@ -2,6 +2,10 @@ package addrs

import (
"fmt"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/tfdiags"
)

// OutputValue is the address of an output value, in the context of the module
Expand Down Expand Up @@ -34,6 +38,7 @@ func (v OutputValue) Absolute(m ModuleInstance) AbsOutputValue {
// configuration. It is related to but separate from ModuleCallOutput, which
// represents a module output from the perspective of its parent module.
type AbsOutputValue struct {
checkable
Module ModuleInstance
OutputValue OutputValue
}
Expand All @@ -49,6 +54,14 @@ func (m ModuleInstance) OutputValue(name string) AbsOutputValue {
}
}

func (v AbsOutputValue) Check(t CheckType, i int) Check {
return Check{
Container: v,
Type: t,
Index: i,
}
}

func (v AbsOutputValue) String() string {
if v.Module.IsRoot() {
return v.OutputValue.String()
Expand All @@ -60,6 +73,68 @@ func (v AbsOutputValue) Equal(o AbsOutputValue) bool {
return v.OutputValue == o.OutputValue && v.Module.Equal(o.Module)
}

func ParseAbsOutputValue(traversal hcl.Traversal) (AbsOutputValue, tfdiags.Diagnostics) {
path, remain, diags := parseModuleInstancePrefix(traversal)
if diags.HasErrors() {
return AbsOutputValue{}, diags
}

if len(remain) != 2 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "An output name is required.",
Subject: traversal.SourceRange().Ptr(),
})
return AbsOutputValue{}, diags
}

if remain.RootName() != "output" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Output address must start with \"output.\".",
Subject: remain[0].SourceRange().Ptr(),
})
return AbsOutputValue{}, diags
}

var name string
switch tt := remain[1].(type) {
case hcl.TraverseAttr:
name = tt.Name
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "An output name is required.",
Subject: remain[1].SourceRange().Ptr(),
})
return AbsOutputValue{}, diags
}

return AbsOutputValue{
Module: path,
OutputValue: OutputValue{
Name: name,
},
}, diags
}

func ParseAbsOutputValueStr(str string) (AbsOutputValue, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
diags = diags.Append(parseDiags)
if parseDiags.HasErrors() {
return AbsOutputValue{}, diags
}

addr, addrDiags := ParseAbsOutputValue(traversal)
diags = diags.Append(addrDiags)
return addr, diags
}

// ModuleCallOutput converts an AbsModuleOutput into a ModuleCallOutput,
// returning also the module instance that the ModuleCallOutput is relative
// to.
Expand Down
66 changes: 66 additions & 0 deletions internal/addrs/output_value_test.go
Expand Up @@ -2,7 +2,10 @@ package addrs

import (
"fmt"
"strings"
"testing"

"github.com/go-test/deep"
)

func TestAbsOutputValueInstanceEqual_true(t *testing.T) {
Expand Down Expand Up @@ -63,3 +66,66 @@ func TestAbsOutputValueInstanceEqual_false(t *testing.T) {
})
}
}

func TestParseAbsOutputValueStr(t *testing.T) {
tests := map[string]struct {
want AbsOutputValue
wantErr string
}{
"module.foo": {
wantErr: "An output name is required",
},
"module.foo.output": {
wantErr: "An output name is required",
},
"module.foo.boop.beep": {
wantErr: "Output address must start with \"output.\"",
},
"module.foo.output[0]": {
wantErr: "An output name is required",
},
"output": {
wantErr: "An output name is required",
},
"output[0]": {
wantErr: "An output name is required",
},
"output.boop": {
want: AbsOutputValue{
Module: RootModuleInstance,
OutputValue: OutputValue{
Name: "boop",
},
},
},
"module.foo.output.beep": {
want: AbsOutputValue{
Module: mustParseModuleInstanceStr("module.foo"),
OutputValue: OutputValue{
Name: "beep",
},
},
},
}

for input, tc := range tests {
t.Run(input, func(t *testing.T) {
got, diags := ParseAbsOutputValueStr(input)
for _, problem := range deep.Equal(got, tc.want) {
t.Errorf(problem)
}
if len(diags) > 0 {
gotErr := diags.Err().Error()
if tc.wantErr == "" {
t.Errorf("got error, expected success: %s", gotErr)
} else if !strings.Contains(gotErr, tc.wantErr) {
t.Errorf("unexpected error\n got: %s\nwant: %s", gotErr, tc.wantErr)
}
} else {
if tc.wantErr != "" {
t.Errorf("got success, expected error: %s", tc.wantErr)
}
}
})
}
}
9 changes: 9 additions & 0 deletions internal/addrs/resource.go
Expand Up @@ -210,6 +210,7 @@ func (r AbsResource) UniqueKey() UniqueKey {
// AbsResourceInstance is an absolute address for a resource instance under a
// given module path.
type AbsResourceInstance struct {
checkable
targetable
Module ModuleInstance
Resource ResourceInstance
Expand Down Expand Up @@ -280,6 +281,14 @@ func (r AbsResourceInstance) AffectedAbsResource() AbsResource {
}
}

func (r AbsResourceInstance) Check(t CheckType, i int) Check {
return Check{
Container: r,
Type: t,
Index: i,
}
}

func (r AbsResourceInstance) Equal(o AbsResourceInstance) bool {
return r.Module.Equal(o.Module) && r.Resource.Equal(o.Resource)
}
Expand Down
26 changes: 26 additions & 0 deletions internal/command/jsonplan/condition.go
@@ -0,0 +1,26 @@
package jsonplan

// conditionResult is the representation of an evaluated condition block.
type conditionResult struct {
// checkAddress is the globally-unique address of the condition block. This
// is intentionally unexported as it is an implementation detail.
checkAddress string

// Address is the absolute address of the condition's containing object.
Address string `json:"address,omitempty"`

// Type is the condition block type, and is one of ResourcePrecondition,
// ResourcePostcondition, or OutputPrecondition.
Type string `json:"condition_type,omitempty"`

// Result is true if the condition succeeds, and false if it fails or is
// known only at apply time.
Result bool `json:"result"`

// Unknown is true if the condition can only be evaluated at apply time.
Unknown bool `json:"unknown"`

// ErrorMessage is the custom error for a failing condition. It is only
// present if the condition fails.
ErrorMessage string `json:"error_message,omitempty"`
}
28 changes: 28 additions & 0 deletions internal/command/jsonplan/plan.go
Expand Up @@ -39,6 +39,7 @@ type plan struct {
PriorState json.RawMessage `json:"prior_state,omitempty"`
Config json.RawMessage `json:"configuration,omitempty"`
RelevantAttributes []resourceAttr `json:"relevant_attributes,omitempty"`
Conditions []conditionResult `json:"condition_results,omitempty"`
}

func newPlan() *plan {
Expand Down Expand Up @@ -177,6 +178,12 @@ func Marshal(
return nil, fmt.Errorf("error in marshaling output changes: %s", err)
}

// output.Conditions
err = output.marshalConditionResults(p.Conditions)
if err != nil {
return nil, fmt.Errorf("error in marshaling condition results: %s", err)
}

// output.PriorState
if sf != nil && !sf.State.Empty() {
output.PriorState, err = jsonstate.Marshal(sf, schemas)
Expand Down Expand Up @@ -476,6 +483,27 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error {
return nil
}

func (p *plan) marshalConditionResults(conditions plans.Conditions) error {
for addr, c := range conditions {
cr := conditionResult{
checkAddress: addr,
Address: c.Address.String(),
Type: c.Type.String(),
ErrorMessage: c.ErrorMessage,
}
if c.Result.IsKnown() {
cr.Result = c.Result.True()
} else {
cr.Unknown = true
}
p.Conditions = append(p.Conditions, cr)
}
sort.Slice(p.Conditions, func(i, j int) bool {
return p.Conditions[i].checkAddress < p.Conditions[j].checkAddress
})
return nil
}

func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) error {
// marshal the planned changes into a module
plan, err := marshalPlannedValues(changes, schemas)
Expand Down

0 comments on commit 3fbedf2

Please sign in to comment.