Skip to content

Commit 9256074

Browse files
authored
Actions in plan/changes (#37320)
* Add actions to the plans and change * jsonplan - ignoring LinkedResources for now, those are not in the MVP * pausing here: we'll work on the plan rendering later
1 parent 7199fbd commit 9256074

File tree

26 files changed

+1072
-353
lines changed

26 files changed

+1072
-353
lines changed

internal/addrs/action.go

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"fmt"
88
"strings"
99

10+
"github.com/hashicorp/hcl/v2"
11+
"github.com/hashicorp/hcl/v2/hclsyntax"
12+
1013
"github.com/hashicorp/terraform/internal/tfdiags"
1114
)
1215

@@ -284,7 +287,7 @@ func (a AbsActionInstance) UniqueKey() UniqueKey {
284287
return absActionInstanceKey(a.String())
285288
}
286289

287-
func (r absActionInstanceKey) uniqueKeySigil() {}
290+
func (a absActionInstanceKey) uniqueKeySigil() {}
288291

289292
// ConfigAction is the address for an action within the configuration.
290293
type ConfigAction struct {
@@ -337,7 +340,7 @@ type AbsActionInvocationInstance struct {
337340
TriggerIndex int
338341

339342
// TriggerBlockSourceRange is the location of the action_trigger block
340-
// within the resources lifecyclye block that triggered this action.
343+
// within the resources lifecycle block that triggered this action.
341344
TriggerBlockSourceRange *tfdiags.SourceRange
342345

343346
// ActionReferenceSourceRange is the location of the action reference
@@ -348,3 +351,152 @@ type AbsActionInvocationInstance struct {
348351
func (a AbsActionInvocationInstance) String() string {
349352
return fmt.Sprintf("%s.%d.%s", a.TriggeringResource.String(), a.TriggerIndex, a.Action.String())
350353
}
354+
355+
// ParseAbsActionInstanceStr is a helper wrapper around
356+
// ParseAbsActionInstance that takes a string and parses it with the HCL
357+
// native syntax traversal parser before interpreting it.
358+
//
359+
// Error diagnostics are returned if either the parsing fails or the analysis
360+
// of the traversal fails. There is no way for the caller to distinguish the
361+
// two kinds of diagnostics programmatically. If error diagnostics are returned
362+
// the returned address may be incomplete.
363+
//
364+
// Since this function has no context about the source of the given string,
365+
// any returned diagnostics will not have meaningful source location
366+
// information.
367+
func ParseAbsActionInstanceStr(str string) (AbsActionInstance, tfdiags.Diagnostics) {
368+
var diags tfdiags.Diagnostics
369+
370+
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
371+
diags = diags.Append(parseDiags)
372+
if parseDiags.HasErrors() {
373+
return AbsActionInstance{}, diags
374+
}
375+
376+
addr, addrDiags := ParseAbsActionInstance(traversal)
377+
diags = diags.Append(addrDiags)
378+
return addr, diags
379+
}
380+
381+
// ParseAbsActionInstance attempts to interpret the given traversal as an
382+
// absolute action instance address, using the same syntax as expected by
383+
// ParseTarget.
384+
//
385+
// If no error diagnostics are returned, the returned target includes the
386+
// address that was extracted and the source range it was extracted from.
387+
//
388+
// If error diagnostics are returned then the AbsResource value is invalid and
389+
// must not be used.
390+
func ParseAbsActionInstance(traversal hcl.Traversal) (AbsActionInstance, tfdiags.Diagnostics) {
391+
moduleAddr, remain, diags := parseModuleInstancePrefix(traversal, false)
392+
if diags.HasErrors() {
393+
return AbsActionInstance{}, diags
394+
}
395+
396+
if remain.IsRelative() {
397+
// (relative means that there's either nothing left or what's next isn't an identifier)
398+
diags = diags.Append(&hcl.Diagnostic{
399+
Severity: hcl.DiagError,
400+
Summary: "Invalid action address",
401+
Detail: "Module path must be followed by an action instance address.",
402+
Subject: remain.SourceRange().Ptr(),
403+
})
404+
return AbsActionInstance{}, diags
405+
}
406+
407+
if remain.RootName() != "action" {
408+
diags = diags.Append(&hcl.Diagnostic{
409+
Severity: hcl.DiagError,
410+
Summary: "Invalid address",
411+
Detail: "Action address must start with \"action.\".",
412+
Subject: remain[0].SourceRange().Ptr(),
413+
})
414+
return AbsActionInstance{}, diags
415+
}
416+
remain = remain[1:]
417+
418+
if len(remain) < 2 {
419+
diags = diags.Append(&hcl.Diagnostic{
420+
Severity: hcl.DiagError,
421+
Summary: "Invalid address",
422+
Detail: "Action specification must include an action type and name.",
423+
Subject: remain.SourceRange().Ptr(),
424+
})
425+
return AbsActionInstance{}, diags
426+
}
427+
428+
var actionType, name string
429+
switch tt := remain[0].(type) {
430+
case hcl.TraverseRoot:
431+
actionType = tt.Name
432+
case hcl.TraverseAttr:
433+
actionType = tt.Name
434+
default:
435+
diags = diags.Append(&hcl.Diagnostic{
436+
Severity: hcl.DiagError,
437+
Summary: "Invalid action address",
438+
Detail: "An action name is required.",
439+
Subject: remain[0].SourceRange().Ptr(),
440+
})
441+
return AbsActionInstance{}, diags
442+
}
443+
444+
switch tt := remain[1].(type) {
445+
case hcl.TraverseAttr:
446+
name = tt.Name
447+
default:
448+
diags = diags.Append(&hcl.Diagnostic{
449+
Severity: hcl.DiagError,
450+
Summary: "Invalid address",
451+
Detail: "An action name is required.",
452+
Subject: remain[1].SourceRange().Ptr(),
453+
})
454+
return AbsActionInstance{}, diags
455+
}
456+
457+
remain = remain[2:]
458+
switch len(remain) {
459+
case 0:
460+
return moduleAddr.ActionInstance(actionType, name, NoKey), diags
461+
case 1:
462+
switch tt := remain[0].(type) {
463+
case hcl.TraverseIndex:
464+
key, err := ParseInstanceKey(tt.Key)
465+
if err != nil {
466+
diags = diags.Append(&hcl.Diagnostic{
467+
Severity: hcl.DiagError,
468+
Summary: "Invalid address",
469+
Detail: fmt.Sprintf("Invalid resource instance key: %s.", err),
470+
Subject: remain[0].SourceRange().Ptr(),
471+
})
472+
return AbsActionInstance{}, diags
473+
}
474+
return moduleAddr.ActionInstance(actionType, name, key), diags
475+
case hcl.TraverseSplat:
476+
// Not yet supported!
477+
diags = diags.Append(&hcl.Diagnostic{
478+
Severity: hcl.DiagError,
479+
Summary: "Invalid address",
480+
Detail: "Action instance key must be given in square brackets.",
481+
Subject: remain[0].SourceRange().Ptr(),
482+
})
483+
return AbsActionInstance{}, diags
484+
default:
485+
diags = diags.Append(&hcl.Diagnostic{
486+
Severity: hcl.DiagError,
487+
Summary: "Invalid address",
488+
Detail: "Action instance key must be given in square brackets.",
489+
Subject: remain[0].SourceRange().Ptr(),
490+
})
491+
return AbsActionInstance{}, diags
492+
}
493+
default:
494+
diags = diags.Append(&hcl.Diagnostic{
495+
Severity: hcl.DiagError,
496+
Summary: "Invalid address",
497+
Detail: "Unexpected extra operators after address.",
498+
Subject: remain[1].SourceRange().Ptr(),
499+
})
500+
return AbsActionInstance{}, diags
501+
}
502+
}

internal/addrs/action_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,75 @@ func TestAbsActionUniqueKey(t *testing.T) {
341341
})
342342
}
343343
}
344+
345+
func TestParseAbsActionInstance(t *testing.T) {
346+
tests := []struct {
347+
input string
348+
want AbsActionInstance
349+
expectErr bool
350+
}{
351+
{
352+
"",
353+
AbsActionInstance{},
354+
true,
355+
},
356+
{
357+
"action.example.foo",
358+
AbsActionInstance{
359+
Action: ActionInstance{
360+
Action: Action{
361+
Type: "example",
362+
Name: "foo",
363+
},
364+
Key: NoKey,
365+
},
366+
Module: RootModuleInstance,
367+
},
368+
false,
369+
},
370+
{
371+
"action.example.foo[0]",
372+
AbsActionInstance{
373+
Action: ActionInstance{
374+
Action: Action{
375+
Type: "example",
376+
Name: "foo",
377+
},
378+
Key: IntKey(0),
379+
},
380+
Module: RootModuleInstance,
381+
},
382+
false,
383+
},
384+
{
385+
"action.example.foo[\"bar\"]",
386+
AbsActionInstance{
387+
Action: ActionInstance{
388+
Action: Action{
389+
Type: "example",
390+
Name: "foo",
391+
},
392+
Key: StringKey("bar"),
393+
},
394+
Module: RootModuleInstance,
395+
},
396+
false,
397+
},
398+
}
399+
400+
for _, test := range tests {
401+
t.Run(fmt.Sprintf("ParseAbsActionStr(%s)", test.input), func(t *testing.T) {
402+
got, gotDiags := ParseAbsActionInstanceStr(test.input)
403+
if gotDiags.HasErrors() != test.expectErr {
404+
if !test.expectErr {
405+
t.Fatalf("wrong results! Expected success, got error: %s\n", gotDiags.Err())
406+
} else {
407+
t.Fatal("wrong results! Expected error(s), got success!")
408+
}
409+
}
410+
if !got.Equal(test.want) {
411+
t.Fatalf("wrong result! Got %s, wanted %s", got.String(), test.want.String())
412+
}
413+
})
414+
}
415+
}

internal/command/jsonformat/plan.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ import (
77
"bytes"
88
"encoding/json"
99
"fmt"
10+
"slices"
1011
"sort"
1112
"strings"
1213

13-
"slices"
14-
1514
"github.com/hashicorp/terraform/internal/command/format"
1615
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
1716
"github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers"
@@ -33,6 +32,7 @@ type Plan struct {
3332
ResourceDrift []jsonplan.ResourceChange `json:"resource_drift,omitempty"`
3433
RelevantAttributes []jsonplan.ResourceAttr `json:"relevant_attributes,omitempty"`
3534
DeferredChanges []jsonplan.DeferredResourceChange `json:"deferred_changes,omitempty"`
35+
ActionInvocations []jsonplan.ActionInvocation `json:"action_invocations,omitempty"`
3636

3737
ProviderFormatVersion string `json:"provider_format_version"`
3838
ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas,omitempty"`

internal/command/jsonformat/state_test.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,17 @@ import (
99

1010
"github.com/google/go-cmp/cmp"
1111
"github.com/mitchellh/colorstring"
12-
13-
"github.com/hashicorp/terraform/internal/command/jsonprovider"
14-
"github.com/hashicorp/terraform/internal/command/jsonstate"
15-
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
16-
"github.com/hashicorp/terraform/internal/states/statefile"
17-
"github.com/hashicorp/terraform/internal/terminal"
18-
1912
"github.com/zclconf/go-cty/cty"
2013

2114
"github.com/hashicorp/terraform/internal/addrs"
15+
"github.com/hashicorp/terraform/internal/command/jsonprovider"
16+
"github.com/hashicorp/terraform/internal/command/jsonstate"
2217
"github.com/hashicorp/terraform/internal/configs/configschema"
2318
"github.com/hashicorp/terraform/internal/providers"
19+
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
2420
"github.com/hashicorp/terraform/internal/states"
21+
"github.com/hashicorp/terraform/internal/states/statefile"
22+
"github.com/hashicorp/terraform/internal/terminal"
2523
"github.com/hashicorp/terraform/internal/terraform"
2624
)
2725

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package jsonplan
5+
6+
import (
7+
"github.com/hashicorp/terraform/internal/plans"
8+
)
9+
10+
type ActionInvocation struct {
11+
// Address is the absolute action address
12+
Address string `json:"address,omitempty"`
13+
14+
// ProviderName allows the property "type" to be interpreted unambiguously
15+
// in the unusual situation where a provider offers a type whose
16+
// name does not start with its own name, such as the "googlebeta" provider
17+
// offering "google_compute_instance".
18+
ProviderName string `json:"provider_name,omitempty"`
19+
}
20+
21+
func MarshalActionInvocations(actions []*plans.ActionInvocationInstanceSrc) ([]ActionInvocation, error) {
22+
ret := make([]ActionInvocation, 0, len(actions))
23+
24+
for _, action := range actions {
25+
ai := ActionInvocation{
26+
Address: action.Addr.String(),
27+
ProviderName: action.ProviderAddr.String(),
28+
}
29+
30+
ret = append(ret, ai)
31+
}
32+
33+
return ret, nil
34+
}

0 commit comments

Comments
 (0)