Skip to content

Commit

Permalink
tfprotov5+tfprotov6: Initial ephemeral resource type implementation (#…
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
SBGoods and austinvalle authored Oct 29, 2024
1 parent 8e75df6 commit 879185f
Show file tree
Hide file tree
Showing 43 changed files with 7,028 additions and 2,166 deletions.
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20241028-161924.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'tfprotov5+tfprotov6: Upgraded protocols and added types to support the new ephemeral resource type'
time: 2024-10-28T16:19:24.079728-04:00
custom:
Issue: "441"
5 changes: 5 additions & 0 deletions .changes/unreleased/NOTES-20241028-162825.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: NOTES
body: 'tfprotov5+tfprotov6: An upcoming release will require the `EphemeralResourceServer` implementation as part of `ProviderServer`. '
time: 2024-10-28T16:28:25.504018-04:00
custom:
Issue: "441"
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ Run `golangci-lint run ./...` or `make lint` after any changes.

Ensure the following tooling is installed:

- [`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)
- [`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)
- 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`).
- [`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`
- [`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`

Expand Down
9 changes: 9 additions & 0 deletions internal/logging/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ func ResourceContext(ctx context.Context, resource string) context.Context {
return ctx
}

// EphemeralResourceContext injects the ephemeral resource type into logger contexts.
func EphemeralResourceContext(ctx context.Context, ephemeralResource string) context.Context {
ctx = tfsdklog.SetField(ctx, KeyEphemeralResourceType, ephemeralResource)
ctx = tfsdklog.SubsystemSetField(ctx, SubsystemProto, KeyEphemeralResourceType, ephemeralResource)
ctx = tflog.SetField(ctx, KeyEphemeralResourceType, ephemeralResource)

return ctx
}

// RpcContext injects the RPC name into logger contexts.
func RpcContext(ctx context.Context, rpc string) context.Context {
ctx = tfsdklog.SetField(ctx, KeyRPC, rpc)
Expand Down
3 changes: 3 additions & 0 deletions internal/logging/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ const (
// The type of data source being operated on, such as "archive_file"
KeyDataSourceType = "tf_data_source_type"

// The type of ephemeral resource being operated on, such as "random_password"
KeyEphemeralResourceType = "tf_ephemeral_resource_type"

// Path to protocol data file, such as "/tmp/example.json"
KeyProtocolDataFile = "tf_proto_data_file"

Expand Down
9 changes: 9 additions & 0 deletions tfprotov5/client_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@ type ImportResourceStateClientCapabilities struct {
// handle deferred responses from the provider.
DeferralAllowed bool
}

// OpenEphemeralResourceClientCapabilities allows Terraform to publish information
// regarding optionally supported protocol features for the OpenEphemeralResource RPC,
// such as forward-compatible Terraform behavior changes.
type OpenEphemeralResourceClientCapabilities struct {
// DeferralAllowed signals that the request from Terraform is able to
// handle deferred responses from the provider.
DeferralAllowed bool
}
185 changes: 185 additions & 0 deletions tfprotov5/ephemeral_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tfprotov5

import (
"context"
"time"
)

// EphemeralResourceMetadata describes metadata for an ephemeral resource in the GetMetadata
// RPC.
type EphemeralResourceMetadata struct {
// TypeName is the name of the ephemeral resource.
TypeName string
}

// EphemeralResourceServer is an interface containing the methods an ephemeral resource
// implementation needs to fill.
type EphemeralResourceServer interface {
// ValidateEphemeralResourceConfig is called when Terraform is checking that an
// ephemeral resource configuration is valid. It is guaranteed to have types
// conforming to your schema, but it is not guaranteed that all values
// will be known. This is your opportunity to do custom or advanced
// validation prior to an ephemeral resource being opened.
ValidateEphemeralResourceConfig(context.Context, *ValidateEphemeralResourceConfigRequest) (*ValidateEphemeralResourceConfigResponse, error)

// OpenEphemeralResource is called when Terraform wants to open the ephemeral resource,
// usually during planning. If the config for the ephemeral resource contains unknown
// values, Terraform will defer the OpenEphemeralResource call until apply.
OpenEphemeralResource(context.Context, *OpenEphemeralResourceRequest) (*OpenEphemeralResourceResponse, error)

// RenewEphemeralResource is called when Terraform detects that the previously specified
// RenewAt timestamp has passed. The RenewAt timestamp is supplied either from the
// OpenEphemeralResource call or a previous RenewEphemeralResource call.
RenewEphemeralResource(context.Context, *RenewEphemeralResourceRequest) (*RenewEphemeralResourceResponse, error)

// CloseEphemeralResource is called when Terraform is closing the ephemeral resource.
CloseEphemeralResource(context.Context, *CloseEphemeralResourceRequest) (*CloseEphemeralResourceResponse, error)
}

// ValidateEphemeralResourceConfigRequest is the request Terraform sends when it
// wants to validate an ephemeral resource's configuration.
type ValidateEphemeralResourceConfigRequest struct {
// TypeName is the type of resource Terraform is validating.
TypeName string

// Config is the configuration the user supplied for that ephemeral resource. See
// the documentation on `DynamicValue` for more information about
// safely accessing the configuration.
//
// The configuration is represented as a tftypes.Object, with each
// attribute and nested block getting its own key and value.
//
// This configuration may contain unknown values if a user uses
// interpolation or other functionality that would prevent Terraform
// from knowing the value at request time. Any attributes not directly
// set in the configuration will be null.
Config *DynamicValue
}

// ValidateEphemeralResourceConfigResponse is the response from the provider about
// the validity of an ephemeral resource's configuration.
type ValidateEphemeralResourceConfigResponse struct {
// Diagnostics report errors or warnings related to the given
// configuration. Returning an empty slice indicates a successful
// validation with no warnings or errors generated.
Diagnostics []*Diagnostic
}

// OpenEphemeralResourceRequest is the request Terraform sends when it
// wants to open an ephemeral resource.
type OpenEphemeralResourceRequest struct {
// TypeName is the type of resource Terraform is opening.
TypeName string

// Config is the configuration the user supplied for that ephemeral resource. See
// the documentation on `DynamicValue` for more information about
// safely accessing the configuration.
//
// The configuration is represented as a tftypes.Object, with each
// attribute and nested block getting its own key and value.
//
// This configuration will always be fully known. If Config contains unknown values,
// Terraform will defer the OpenEphemeralResource RPC until apply.
Config *DynamicValue

// ClientCapabilities defines optionally supported protocol features for the
// OpenEphemeralResource RPC, such as forward-compatible Terraform behavior changes.
ClientCapabilities *OpenEphemeralResourceClientCapabilities
}

// OpenEphemeralResourceResponse is the response from the provider about the current
// state of the opened ephemeral resource.
type OpenEphemeralResourceResponse struct {
// Result is the provider's understanding of what the ephemeral resource's
// data is after it has been opened, represented as a `DynamicValue`.
// See the documentation for `DynamicValue` for information about
// safely creating the `DynamicValue`.
//
// Any attribute, whether computed or not, that has a known value in
// the Config in the OpenEphemeralResourceRequest must be preserved
// exactly as it was in Result.
//
// Any attribute in the Config in the OpenEphemeralResourceRequest
// that is unknown must take on a known value at this time. No unknown
// values are allowed in the Result.
//
// The result should be represented as a tftypes.Object, with each
// attribute and nested block getting its own key and value.
Result *DynamicValue

// Diagnostics report errors or warnings related to opening the
// requested ephemeral resource. Returning an empty slice
// indicates a successful creation with no warnings or errors
// generated.
Diagnostics []*Diagnostic

// Private should be set to any private data that the provider would like to be
// sent to the next Renew or Close call.
Private []byte

// RenewAt indicates to Terraform that the ephemeral resource
// needs to be renewed at the specified time. Terraform will
// call the RenewEphemeralResource RPC when the specified time has passed.
RenewAt time.Time

// Deferred is used to indicate to Terraform that the OpenEphemeralResource operation
// needs to be deferred for a reason.
Deferred *Deferred
}

// RenewEphemeralResourceRequest is the request Terraform sends when it
// wants to renew an ephemeral resource.
type RenewEphemeralResourceRequest struct {
// TypeName is the type of resource Terraform is renewing.
TypeName string

// Private is any provider-defined private data stored with the
// ephemeral resource from the most recent Open or Renew call.
//
// To ensure private data is preserved, copy any necessary data to
// the RenewEphemeralResourceResponse type Private field.
Private []byte
}

// RenewEphemeralResourceResponse is the response from the provider after an ephemeral resource
// has been renewed.
type RenewEphemeralResourceResponse struct {
// Diagnostics report errors or warnings related to renewing the
// requested ephemeral resource. Returning an empty slice
// indicates a successful creation with no warnings or errors
// generated.
Diagnostics []*Diagnostic

// Private should be set to any private data that the provider would like to be
// sent to the next Renew or Close call.
Private []byte

// RenewAt indicates to Terraform that the ephemeral resource
// needs to be renewed at the specified time. Terraform will
// call the RenewEphemeralResource RPC when the specified time has passed.
RenewAt time.Time
}

// CloseEphemeralResourceRequest is the request Terraform sends when it
// wants to close an ephemeral resource.
type CloseEphemeralResourceRequest struct {
// TypeName is the type of resource Terraform is closing.
TypeName string

// Private is any provider-defined private data stored with the
// ephemeral resource from the most recent Open or Renew call.
Private []byte
}

// CloseEphemeralResourceResponse is the response from the provider about
// the closed ephemeral resource.
type CloseEphemeralResourceResponse struct {
// Diagnostics report errors or warnings related to closing the
// requested ephemeral resource. Returning an empty slice
// indicates a successful creation with no warnings or errors
// generated.
Diagnostics []*Diagnostic
}
12 changes: 12 additions & 0 deletions tfprotov5/internal/fromproto/client_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,15 @@ func ImportResourceStateClientCapabilities(in *tfplugin5.ClientCapabilities) *tf

return resp
}

func OpenEphemeralResourceClientCapabilities(in *tfplugin5.ClientCapabilities) *tfprotov5.OpenEphemeralResourceClientCapabilities {
if in == nil {
return nil
}

resp := &tfprotov5.OpenEphemeralResourceClientCapabilities{
DeferralAllowed: in.DeferralAllowed,
}

return resp
}
40 changes: 40 additions & 0 deletions tfprotov5/internal/fromproto/client_capabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,43 @@ func TestImportResourceStateClientCapabilities(t *testing.T) {
})
}
}

func TestOpenEphemeralResourceClientCapabilities(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
in *tfplugin5.ClientCapabilities
expected *tfprotov5.OpenEphemeralResourceClientCapabilities
}{
"nil": {
in: nil,
expected: nil,
},
"zero": {
in: &tfplugin5.ClientCapabilities{},
expected: &tfprotov5.OpenEphemeralResourceClientCapabilities{},
},
"DeferralAllowed": {
in: &tfplugin5.ClientCapabilities{
DeferralAllowed: true,
},
expected: &tfprotov5.OpenEphemeralResourceClientCapabilities{
DeferralAllowed: true,
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got := fromproto.OpenEphemeralResourceClientCapabilities(testCase.in)

if diff := cmp.Diff(got, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}
54 changes: 54 additions & 0 deletions tfprotov5/internal/fromproto/ephemeral_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package fromproto

import (
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
)

func ValidateEphemeralResourceConfigRequest(in *tfplugin5.ValidateEphemeralResourceConfig_Request) *tfprotov5.ValidateEphemeralResourceConfigRequest {
if in == nil {
return nil
}

return &tfprotov5.ValidateEphemeralResourceConfigRequest{
TypeName: in.TypeName,
Config: DynamicValue(in.Config),
}
}

func OpenEphemeralResourceRequest(in *tfplugin5.OpenEphemeralResource_Request) *tfprotov5.OpenEphemeralResourceRequest {
if in == nil {
return nil
}

return &tfprotov5.OpenEphemeralResourceRequest{
TypeName: in.TypeName,
Config: DynamicValue(in.Config),
ClientCapabilities: OpenEphemeralResourceClientCapabilities(in.ClientCapabilities),
}
}

func RenewEphemeralResourceRequest(in *tfplugin5.RenewEphemeralResource_Request) *tfprotov5.RenewEphemeralResourceRequest {
if in == nil {
return nil
}

return &tfprotov5.RenewEphemeralResourceRequest{
TypeName: in.TypeName,
Private: in.Private,
}
}

func CloseEphemeralResourceRequest(in *tfplugin5.CloseEphemeralResource_Request) *tfprotov5.CloseEphemeralResourceRequest {
if in == nil {
return nil
}

return &tfprotov5.CloseEphemeralResourceRequest{
TypeName: in.TypeName,
Private: in.Private,
}
}
Loading

0 comments on commit 879185f

Please sign in to comment.