Skip to content

Commit 879185f

Browse files
SBGoodsaustinvalle
andauthored
tfprotov5+tfprotov6: Initial ephemeral resource type implementation (#441)
* Upgrade Go to `v1.22` * Add support for ephemeral resources in protocol version 6 * Add support for ephemeral resources in protocol version 5 * Add ephemeral resources field to `GetMetadata_Response()` * Remove `State` field from `RenewEphemeralResource` RPC response and rename `PriorState` request fields to `State`. * switch interfaces to be optional * removed `config` from renew request * update protocol to match core + added logging * Update protocol bindings * Add changelog entries * Fix comment wording based off of PR feedback --------- Co-authored-by: Austin Valle <[email protected]>
1 parent 8e75df6 commit 879185f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+7028
-2166
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: FEATURES
2+
body: 'tfprotov5+tfprotov6: Upgraded protocols and added types to support the new ephemeral resource type'
3+
time: 2024-10-28T16:19:24.079728-04:00
4+
custom:
5+
Issue: "441"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: NOTES
2+
body: 'tfprotov5+tfprotov6: An upcoming release will require the `EphemeralResourceServer` implementation as part of `ProviderServer`. '
3+
time: 2024-10-28T16:28:25.504018-04:00
4+
custom:
5+
Issue: "441"

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ Run `golangci-lint run ./...` or `make lint` after any changes.
179179

180180
Ensure the following tooling is installed:
181181

182-
- [`protoc`](https://github.com/protocolbuffers/protobuf): Protocol Buffers compiler. This isn't Go specific tooling, so follow this [installation guide](https://github.com/protocolbuffers/protobuf#protocol-compiler-installation)
182+
- [`protoc`](https://github.com/protocolbuffers/protobuf): Protocol Buffers compiler. This isn't Go specific tooling, so follow this [installation guide](https://github.com/protocolbuffers/protobuf#protobuf-compiler-installation)
183+
- The Terraform Plugin Protocol uses well-known types (`Timestamp`), so be sure to copy the `include` directory to a folder included in your `PATH` (for example, on MacOS, `/usr/local/include`).
183184
- [`protoc-gen-go`](https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go): Go plugin for Protocol Buffers compiler. Install by running `make tools`
184185
- [`protoc-gen-go-grpc`](https://pkg.go.dev/google.golang.org/grpc/cmd/protoc-gen-go-grpc): Go gRPC plugin for Protocol Buffers compiler. Install by running `make tools`
185186

internal/logging/context.go

+9
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ func ResourceContext(ctx context.Context, resource string) context.Context {
8282
return ctx
8383
}
8484

85+
// EphemeralResourceContext injects the ephemeral resource type into logger contexts.
86+
func EphemeralResourceContext(ctx context.Context, ephemeralResource string) context.Context {
87+
ctx = tfsdklog.SetField(ctx, KeyEphemeralResourceType, ephemeralResource)
88+
ctx = tfsdklog.SubsystemSetField(ctx, SubsystemProto, KeyEphemeralResourceType, ephemeralResource)
89+
ctx = tflog.SetField(ctx, KeyEphemeralResourceType, ephemeralResource)
90+
91+
return ctx
92+
}
93+
8594
// RpcContext injects the RPC name into logger contexts.
8695
func RpcContext(ctx context.Context, rpc string) context.Context {
8796
ctx = tfsdklog.SetField(ctx, KeyRPC, rpc)

internal/logging/keys.go

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ const (
5757
// The type of data source being operated on, such as "archive_file"
5858
KeyDataSourceType = "tf_data_source_type"
5959

60+
// The type of ephemeral resource being operated on, such as "random_password"
61+
KeyEphemeralResourceType = "tf_ephemeral_resource_type"
62+
6063
// Path to protocol data file, such as "/tmp/example.json"
6164
KeyProtocolDataFile = "tf_proto_data_file"
6265

tfprotov5/client_capabilities.go

+9
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,12 @@ type ImportResourceStateClientCapabilities struct {
4747
// handle deferred responses from the provider.
4848
DeferralAllowed bool
4949
}
50+
51+
// OpenEphemeralResourceClientCapabilities allows Terraform to publish information
52+
// regarding optionally supported protocol features for the OpenEphemeralResource RPC,
53+
// such as forward-compatible Terraform behavior changes.
54+
type OpenEphemeralResourceClientCapabilities struct {
55+
// DeferralAllowed signals that the request from Terraform is able to
56+
// handle deferred responses from the provider.
57+
DeferralAllowed bool
58+
}

tfprotov5/ephemeral_resource.go

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tfprotov5
5+
6+
import (
7+
"context"
8+
"time"
9+
)
10+
11+
// EphemeralResourceMetadata describes metadata for an ephemeral resource in the GetMetadata
12+
// RPC.
13+
type EphemeralResourceMetadata struct {
14+
// TypeName is the name of the ephemeral resource.
15+
TypeName string
16+
}
17+
18+
// EphemeralResourceServer is an interface containing the methods an ephemeral resource
19+
// implementation needs to fill.
20+
type EphemeralResourceServer interface {
21+
// ValidateEphemeralResourceConfig is called when Terraform is checking that an
22+
// ephemeral resource configuration is valid. It is guaranteed to have types
23+
// conforming to your schema, but it is not guaranteed that all values
24+
// will be known. This is your opportunity to do custom or advanced
25+
// validation prior to an ephemeral resource being opened.
26+
ValidateEphemeralResourceConfig(context.Context, *ValidateEphemeralResourceConfigRequest) (*ValidateEphemeralResourceConfigResponse, error)
27+
28+
// OpenEphemeralResource is called when Terraform wants to open the ephemeral resource,
29+
// usually during planning. If the config for the ephemeral resource contains unknown
30+
// values, Terraform will defer the OpenEphemeralResource call until apply.
31+
OpenEphemeralResource(context.Context, *OpenEphemeralResourceRequest) (*OpenEphemeralResourceResponse, error)
32+
33+
// RenewEphemeralResource is called when Terraform detects that the previously specified
34+
// RenewAt timestamp has passed. The RenewAt timestamp is supplied either from the
35+
// OpenEphemeralResource call or a previous RenewEphemeralResource call.
36+
RenewEphemeralResource(context.Context, *RenewEphemeralResourceRequest) (*RenewEphemeralResourceResponse, error)
37+
38+
// CloseEphemeralResource is called when Terraform is closing the ephemeral resource.
39+
CloseEphemeralResource(context.Context, *CloseEphemeralResourceRequest) (*CloseEphemeralResourceResponse, error)
40+
}
41+
42+
// ValidateEphemeralResourceConfigRequest is the request Terraform sends when it
43+
// wants to validate an ephemeral resource's configuration.
44+
type ValidateEphemeralResourceConfigRequest struct {
45+
// TypeName is the type of resource Terraform is validating.
46+
TypeName string
47+
48+
// Config is the configuration the user supplied for that ephemeral resource. See
49+
// the documentation on `DynamicValue` for more information about
50+
// safely accessing the configuration.
51+
//
52+
// The configuration is represented as a tftypes.Object, with each
53+
// attribute and nested block getting its own key and value.
54+
//
55+
// This configuration may contain unknown values if a user uses
56+
// interpolation or other functionality that would prevent Terraform
57+
// from knowing the value at request time. Any attributes not directly
58+
// set in the configuration will be null.
59+
Config *DynamicValue
60+
}
61+
62+
// ValidateEphemeralResourceConfigResponse is the response from the provider about
63+
// the validity of an ephemeral resource's configuration.
64+
type ValidateEphemeralResourceConfigResponse struct {
65+
// Diagnostics report errors or warnings related to the given
66+
// configuration. Returning an empty slice indicates a successful
67+
// validation with no warnings or errors generated.
68+
Diagnostics []*Diagnostic
69+
}
70+
71+
// OpenEphemeralResourceRequest is the request Terraform sends when it
72+
// wants to open an ephemeral resource.
73+
type OpenEphemeralResourceRequest struct {
74+
// TypeName is the type of resource Terraform is opening.
75+
TypeName string
76+
77+
// Config is the configuration the user supplied for that ephemeral resource. See
78+
// the documentation on `DynamicValue` for more information about
79+
// safely accessing the configuration.
80+
//
81+
// The configuration is represented as a tftypes.Object, with each
82+
// attribute and nested block getting its own key and value.
83+
//
84+
// This configuration will always be fully known. If Config contains unknown values,
85+
// Terraform will defer the OpenEphemeralResource RPC until apply.
86+
Config *DynamicValue
87+
88+
// ClientCapabilities defines optionally supported protocol features for the
89+
// OpenEphemeralResource RPC, such as forward-compatible Terraform behavior changes.
90+
ClientCapabilities *OpenEphemeralResourceClientCapabilities
91+
}
92+
93+
// OpenEphemeralResourceResponse is the response from the provider about the current
94+
// state of the opened ephemeral resource.
95+
type OpenEphemeralResourceResponse struct {
96+
// Result is the provider's understanding of what the ephemeral resource's
97+
// data is after it has been opened, represented as a `DynamicValue`.
98+
// See the documentation for `DynamicValue` for information about
99+
// safely creating the `DynamicValue`.
100+
//
101+
// Any attribute, whether computed or not, that has a known value in
102+
// the Config in the OpenEphemeralResourceRequest must be preserved
103+
// exactly as it was in Result.
104+
//
105+
// Any attribute in the Config in the OpenEphemeralResourceRequest
106+
// that is unknown must take on a known value at this time. No unknown
107+
// values are allowed in the Result.
108+
//
109+
// The result should be represented as a tftypes.Object, with each
110+
// attribute and nested block getting its own key and value.
111+
Result *DynamicValue
112+
113+
// Diagnostics report errors or warnings related to opening the
114+
// requested ephemeral resource. Returning an empty slice
115+
// indicates a successful creation with no warnings or errors
116+
// generated.
117+
Diagnostics []*Diagnostic
118+
119+
// Private should be set to any private data that the provider would like to be
120+
// sent to the next Renew or Close call.
121+
Private []byte
122+
123+
// RenewAt indicates to Terraform that the ephemeral resource
124+
// needs to be renewed at the specified time. Terraform will
125+
// call the RenewEphemeralResource RPC when the specified time has passed.
126+
RenewAt time.Time
127+
128+
// Deferred is used to indicate to Terraform that the OpenEphemeralResource operation
129+
// needs to be deferred for a reason.
130+
Deferred *Deferred
131+
}
132+
133+
// RenewEphemeralResourceRequest is the request Terraform sends when it
134+
// wants to renew an ephemeral resource.
135+
type RenewEphemeralResourceRequest struct {
136+
// TypeName is the type of resource Terraform is renewing.
137+
TypeName string
138+
139+
// Private is any provider-defined private data stored with the
140+
// ephemeral resource from the most recent Open or Renew call.
141+
//
142+
// To ensure private data is preserved, copy any necessary data to
143+
// the RenewEphemeralResourceResponse type Private field.
144+
Private []byte
145+
}
146+
147+
// RenewEphemeralResourceResponse is the response from the provider after an ephemeral resource
148+
// has been renewed.
149+
type RenewEphemeralResourceResponse struct {
150+
// Diagnostics report errors or warnings related to renewing the
151+
// requested ephemeral resource. Returning an empty slice
152+
// indicates a successful creation with no warnings or errors
153+
// generated.
154+
Diagnostics []*Diagnostic
155+
156+
// Private should be set to any private data that the provider would like to be
157+
// sent to the next Renew or Close call.
158+
Private []byte
159+
160+
// RenewAt indicates to Terraform that the ephemeral resource
161+
// needs to be renewed at the specified time. Terraform will
162+
// call the RenewEphemeralResource RPC when the specified time has passed.
163+
RenewAt time.Time
164+
}
165+
166+
// CloseEphemeralResourceRequest is the request Terraform sends when it
167+
// wants to close an ephemeral resource.
168+
type CloseEphemeralResourceRequest struct {
169+
// TypeName is the type of resource Terraform is closing.
170+
TypeName string
171+
172+
// Private is any provider-defined private data stored with the
173+
// ephemeral resource from the most recent Open or Renew call.
174+
Private []byte
175+
}
176+
177+
// CloseEphemeralResourceResponse is the response from the provider about
178+
// the closed ephemeral resource.
179+
type CloseEphemeralResourceResponse struct {
180+
// Diagnostics report errors or warnings related to closing the
181+
// requested ephemeral resource. Returning an empty slice
182+
// indicates a successful creation with no warnings or errors
183+
// generated.
184+
Diagnostics []*Diagnostic
185+
}

tfprotov5/internal/fromproto/client_capabilities.go

+12
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,15 @@ func ImportResourceStateClientCapabilities(in *tfplugin5.ClientCapabilities) *tf
6767

6868
return resp
6969
}
70+
71+
func OpenEphemeralResourceClientCapabilities(in *tfplugin5.ClientCapabilities) *tfprotov5.OpenEphemeralResourceClientCapabilities {
72+
if in == nil {
73+
return nil
74+
}
75+
76+
resp := &tfprotov5.OpenEphemeralResourceClientCapabilities{
77+
DeferralAllowed: in.DeferralAllowed,
78+
}
79+
80+
return resp
81+
}

tfprotov5/internal/fromproto/client_capabilities_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,43 @@ func TestImportResourceStateClientCapabilities(t *testing.T) {
211211
})
212212
}
213213
}
214+
215+
func TestOpenEphemeralResourceClientCapabilities(t *testing.T) {
216+
t.Parallel()
217+
218+
testCases := map[string]struct {
219+
in *tfplugin5.ClientCapabilities
220+
expected *tfprotov5.OpenEphemeralResourceClientCapabilities
221+
}{
222+
"nil": {
223+
in: nil,
224+
expected: nil,
225+
},
226+
"zero": {
227+
in: &tfplugin5.ClientCapabilities{},
228+
expected: &tfprotov5.OpenEphemeralResourceClientCapabilities{},
229+
},
230+
"DeferralAllowed": {
231+
in: &tfplugin5.ClientCapabilities{
232+
DeferralAllowed: true,
233+
},
234+
expected: &tfprotov5.OpenEphemeralResourceClientCapabilities{
235+
DeferralAllowed: true,
236+
},
237+
},
238+
}
239+
240+
for name, testCase := range testCases {
241+
name, testCase := name, testCase
242+
243+
t.Run(name, func(t *testing.T) {
244+
t.Parallel()
245+
246+
got := fromproto.OpenEphemeralResourceClientCapabilities(testCase.in)
247+
248+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
249+
t.Errorf("unexpected difference: %s", diff)
250+
}
251+
})
252+
}
253+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fromproto
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
8+
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
9+
)
10+
11+
func ValidateEphemeralResourceConfigRequest(in *tfplugin5.ValidateEphemeralResourceConfig_Request) *tfprotov5.ValidateEphemeralResourceConfigRequest {
12+
if in == nil {
13+
return nil
14+
}
15+
16+
return &tfprotov5.ValidateEphemeralResourceConfigRequest{
17+
TypeName: in.TypeName,
18+
Config: DynamicValue(in.Config),
19+
}
20+
}
21+
22+
func OpenEphemeralResourceRequest(in *tfplugin5.OpenEphemeralResource_Request) *tfprotov5.OpenEphemeralResourceRequest {
23+
if in == nil {
24+
return nil
25+
}
26+
27+
return &tfprotov5.OpenEphemeralResourceRequest{
28+
TypeName: in.TypeName,
29+
Config: DynamicValue(in.Config),
30+
ClientCapabilities: OpenEphemeralResourceClientCapabilities(in.ClientCapabilities),
31+
}
32+
}
33+
34+
func RenewEphemeralResourceRequest(in *tfplugin5.RenewEphemeralResource_Request) *tfprotov5.RenewEphemeralResourceRequest {
35+
if in == nil {
36+
return nil
37+
}
38+
39+
return &tfprotov5.RenewEphemeralResourceRequest{
40+
TypeName: in.TypeName,
41+
Private: in.Private,
42+
}
43+
}
44+
45+
func CloseEphemeralResourceRequest(in *tfplugin5.CloseEphemeralResource_Request) *tfprotov5.CloseEphemeralResourceRequest {
46+
if in == nil {
47+
return nil
48+
}
49+
50+
return &tfprotov5.CloseEphemeralResourceRequest{
51+
TypeName: in.TypeName,
52+
Private: in.Private,
53+
}
54+
}

0 commit comments

Comments
 (0)