Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tfprotov5+tfprotov6: Initial ephemeral resource type implementation #441

Merged
merged 14 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
190 changes: 190 additions & 0 deletions tfprotov5/ephemeral_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// 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 a
// ephemeral resource's 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 ephemeral resource creation.
SBGoods marked this conversation as resolved.
Show resolved Hide resolved
SBGoods marked this conversation as resolved.
Show resolved Hide resolved
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 at
// the end of the Terraform run.
SBGoods marked this conversation as resolved.
Show resolved Hide resolved
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 state that the provider would like sent
// with requests for this ephemeral resource. This state will be associated with
// the ephemeral resource, but will not be considered when calculating diffs.
SBGoods marked this conversation as resolved.
Show resolved Hide resolved
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 state stored with the
// ephemeral resource. It is used for keeping state with the resource that is not
// meant to be included when calculating diffs.
//
// To ensure private state data is preserved, copy any necessary data to
// the RenewEphemeralResourceResponse type Private field.
SBGoods marked this conversation as resolved.
Show resolved Hide resolved
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 state that the provider would like sent
// with requests for this ephemeral resource. This state will be associated with
// the ephemeral resource, but will not be considered when calculating diffs.
SBGoods marked this conversation as resolved.
Show resolved Hide resolved
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 state stored with the
// ephemeral resource. It is used for keeping state with the resource that is not
// meant to be included when calculating diffs.
SBGoods marked this conversation as resolved.
Show resolved Hide resolved
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
Loading