Skip to content

Commit 2f08b28

Browse files
committed
add invoke impl with just completed event
1 parent e43f576 commit 2f08b28

File tree

12 files changed

+674
-11
lines changed

12 files changed

+674
-11
lines changed

action/action.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ type Action interface {
1212
// Metadata should return the full name of the action, such as examplecloud_do_thing.
1313
Metadata(context.Context, MetadataRequest, *MetadataResponse)
1414

15-
// TODO:Actions: Eventual landing place for all required methods to implement for an action
15+
// Invoke is called to run the logic of the action and update linked resources if applicable.
16+
// Config, linked resource planned state, and linked resource prior state values should
17+
// be read from the InvokeRequest and new linked resource state values set on the InvokeResponse.
18+
Invoke(context.Context, InvokeRequest, *InvokeResponse)
1619
}
1720

1821
// ActionWithConfigure is an interface type that extends Action to

action/invoke.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package action
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework/diag"
8+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
9+
)
10+
11+
// InvokeRequest represents a request for the provider to invoke the action and update
12+
// the requested action's linked resources.
13+
type InvokeRequest struct {
14+
// Config is the configuration the user supplied for the action.
15+
Config tfsdk.Config
16+
17+
// TODO:Actions: Add linked resources once lifecycle/linked actions are implemented
18+
}
19+
20+
// InvokeResponse represents a response to an InvokeRequest. An
21+
// instance of this response struct is supplied as
22+
// an argument to the action's Invoke function, in which the provider
23+
// should set values on the InvokeResponse as appropriate.
24+
type InvokeResponse struct {
25+
// Diagnostics report errors or warnings related to invoking the action or updating
26+
// the state of the requested action's linked resources. Returning an empty slice
27+
// indicates a successful invocation with no warnings or errors
28+
// generated.
29+
Diagnostics diag.Diagnostics
30+
31+
// TODO:Actions: Add linked resources once lifecycle/linked actions are implemented
32+
}

internal/fromproto5/invokeaction.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func InvokeActionRequest(ctx context.Context, proto5 *tfprotov5.InvokeActionRequ
3737
}
3838

3939
fw := &fwserver.InvokeActionRequest{
40+
Action: reqAction,
4041
ActionSchema: actionSchema,
4142
}
4243

internal/fromproto6/invokeaction.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func InvokeActionRequest(ctx context.Context, proto6 *tfprotov6.InvokeActionRequ
3737
}
3838

3939
fw := &fwserver.InvokeActionRequest{
40+
Action: reqAction,
4041
ActionSchema: actionSchema,
4142
}
4243

internal/fwserver/server_invokeaction.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ package fwserver
66
import (
77
"context"
88

9+
"github.com/hashicorp/terraform-plugin-framework/action"
910
"github.com/hashicorp/terraform-plugin-framework/diag"
1011
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
12+
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
1113
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
14+
"github.com/hashicorp/terraform-plugin-go/tftypes"
1215
)
1316

1417
// InvokeActionRequest is the framework server request for the InvokeAction RPC.
1518
type InvokeActionRequest struct {
19+
Action action.Action
1620
ActionSchema fwschema.Schema
1721
Config *tfsdk.Config
1822
}
@@ -24,9 +28,44 @@ type InvokeActionResponse struct {
2428

2529
// InvokeAction implements the framework server InvokeAction RPC.
2630
func (s *Server) InvokeAction(ctx context.Context, req *InvokeActionRequest, resp *InvokeActionResponse) {
27-
// TODO:Actions: Implementation coming soon...
28-
resp.Diagnostics.AddError(
29-
"InvokeAction Not Implemented",
30-
"InvokeAction has not yet been implemented in terraform-plugin-framework.",
31-
)
31+
if req == nil {
32+
return
33+
}
34+
35+
if actionWithConfigure, ok := req.Action.(action.ActionWithConfigure); ok {
36+
logging.FrameworkTrace(ctx, "Action implements ActionWithConfigure")
37+
38+
configureReq := action.ConfigureRequest{
39+
ProviderData: s.ActionConfigureData,
40+
}
41+
configureResp := action.ConfigureResponse{}
42+
43+
logging.FrameworkTrace(ctx, "Calling provider defined Action Configure")
44+
actionWithConfigure.Configure(ctx, configureReq, &configureResp)
45+
logging.FrameworkTrace(ctx, "Called provider defined Action Configure")
46+
47+
resp.Diagnostics.Append(configureResp.Diagnostics...)
48+
49+
if resp.Diagnostics.HasError() {
50+
return
51+
}
52+
}
53+
54+
if req.Config == nil {
55+
req.Config = &tfsdk.Config{
56+
Raw: tftypes.NewValue(req.ActionSchema.Type().TerraformType(ctx), nil),
57+
Schema: req.ActionSchema,
58+
}
59+
}
60+
61+
invokeReq := action.InvokeRequest{
62+
Config: *req.Config,
63+
}
64+
invokeResp := action.InvokeResponse{}
65+
66+
logging.FrameworkTrace(ctx, "Calling provider defined Action Invoke")
67+
req.Action.Invoke(ctx, invokeReq, &invokeResp)
68+
logging.FrameworkTrace(ctx, "Called provider defined Action Invoke")
69+
70+
resp.Diagnostics = invokeResp.Diagnostics
3271
}

internal/fwserver/server_invokeaction_test.go

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,183 @@
33

44
package fwserver_test
55

6-
// TODO:Actions: Add unit tests once InvokeAction is implemented
6+
import (
7+
"context"
8+
"fmt"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
"github.com/hashicorp/terraform-plugin-framework/action"
13+
"github.com/hashicorp/terraform-plugin-framework/action/schema"
14+
"github.com/hashicorp/terraform-plugin-framework/diag"
15+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
16+
"github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider"
17+
"github.com/hashicorp/terraform-plugin-framework/provider"
18+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
19+
"github.com/hashicorp/terraform-plugin-framework/types"
20+
"github.com/hashicorp/terraform-plugin-go/tftypes"
21+
)
22+
23+
func TestServerInvokeAction(t *testing.T) {
24+
t.Parallel()
25+
26+
testType := tftypes.Object{
27+
AttributeTypes: map[string]tftypes.Type{
28+
"test_required": tftypes.String,
29+
},
30+
}
31+
32+
testConfigValue := tftypes.NewValue(testType, map[string]tftypes.Value{
33+
"test_required": tftypes.NewValue(tftypes.String, "test-config-value"),
34+
})
35+
36+
testUnlinkedSchema := schema.UnlinkedSchema{
37+
Attributes: map[string]schema.Attribute{
38+
"test_required": schema.StringAttribute{
39+
Required: true,
40+
},
41+
},
42+
}
43+
44+
testUnlinkedConfig := &tfsdk.Config{
45+
Raw: testConfigValue,
46+
Schema: testUnlinkedSchema,
47+
}
48+
49+
testCases := map[string]struct {
50+
server *fwserver.Server
51+
request *fwserver.InvokeActionRequest
52+
expectedResponse *fwserver.InvokeActionResponse
53+
configureProviderReq *provider.ConfigureRequest
54+
}{
55+
"nil": {
56+
server: &fwserver.Server{
57+
Provider: &testprovider.Provider{},
58+
},
59+
expectedResponse: &fwserver.InvokeActionResponse{},
60+
},
61+
"unlinked-nil-config": {
62+
server: &fwserver.Server{
63+
Provider: &testprovider.Provider{},
64+
},
65+
request: &fwserver.InvokeActionRequest{
66+
ActionSchema: testUnlinkedSchema,
67+
Action: &testprovider.Action{
68+
InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
69+
if !req.Config.Raw.IsNull() {
70+
resp.Diagnostics.AddError("Unexpected Config in action Invoke", "Expected Config to be null")
71+
}
72+
},
73+
},
74+
},
75+
expectedResponse: &fwserver.InvokeActionResponse{},
76+
},
77+
"request-config": {
78+
server: &fwserver.Server{
79+
Provider: &testprovider.Provider{},
80+
},
81+
request: &fwserver.InvokeActionRequest{
82+
Config: testUnlinkedConfig,
83+
ActionSchema: testUnlinkedSchema,
84+
Action: &testprovider.Action{
85+
InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
86+
var config struct {
87+
TestRequired types.String `tfsdk:"test_required"`
88+
}
89+
90+
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
91+
92+
if config.TestRequired.ValueString() != "test-config-value" {
93+
resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString())
94+
}
95+
},
96+
},
97+
},
98+
expectedResponse: &fwserver.InvokeActionResponse{},
99+
},
100+
"action-configure-data": {
101+
server: &fwserver.Server{
102+
ActionConfigureData: "test-provider-configure-value",
103+
Provider: &testprovider.Provider{},
104+
},
105+
request: &fwserver.InvokeActionRequest{
106+
Config: testUnlinkedConfig,
107+
ActionSchema: testUnlinkedSchema,
108+
Action: &testprovider.ActionWithConfigure{
109+
ConfigureMethod: func(ctx context.Context, req action.ConfigureRequest, resp *action.ConfigureResponse) {
110+
providerData, ok := req.ProviderData.(string)
111+
112+
if !ok {
113+
resp.Diagnostics.AddError(
114+
"Unexpected ConfigureRequest.ProviderData",
115+
fmt.Sprintf("Expected string, got: %T", req.ProviderData),
116+
)
117+
return
118+
}
119+
120+
if providerData != "test-provider-configure-value" {
121+
resp.Diagnostics.AddError(
122+
"Unexpected ConfigureRequest.ProviderData",
123+
fmt.Sprintf("Expected test-provider-configure-value, got: %q", providerData),
124+
)
125+
}
126+
},
127+
Action: &testprovider.Action{
128+
InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
129+
// In practice, the Configure method would save the
130+
// provider data to the Action implementation and
131+
// use it here. The fact that Configure is able to
132+
// read the data proves this can work.
133+
},
134+
},
135+
},
136+
},
137+
expectedResponse: &fwserver.InvokeActionResponse{},
138+
},
139+
"response-diagnostics": {
140+
server: &fwserver.Server{
141+
Provider: &testprovider.Provider{},
142+
},
143+
request: &fwserver.InvokeActionRequest{
144+
Config: testUnlinkedConfig,
145+
ActionSchema: testUnlinkedSchema,
146+
Action: &testprovider.Action{
147+
InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
148+
resp.Diagnostics.AddWarning("warning summary", "warning detail")
149+
resp.Diagnostics.AddError("error summary", "error detail")
150+
},
151+
},
152+
},
153+
expectedResponse: &fwserver.InvokeActionResponse{
154+
Diagnostics: diag.Diagnostics{
155+
diag.NewWarningDiagnostic(
156+
"warning summary",
157+
"warning detail",
158+
),
159+
diag.NewErrorDiagnostic(
160+
"error summary",
161+
"error detail",
162+
),
163+
},
164+
},
165+
},
166+
}
167+
168+
for name, testCase := range testCases {
169+
t.Run(name, func(t *testing.T) {
170+
t.Parallel()
171+
172+
if testCase.configureProviderReq != nil {
173+
configureProviderResp := &provider.ConfigureResponse{}
174+
testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp)
175+
}
176+
177+
response := &fwserver.InvokeActionResponse{}
178+
testCase.server.InvokeAction(context.Background(), testCase.request, response)
179+
180+
if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" {
181+
t.Errorf("unexpected difference: %s", diff)
182+
}
183+
})
184+
}
185+
}

internal/proto5server/server_invokeaction.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ func (s *Server) InvokeAction(ctx context.Context, proto5Req *tfprotov5.InvokeAc
6060
return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics)
6161
}
6262

63+
// TODO:Actions: Create messaging call back for progress updates
64+
6365
s.FrameworkServer.InvokeAction(ctx, fwReq, fwResp)
6466

6567
// TODO:Actions: This is a stub implementation, so we aren't currently exposing any streaming mechanism to the developer.

0 commit comments

Comments
 (0)