diff --git a/action/action.go b/action/action.go new file mode 100644 index 000000000..f172651f9 --- /dev/null +++ b/action/action.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package action + +import "context" + +type Action interface { + // Schema should return the schema for this action. + Schema(context.Context, SchemaRequest, *SchemaResponse) + + // Metadata should return the full name of the action, such as examplecloud_do_thing. + Metadata(context.Context, MetadataRequest, *MetadataResponse) + + // TODO:Actions: Eventual landing place for all required methods to implement for an action +} diff --git a/action/doc.go b/action/doc.go new file mode 100644 index 000000000..351e8b352 --- /dev/null +++ b/action/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// TODO:Actions: Eventual package docs for actions +package action diff --git a/action/metadata.go b/action/metadata.go new file mode 100644 index 000000000..46b4c3366 --- /dev/null +++ b/action/metadata.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package action + +// MetadataRequest represents a request for the Action to return metadata, +// such as its type name. An instance of this request struct is supplied as +// an argument to the Action type Metadata method. +type MetadataRequest struct { + // ProviderTypeName is the string returned from + // [provider.MetadataResponse.TypeName], if the Provider type implements + // the Metadata method. This string should prefix the Action type name + // with an underscore in the response. + ProviderTypeName string +} + +// MetadataResponse represents a response to a MetadataRequest. An +// instance of this response struct is supplied as an argument to the +// Action type Metadata method. +type MetadataResponse struct { + // TypeName should be the full action type, including the provider + // type prefix and an underscore. For example, examplecloud_thing. + TypeName string +} diff --git a/action/schema.go b/action/schema.go new file mode 100644 index 000000000..9eb52bf7b --- /dev/null +++ b/action/schema.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package action + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// SchemaRequest represents a request for the Action to return its schema. +// An instance of this request struct is supplied as an argument to the +// Action type Schema method. +type SchemaRequest struct{} + +// SchemaResponse represents a response to a SchemaRequest. An instance of this +// response struct is supplied as an argument to the Action type Schema +// method. +type SchemaResponse struct { + // TODO:Actions: This will eventually be replaced by an interface defined in + // an "actions/schema" package. Schema implementations that will fulfill this + // interface will be unlinked, linked, or lifecycle. (also defined in the "actions/schema" package) + Schema fwschema.Schema + + // Diagnostics report errors or warnings related to retrieving the action schema. + // An empty slice indicates success, with no warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/go.mod b/go.mod index 4479b2d26..3996d6843 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.7 require ( github.com/google/go-cmp v0.7.0 - github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1 + github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250709165734-a8477a15f806 github.com/hashicorp/terraform-plugin-log v0.9.0 ) diff --git a/go.sum b/go.sum index 5af35aa4f..58d2914b5 100644 --- a/go.sum +++ b/go.sum @@ -21,10 +21,8 @@ github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0U github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250616135123-a19df43120ea h1:U9EAAeQtszGlR7mDS7rY77B/a4/XiMDB8HfAtqLAuAQ= -github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250616135123-a19df43120ea/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= -github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1 h1:ZId6oWG8VTKhz207quE/Xh8a3HuoLtM/QkcSSypekIQ= -github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= +github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250709165734-a8477a15f806 h1:i3kA1sT/Fk8Ex+VVKdjf9sFOPwS7w3Q73pfbnxKwdjg= +github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250709165734-a8477a15f806/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= diff --git a/internal/fromproto5/invokeaction.go b/internal/fromproto5/invokeaction.go new file mode 100644 index 000000000..6290147d3 --- /dev/null +++ b/internal/fromproto5/invokeaction.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" +) + +// InvokeActionRequest returns the *fwserver.InvokeActionRequest equivalent of a *tfprotov5.InvokeActionRequest. +func InvokeActionRequest(ctx context.Context, proto5 *tfprotov5.InvokeActionRequest, reqAction action.Action, actionSchema fwschema.Schema) (*fwserver.InvokeActionRequest, diag.Diagnostics) { + if proto5 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if actionSchema == nil { + diags.AddError( + "Missing Action Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.InvokeActionRequest{ + ActionSchema: actionSchema, + } + + config, configDiags := Config(ctx, proto5.Config, actionSchema) + + diags.Append(configDiags...) + + fw.Config = config + + // TODO:Actions: Here we need to retrieve linked resource data + + return fw, diags +} diff --git a/internal/fromproto5/invokeaction_test.go b/internal/fromproto5/invokeaction_test.go new file mode 100644 index 000000000..c5b20a804 --- /dev/null +++ b/internal/fromproto5/invokeaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +// TODO:Actions: Add unit tests once this mapping logic is complete diff --git a/internal/fromproto5/planaction.go b/internal/fromproto5/planaction.go new file mode 100644 index 000000000..bb71e5815 --- /dev/null +++ b/internal/fromproto5/planaction.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" +) + +// PlanActionRequest returns the *fwserver.PlanActionRequest equivalent of a *tfprotov5.PlanActionRequest. +func PlanActionRequest(ctx context.Context, proto5 *tfprotov5.PlanActionRequest, reqAction action.Action, actionSchema fwschema.Schema) (*fwserver.PlanActionRequest, diag.Diagnostics) { + if proto5 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if actionSchema == nil { + diags.AddError( + "Missing Action Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.PlanActionRequest{ + ActionSchema: actionSchema, + } + + config, configDiags := Config(ctx, proto5.Config, actionSchema) + + diags.Append(configDiags...) + + fw.Config = config + + // TODO:Actions: Here we need to retrieve client capabilities and linked resource data + + return fw, diags +} diff --git a/internal/fromproto5/planaction_test.go b/internal/fromproto5/planaction_test.go new file mode 100644 index 000000000..c5b20a804 --- /dev/null +++ b/internal/fromproto5/planaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +// TODO:Actions: Add unit tests once this mapping logic is complete diff --git a/internal/fromproto6/invokeaction.go b/internal/fromproto6/invokeaction.go new file mode 100644 index 000000000..270a2bacb --- /dev/null +++ b/internal/fromproto6/invokeaction.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" +) + +// InvokeActionRequest returns the *fwserver.InvokeActionRequest equivalent of a *tfprotov6.InvokeActionRequest. +func InvokeActionRequest(ctx context.Context, proto6 *tfprotov6.InvokeActionRequest, reqAction action.Action, actionSchema fwschema.Schema) (*fwserver.InvokeActionRequest, diag.Diagnostics) { + if proto6 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if actionSchema == nil { + diags.AddError( + "Missing Action Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.InvokeActionRequest{ + ActionSchema: actionSchema, + } + + config, configDiags := Config(ctx, proto6.Config, actionSchema) + + diags.Append(configDiags...) + + fw.Config = config + + // TODO:Actions: Here we need to retrieve linked resource data + + return fw, diags +} diff --git a/internal/fromproto6/invokeaction_test.go b/internal/fromproto6/invokeaction_test.go new file mode 100644 index 000000000..5c9fa3702 --- /dev/null +++ b/internal/fromproto6/invokeaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +// TODO:Actions: Add unit tests once this mapping logic is complete diff --git a/internal/fromproto6/planaction.go b/internal/fromproto6/planaction.go new file mode 100644 index 000000000..7715037c9 --- /dev/null +++ b/internal/fromproto6/planaction.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" +) + +// PlanActionRequest returns the *fwserver.PlanActionRequest equivalent of a *tfprotov6.PlanActionRequest. +func PlanActionRequest(ctx context.Context, proto6 *tfprotov6.PlanActionRequest, reqAction action.Action, actionSchema fwschema.Schema) (*fwserver.PlanActionRequest, diag.Diagnostics) { + if proto6 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if actionSchema == nil { + diags.AddError( + "Missing Action Schema", + "An unexpected error was encountered when handling the request. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.PlanActionRequest{ + ActionSchema: actionSchema, + } + + config, configDiags := Config(ctx, proto6.Config, actionSchema) + + diags.Append(configDiags...) + + fw.Config = config + + // TODO:Actions: Here we need to retrieve client capabilities and linked resource data + + return fw, diags +} diff --git a/internal/fromproto6/planaction_test.go b/internal/fromproto6/planaction_test.go new file mode 100644 index 000000000..5c9fa3702 --- /dev/null +++ b/internal/fromproto6/planaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +// TODO:Actions: Add unit tests once this mapping logic is complete diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index f178b21aa..3fef10f28 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -8,6 +8,7 @@ import ( "fmt" "sync" + "github.com/hashicorp/terraform-plugin-framework/action" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" @@ -40,6 +41,29 @@ type Server struct { // to [ephemeral.ConfigureRequest.ProviderData]. EphemeralResourceConfigureData any + // actionSchemas is the cached Action Schemas for RPCs that need to + // convert configuration data from the protocol. If not found, it will be + // fetched from the Action.Schema() method. + actionSchemas map[string]fwschema.Schema + + // actionSchemasMutex is a mutex to protect concurrent actionSchemas + // access from race conditions. + actionSchemasMutex sync.RWMutex + + // actionFuncs is the cached Action functions for RPCs that need to + // access actions. If not found, it will be fetched from the + // Provider.Actions() method. + actionFuncs map[string]func() action.Action + + // actionFuncsDiags is the cached Diagnostics obtained while populating + // actionFuncs. This is to ensure any warnings or errors are also + // returned appropriately when fetching actionFuncs. + actionFuncsDiags diag.Diagnostics + + // actionFuncsMutex is a mutex to protect concurrent actionFuncs + // access from race conditions. + actionFuncsMutex sync.Mutex + // dataSourceSchemas is the cached DataSource Schemas for RPCs that need to // convert configuration data from the protocol. If not found, it will be // fetched from the DataSourceType.GetSchema() method. diff --git a/internal/fwserver/server_actions.go b/internal/fwserver/server_actions.go new file mode 100644 index 000000000..73f2b68a8 --- /dev/null +++ b/internal/fwserver/server_actions.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +// Action returns the Action for a given action type. +func (s *Server) Action(ctx context.Context, actionType string) (action.Action, diag.Diagnostics) { + actionFuncs, diags := s.ActionFuncs(ctx) + + actionFunc, ok := actionFuncs[actionType] + + if !ok { + diags.AddError( + "Action Type Not Found", + fmt.Sprintf("No action type named %q was found in the provider.", actionType), + ) + + return nil, diags + } + + return actionFunc(), diags +} + +// ActionFuncs returns a map of Action functions. The results are cached +// on first use. +func (s *Server) ActionFuncs(ctx context.Context) (map[string]func() action.Action, diag.Diagnostics) { + logging.FrameworkTrace(ctx, "Checking ActionFuncs lock") + s.actionFuncsMutex.Lock() + defer s.actionFuncsMutex.Unlock() + + if s.actionFuncs != nil { + return s.actionFuncs, s.actionFuncsDiags + } + + providerTypeName := s.ProviderTypeName(ctx) + s.actionFuncs = make(map[string]func() action.Action) + + provider, ok := s.Provider.(provider.ProviderWithActions) + if !ok { + // Only action specific RPCs should return diagnostics about the + // provider not implementing actions or missing actions. + return s.actionFuncs, s.actionFuncsDiags + } + + logging.FrameworkTrace(ctx, "Calling provider defined Provider Actions") + actionFuncsSlice := provider.Actions(ctx) + logging.FrameworkTrace(ctx, "Called provider defined Provider Actions") + + for _, actionFunc := range actionFuncsSlice { + actionImpl := actionFunc() + + actionTypeReq := action.MetadataRequest{ + ProviderTypeName: providerTypeName, + } + actionTypeResp := action.MetadataResponse{} + + actionImpl.Metadata(ctx, actionTypeReq, &actionTypeResp) + + if actionTypeResp.TypeName == "" { + s.actionFuncsDiags.AddError( + "Action Type Missing", + fmt.Sprintf("The %T Action returned an empty string from the Metadata method. ", actionImpl)+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + logging.FrameworkTrace(ctx, "Found action", map[string]interface{}{logging.KeyActionType: actionTypeResp.TypeName}) + + if _, ok := s.actionFuncs[actionTypeResp.TypeName]; ok { + s.actionFuncsDiags.AddError( + "Duplicate Action Defined", + fmt.Sprintf("The %s action type was returned for multiple actions. ", actionTypeResp.TypeName)+ + "Action types must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + s.actionFuncs[actionTypeResp.TypeName] = actionFunc + } + + return s.actionFuncs, s.actionFuncsDiags +} + +// ActionSchema returns the Action Schema for the given type name and +// caches the result for later Action operations. +func (s *Server) ActionSchema(ctx context.Context, actionType string) (fwschema.Schema, diag.Diagnostics) { + s.actionSchemasMutex.RLock() + actionSchema, ok := s.actionSchemas[actionType] + s.actionSchemasMutex.RUnlock() + + if ok { + return actionSchema, nil + } + + var diags diag.Diagnostics + + actionImpl, actionDiags := s.Action(ctx, actionType) + + diags.Append(actionDiags...) + + if diags.HasError() { + return nil, diags + } + + schemaReq := action.SchemaRequest{} + schemaResp := action.SchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Action Schema method", map[string]interface{}{logging.KeyActionType: actionType}) + actionImpl.Schema(ctx, schemaReq, &schemaResp) + logging.FrameworkTrace(ctx, "Called provider defined Action Schema method", map[string]interface{}{logging.KeyActionType: actionType}) + + diags.Append(schemaResp.Diagnostics...) + + if diags.HasError() { + return schemaResp.Schema, diags + } + + s.actionSchemasMutex.Lock() + + if s.actionSchemas == nil { + s.actionSchemas = make(map[string]fwschema.Schema) + } + + s.actionSchemas[actionType] = schemaResp.Schema + + s.actionSchemasMutex.Unlock() + + return schemaResp.Schema, diags +} diff --git a/internal/fwserver/server_invokeaction.go b/internal/fwserver/server_invokeaction.go new file mode 100644 index 000000000..200a5b811 --- /dev/null +++ b/internal/fwserver/server_invokeaction.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// InvokeActionRequest is the framework server request for the InvokeAction RPC. +type InvokeActionRequest struct { + ActionSchema fwschema.Schema + Config *tfsdk.Config +} + +// InvokeActionEventsStream is the framework server stream for the InvokeAction RPC. +type InvokeActionResponse struct { + Diagnostics diag.Diagnostics +} + +// InvokeAction implements the framework server InvokeAction RPC. +func (s *Server) InvokeAction(ctx context.Context, req *InvokeActionRequest, resp *InvokeActionResponse) { + // TODO:Actions: Implementation coming soon... + resp.Diagnostics.AddError( + "InvokeAction Not Implemented", + "InvokeAction has not yet been implemented in terraform-plugin-framework.", + ) +} diff --git a/internal/fwserver/server_invokeaction_test.go b/internal/fwserver/server_invokeaction_test.go new file mode 100644 index 000000000..5879cddb3 --- /dev/null +++ b/internal/fwserver/server_invokeaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +// TODO:Actions: Add unit tests once InvokeAction is implemented diff --git a/internal/fwserver/server_planaction.go b/internal/fwserver/server_planaction.go new file mode 100644 index 000000000..1a5bc192f --- /dev/null +++ b/internal/fwserver/server_planaction.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// PlanActionRequest is the framework server request for the PlanAction RPC. +type PlanActionRequest struct { + ActionSchema fwschema.Schema + Config *tfsdk.Config +} + +// PlanActionResponse is the framework server response for the PlanAction RPC. +type PlanActionResponse struct { + Diagnostics diag.Diagnostics +} + +// PlanAction implements the framework server PlanAction RPC. +func (s *Server) PlanAction(ctx context.Context, req *PlanActionRequest, resp *PlanActionResponse) { + // TODO:Actions: Implementation coming soon... + resp.Diagnostics.AddError( + "PlanAction Not Implemented", + "PlanAction has not yet been implemented in terraform-plugin-framework.", + ) +} diff --git a/internal/fwserver/server_planaction_test.go b/internal/fwserver/server_planaction_test.go new file mode 100644 index 000000000..1ecb5c4f3 --- /dev/null +++ b/internal/fwserver/server_planaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +// TODO:Actions: Add unit tests once PlanAction is implemented diff --git a/internal/logging/keys.go b/internal/logging/keys.go index c9d2dc9a9..a18586320 100644 --- a/internal/logging/keys.go +++ b/internal/logging/keys.go @@ -15,6 +15,9 @@ const ( // as parent.0.child in this project. KeyAttributePath = "tf_attribute_path" + // The type of action being operated on, such as "examplecloud_do_thing" + KeyActionType = "tf_action_type" + // The type of data source being operated on, such as "archive_file" KeyDataSourceType = "tf_data_source_type" diff --git a/internal/proto5server/server_invokeaction.go b/internal/proto5server/server_invokeaction.go new file mode 100644 index 000000000..e7e2d6fd6 --- /dev/null +++ b/internal/proto5server/server_invokeaction.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// invokeActionErrorDiagnostics returns a value suitable for +// [InvokeActionServerStream.Events]. It yields a single result that contains +// the given error diagnostics. +func invokeActionErrorDiagnostics(ctx context.Context, diags diag.Diagnostics) (*tfprotov5.InvokeActionServerStream, error) { + return &tfprotov5.InvokeActionServerStream{ + Events: func(push func(tfprotov5.InvokeActionEvent) bool) { + push(tfprotov5.InvokeActionEvent{ + Type: tfprotov5.CompletedInvokeActionEventType{ + Diagnostics: toproto5.Diagnostics(ctx, diags), + }, + }) + }, + }, nil +} + +// InvokeAction satisfies the tfprotov5.ProviderServer interface. +func (s *Server) InvokeAction(ctx context.Context, proto5Req *tfprotov5.InvokeActionRequest) (*tfprotov5.InvokeActionServerStream, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.InvokeActionResponse{} + + action, diags := s.FrameworkServer.Action(ctx, proto5Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics) + } + + actionSchema, diags := s.FrameworkServer.ActionSchema(ctx, proto5Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics) + } + + fwReq, diags := fromproto5.InvokeActionRequest(ctx, proto5Req, action, actionSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics) + } + + s.FrameworkServer.InvokeAction(ctx, fwReq, fwResp) + + // TODO:Actions: This is a stub implementation, so we aren't currently exposing any streaming mechanism to the developer. + // That will eventually need to change to send progress events back to Terraform. + // + // This logic will likely need to be moved over to the "toproto" package as well. + protoStream := &tfprotov5.InvokeActionServerStream{ + Events: func(push func(tfprotov5.InvokeActionEvent) bool) { + push(tfprotov5.InvokeActionEvent{ + Type: tfprotov5.CompletedInvokeActionEventType{ + Diagnostics: toproto5.Diagnostics(ctx, fwResp.Diagnostics), + }, + }) + }, + } + + return protoStream, nil +} diff --git a/internal/proto5server/server_invokeaction_test.go b/internal/proto5server/server_invokeaction_test.go new file mode 100644 index 000000000..4cc50c03a --- /dev/null +++ b/internal/proto5server/server_invokeaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server_test + +// TODO:Actions: Add unit tests once InvokeAction is implemented diff --git a/internal/proto5server/server_planaction.go b/internal/proto5server/server_planaction.go new file mode 100644 index 000000000..39a31ff4b --- /dev/null +++ b/internal/proto5server/server_planaction.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// PlanAction satisfies the tfprotov5.ProviderServer interface. +func (s *Server) PlanAction(ctx context.Context, proto5Req *tfprotov5.PlanActionRequest) (*tfprotov5.PlanActionResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.PlanActionResponse{} + + action, diags := s.FrameworkServer.Action(ctx, proto5Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.PlanActionResponse(ctx, fwResp), nil + } + + actionSchema, diags := s.FrameworkServer.ActionSchema(ctx, proto5Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.PlanActionResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.PlanActionRequest(ctx, proto5Req, action, actionSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.PlanActionResponse(ctx, fwResp), nil + } + + s.FrameworkServer.PlanAction(ctx, fwReq, fwResp) + + return toproto5.PlanActionResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_planaction_test.go b/internal/proto5server/server_planaction_test.go new file mode 100644 index 000000000..ccf86051e --- /dev/null +++ b/internal/proto5server/server_planaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server_test + +// TODO:Actions: Add unit tests once PlanAction is implemented diff --git a/internal/proto6server/server_invokeaction.go b/internal/proto6server/server_invokeaction.go new file mode 100644 index 000000000..3a6d78cee --- /dev/null +++ b/internal/proto6server/server_invokeaction.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// invokeActionErrorDiagnostics returns a value suitable for +// [InvokeActionServerStream.Events]. It yields a single result that contains +// the given error diagnostics. +func invokeActionErrorDiagnostics(ctx context.Context, diags diag.Diagnostics) (*tfprotov6.InvokeActionServerStream, error) { + return &tfprotov6.InvokeActionServerStream{ + Events: func(push func(tfprotov6.InvokeActionEvent) bool) { + push(tfprotov6.InvokeActionEvent{ + Type: tfprotov6.CompletedInvokeActionEventType{ + Diagnostics: toproto6.Diagnostics(ctx, diags), + }, + }) + }, + }, nil +} + +// InvokeAction satisfies the tfprotov6.ProviderServer interface. +func (s *Server) InvokeAction(ctx context.Context, proto6Req *tfprotov6.InvokeActionRequest) (*tfprotov6.InvokeActionServerStream, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.InvokeActionResponse{} + + action, diags := s.FrameworkServer.Action(ctx, proto6Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics) + } + + actionSchema, diags := s.FrameworkServer.ActionSchema(ctx, proto6Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics) + } + + fwReq, diags := fromproto6.InvokeActionRequest(ctx, proto6Req, action, actionSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics) + } + + s.FrameworkServer.InvokeAction(ctx, fwReq, fwResp) + + // TODO:Actions: This is a stub implementation, so we aren't currently exposing any streaming mechanism to the developer. + // That will eventually need to change to send progress events back to Terraform. + // + // This logic will likely need to be moved over to the "toproto" package as well. + protoStream := &tfprotov6.InvokeActionServerStream{ + Events: func(push func(tfprotov6.InvokeActionEvent) bool) { + push(tfprotov6.InvokeActionEvent{ + Type: tfprotov6.CompletedInvokeActionEventType{ + Diagnostics: toproto6.Diagnostics(ctx, fwResp.Diagnostics), + }, + }) + }, + } + + return protoStream, nil +} diff --git a/internal/proto6server/server_invokeaction_test.go b/internal/proto6server/server_invokeaction_test.go new file mode 100644 index 000000000..7bc3d2a68 --- /dev/null +++ b/internal/proto6server/server_invokeaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server_test + +// TODO:Actions: Add unit tests once InvokeAction is implemented diff --git a/internal/proto6server/server_planaction.go b/internal/proto6server/server_planaction.go new file mode 100644 index 000000000..a92a28d63 --- /dev/null +++ b/internal/proto6server/server_planaction.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// PlanAction satisfies the tfprotov6.ProviderServer interface. +func (s *Server) PlanAction(ctx context.Context, proto6Req *tfprotov6.PlanActionRequest) (*tfprotov6.PlanActionResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.PlanActionResponse{} + + action, diags := s.FrameworkServer.Action(ctx, proto6Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.PlanActionResponse(ctx, fwResp), nil + } + + actionSchema, diags := s.FrameworkServer.ActionSchema(ctx, proto6Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.PlanActionResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.PlanActionRequest(ctx, proto6Req, action, actionSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.PlanActionResponse(ctx, fwResp), nil + } + + s.FrameworkServer.PlanAction(ctx, fwReq, fwResp) + + return toproto6.PlanActionResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_planaction_test.go b/internal/proto6server/server_planaction_test.go new file mode 100644 index 000000000..0ff1b5e7c --- /dev/null +++ b/internal/proto6server/server_planaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server_test + +// TODO:Actions: Add unit tests once PlanAction is implemented diff --git a/internal/toproto5/planaction.go b/internal/toproto5/planaction.go new file mode 100644 index 000000000..b55fd4557 --- /dev/null +++ b/internal/toproto5/planaction.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" +) + +// PlanActionResponse returns the *tfprotov5.PlanActionResponse equivalent of a *fwserver.PlanActionResponse. +func PlanActionResponse(ctx context.Context, fw *fwserver.PlanActionResponse) *tfprotov5.PlanActionResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.PlanActionResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + // TODO:Actions: Here we need to set deferred and linked resource data + + return proto5 +} diff --git a/internal/toproto5/planaction_test.go b/internal/toproto5/planaction_test.go new file mode 100644 index 000000000..41e665462 --- /dev/null +++ b/internal/toproto5/planaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +// TODO:Actions: Add unit tests once this mapping logic is complete diff --git a/internal/toproto6/planaction.go b/internal/toproto6/planaction.go new file mode 100644 index 000000000..9d4f86ac8 --- /dev/null +++ b/internal/toproto6/planaction.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" +) + +// PlanActionResponse returns the *tfprotov6.PlanActionResponse equivalent of a *fwserver.PlanActionResponse. +func PlanActionResponse(ctx context.Context, fw *fwserver.PlanActionResponse) *tfprotov6.PlanActionResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.PlanActionResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + // TODO:Actions: Here we need to set deferred and linked resource data + + return proto6 +} diff --git a/internal/toproto6/planaction_test.go b/internal/toproto6/planaction_test.go new file mode 100644 index 000000000..29cade55e --- /dev/null +++ b/internal/toproto6/planaction_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +// TODO:Actions: Add unit tests once this mapping logic is complete diff --git a/provider/provider.go b/provider/provider.go index a7dc583f6..f32e2773d 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -6,6 +6,7 @@ package provider import ( "context" + "github.com/hashicorp/terraform-plugin-framework/action" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" @@ -134,6 +135,21 @@ type ProviderWithListResources interface { ListResources(context.Context) []func() list.ListResource } +// ProviderWithActions is an interface type that extends Provider to +// include actions for usage in practitioner configurations. +// +// TODO:Actions: State which Terraform version will support actions +type ProviderWithActions interface { + Provider + + // Actions returns a slice of functions to instantiate each Action + // implementation. + // + // The action type is determined by the Action implementing + // the Metadata method. All action types must have unique names. + Actions(context.Context) []func() action.Action +} + // ProviderWithValidateConfig is an interface type that extends Provider to include imperative validation. // // Declaring validation using this methodology simplifies one-off