From 9ec6758ee4037f810af7338265a8132430d0f77c Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 9 Jul 2025 13:54:22 -0400 Subject: [PATCH 01/21] go mod --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) 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= From 5eeac4cfb90b83dafc43157f797094e87b326100 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 9 Jul 2025 13:54:34 -0400 Subject: [PATCH 02/21] generate RPC methods --- internal/proto5server/server_invokeaction.go | 16 ++++++++++++++++ internal/proto5server/server_planaction.go | 16 ++++++++++++++++ internal/proto6server/server_invokeaction.go | 16 ++++++++++++++++ internal/proto6server/server_planaction.go | 16 ++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 internal/proto5server/server_invokeaction.go create mode 100644 internal/proto5server/server_planaction.go create mode 100644 internal/proto6server/server_invokeaction.go create mode 100644 internal/proto6server/server_planaction.go diff --git a/internal/proto5server/server_invokeaction.go b/internal/proto5server/server_invokeaction.go new file mode 100644 index 000000000..90534898c --- /dev/null +++ b/internal/proto5server/server_invokeaction.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// InvokeAction satisfies the tfprotov5.ProviderServer interface. +func (s *Server) InvokeAction(ctx context.Context, proto5Req *tfprotov5.InvokeActionRequest) (*tfprotov5.InvokeActionServerStream, error) { + // TODO:Actions: Implement + panic("unimplemented") +} diff --git a/internal/proto5server/server_planaction.go b/internal/proto5server/server_planaction.go new file mode 100644 index 000000000..d8a2adbd3 --- /dev/null +++ b/internal/proto5server/server_planaction.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "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) { + // TODO:Actions: Implement + panic("unimplemented") +} diff --git a/internal/proto6server/server_invokeaction.go b/internal/proto6server/server_invokeaction.go new file mode 100644 index 000000000..80b80b6b8 --- /dev/null +++ b/internal/proto6server/server_invokeaction.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// InvokeAction satisfies the tfprotov6.ProviderServer interface. +func (s *Server) InvokeAction(ctx context.Context, proto6Req *tfprotov6.InvokeActionRequest) (*tfprotov6.InvokeActionServerStream, error) { + // TODO:Actions: Implement + panic("unimplemented") +} diff --git a/internal/proto6server/server_planaction.go b/internal/proto6server/server_planaction.go new file mode 100644 index 000000000..42680d36a --- /dev/null +++ b/internal/proto6server/server_planaction.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "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) { + // TODO:Actions: Implement + panic("unimplemented") +} From bf10880d2087259b3efb4aaf92cd783831fdf33a Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 9 Jul 2025 18:02:09 -0400 Subject: [PATCH 03/21] protov5 and fwserver impl --- action/action.go | 16 ++ action/doc.go | 5 + action/metadata.go | 24 +++ action/schema.go | 28 ++++ internal/fromproto5/invokeaction.go | 52 +++++++ internal/fromproto5/invokeaction_test.go | 6 + internal/fromproto5/planaction.go | 52 +++++++ internal/fromproto5/planaction_test.go | 6 + internal/fwserver/server.go | 24 +++ internal/fwserver/server_actions.go | 142 ++++++++++++++++++ internal/fwserver/server_invokeaction.go | 32 ++++ internal/fwserver/server_invokeaction_test.go | 6 + internal/fwserver/server_planaction.go | 32 ++++ internal/fwserver/server_planaction_test.go | 6 + internal/logging/keys.go | 3 + internal/proto5server/server_invokeaction.go | 68 ++++++++- .../proto5server/server_invokeaction_test.go | 6 + internal/proto5server/server_planaction.go | 38 ++++- .../proto5server/server_planaction_test.go | 6 + internal/toproto5/planaction.go | 27 ++++ internal/toproto5/planaction_test.go | 6 + provider/provider.go | 16 ++ 22 files changed, 597 insertions(+), 4 deletions(-) create mode 100644 action/action.go create mode 100644 action/doc.go create mode 100644 action/metadata.go create mode 100644 action/schema.go create mode 100644 internal/fromproto5/invokeaction.go create mode 100644 internal/fromproto5/invokeaction_test.go create mode 100644 internal/fromproto5/planaction.go create mode 100644 internal/fromproto5/planaction_test.go create mode 100644 internal/fwserver/server_actions.go create mode 100644 internal/fwserver/server_invokeaction.go create mode 100644 internal/fwserver/server_invokeaction_test.go create mode 100644 internal/fwserver/server_planaction.go create mode 100644 internal/fwserver/server_planaction_test.go create mode 100644 internal/proto5server/server_invokeaction_test.go create mode 100644 internal/proto5server/server_planaction_test.go create mode 100644 internal/toproto5/planaction.go create mode 100644 internal/toproto5/planaction_test.go 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/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/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 index 90534898c..e7e2d6fd6 100644 --- a/internal/proto5server/server_invokeaction.go +++ b/internal/proto5server/server_invokeaction.go @@ -6,11 +6,75 @@ 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) { - // TODO:Actions: Implement - panic("unimplemented") + 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 index d8a2adbd3..39a31ff4b 100644 --- a/internal/proto5server/server_planaction.go +++ b/internal/proto5server/server_planaction.go @@ -6,11 +6,45 @@ 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) { - // TODO:Actions: Implement - panic("unimplemented") + 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/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/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 From 3c21a45de28a92992d3596921a01a42ae7983514 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 9 Jul 2025 18:07:03 -0400 Subject: [PATCH 04/21] protov6 copy --- internal/fromproto6/invokeaction.go | 52 ++++++++++++++ internal/fromproto6/invokeaction_test.go | 6 ++ internal/fromproto6/planaction.go | 52 ++++++++++++++ internal/fromproto6/planaction_test.go | 6 ++ internal/proto6server/server_invokeaction.go | 68 ++++++++++++++++++- .../proto6server/server_invokeaction_test.go | 6 ++ internal/proto6server/server_planaction.go | 38 ++++++++++- .../proto6server/server_planaction_test.go | 6 ++ internal/toproto6/planaction.go | 27 ++++++++ internal/toproto6/planaction_test.go | 6 ++ 10 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 internal/fromproto6/invokeaction.go create mode 100644 internal/fromproto6/invokeaction_test.go create mode 100644 internal/fromproto6/planaction.go create mode 100644 internal/fromproto6/planaction_test.go create mode 100644 internal/proto6server/server_invokeaction_test.go create mode 100644 internal/proto6server/server_planaction_test.go create mode 100644 internal/toproto6/planaction.go create mode 100644 internal/toproto6/planaction_test.go 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/proto6server/server_invokeaction.go b/internal/proto6server/server_invokeaction.go index 80b80b6b8..3a6d78cee 100644 --- a/internal/proto6server/server_invokeaction.go +++ b/internal/proto6server/server_invokeaction.go @@ -6,11 +6,75 @@ 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) { - // TODO:Actions: Implement - panic("unimplemented") + 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 index 42680d36a..a92a28d63 100644 --- a/internal/proto6server/server_planaction.go +++ b/internal/proto6server/server_planaction.go @@ -6,11 +6,45 @@ 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) { - // TODO:Actions: Implement - panic("unimplemented") + 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/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 From 61eadc9fe7f8247b17d7e5d9f9c14ea01107c4a6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 10 Jul 2025 10:40:30 -0400 Subject: [PATCH 05/21] add initial schema attributes and unlinked schema --- action/schema/attribute.go | 52 + action/schema/block.go | 43 + action/schema/bool_attribute.go | 170 +++ action/schema/bool_attribute_test.go | 437 ++++++ action/schema/list_attribute.go | 204 +++ action/schema/list_attribute_test.go | 522 +++++++ action/schema/nested_attribute.go | 14 + action/schema/nested_attribute_object.go | 65 + action/schema/nested_attribute_object_test.go | 233 +++ action/schema/nested_block_object.go | 77 + action/schema/nested_block_object_test.go | 317 +++++ action/schema/schema_type.go | 26 + action/schema/single_nested_attribute.go | 226 +++ action/schema/single_nested_attribute_test.go | 575 ++++++++ action/schema/single_nested_block.go | 191 +++ action/schema/single_nested_block_test.go | 433 ++++++ action/schema/string_attribute.go | 169 +++ action/schema/string_attribute_test.go | 437 ++++++ action/schema/unlinked_schema.go | 164 +++ action/schema/unlinked_schema_test.go | 1250 +++++++++++++++++ 20 files changed, 5605 insertions(+) create mode 100644 action/schema/attribute.go create mode 100644 action/schema/block.go create mode 100644 action/schema/bool_attribute.go create mode 100644 action/schema/bool_attribute_test.go create mode 100644 action/schema/list_attribute.go create mode 100644 action/schema/list_attribute_test.go create mode 100644 action/schema/nested_attribute.go create mode 100644 action/schema/nested_attribute_object.go create mode 100644 action/schema/nested_attribute_object_test.go create mode 100644 action/schema/nested_block_object.go create mode 100644 action/schema/nested_block_object_test.go create mode 100644 action/schema/schema_type.go create mode 100644 action/schema/single_nested_attribute.go create mode 100644 action/schema/single_nested_attribute_test.go create mode 100644 action/schema/single_nested_block.go create mode 100644 action/schema/single_nested_block_test.go create mode 100644 action/schema/string_attribute.go create mode 100644 action/schema/string_attribute_test.go create mode 100644 action/schema/unlinked_schema.go create mode 100644 action/schema/unlinked_schema_test.go diff --git a/action/schema/attribute.go b/action/schema/attribute.go new file mode 100644 index 000000000..1364b42bc --- /dev/null +++ b/action/schema/attribute.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// TODO:Actions: Add all of the attribute and nested attribute types listed below +// +// Attribute define a value field inside an action type schema. Implementations in this +// package include: +// - BoolAttribute +// - DynamicAttribute +// - Float32Attribute +// - Float64Attribute +// - Int32Attribute +// - Int64Attribute +// - ListAttribute +// - MapAttribute +// - NumberAttribute +// - ObjectAttribute +// - SetAttribute +// - StringAttribute +// +// Additionally, the NestedAttribute interface extends Attribute with nested +// attributes. Only supported in protocol version 6. Implementations in this +// package include: +// - ListNestedAttribute +// - MapNestedAttribute +// - SetNestedAttribute +// - SingleNestedAttribute +// +// In practitioner configurations, an equals sign (=) is required to set +// the value. [Configuration Reference] +// +// [Configuration Reference]: https://developer.hashicorp.com/terraform/language/syntax/configuration +type Attribute interface { + fwschema.Attribute +} + +// schemaAttributes is an action attribute to fwschema type conversion function. +func schemaAttributes(attributes map[string]Attribute) map[string]fwschema.Attribute { + result := make(map[string]fwschema.Attribute, len(attributes)) + + for name, attribute := range attributes { + result[name] = attribute + } + + return result +} diff --git a/action/schema/block.go b/action/schema/block.go new file mode 100644 index 000000000..ecc291cf0 --- /dev/null +++ b/action/schema/block.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// TODO:Actions: Add all of the block and nested block types listed below +// +// Block defines a structural field inside an action type schema. Implementations in this +// package include: +// - ListNestedBlock +// - SetNestedBlock +// - SingleNestedBlock +// +// In practitioner configurations, an equals sign (=) cannot be used to set the +// value. Blocks are instead repeated as necessary, or require the use of +// [Dynamic Block Expressions]. +// +// Prefer NestedAttribute over Block. Blocks should typically be used for +// configuration compatibility with previously existing schemas from an older +// Terraform Plugin SDK. Efforts should be made to convert from Block to +// NestedAttribute as a breaking change for practitioners. +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +// +// [Configuration Reference]: https://developer.hashicorp.com/terraform/language/syntax/configuration +type Block interface { + fwschema.Block +} + +// schemaBlocks is an action block to fwschema type conversion function. +func schemaBlocks(blocks map[string]Block) map[string]fwschema.Block { + result := make(map[string]fwschema.Block, len(blocks)) + + for name, block := range blocks { + result[name] = block + } + + return result +} diff --git a/action/schema/bool_attribute.go b/action/schema/bool_attribute.go new file mode 100644 index 000000000..5558946e6 --- /dev/null +++ b/action/schema/bool_attribute.go @@ -0,0 +1,170 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = BoolAttribute{} +) + +// BoolAttribute represents a schema attribute that is a boolean. When +// retrieving the value for this attribute, use types.Bool as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a boolean or directly via the true/false keywords. +// +// example_attribute = true +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type BoolAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.BoolType. When retrieving data, the basetypes.BoolValuable + // associated with this custom type must be used in place of types.Bool. + CustomType basetypes.BoolTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a BoolAttribute. +func (a BoolAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a BoolAttribute +// and all fields are equal. +func (a BoolAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(BoolAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a BoolAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a BoolAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a BoolAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a BoolAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.BoolType +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a BoolAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a BoolAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a BoolAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a BoolAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} diff --git a/action/schema/bool_attribute_test.go b/action/schema/bool_attribute_test.go new file mode 100644 index 000000000..725f14c8b --- /dev/null +++ b/action/schema/bool_attribute_test.go @@ -0,0 +1,437 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.BoolAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.BoolType"), + }, + "ElementKeyInt": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.BoolType"), + }, + "ElementKeyString": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.BoolType"), + }, + "ElementKeyValue": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.BoolType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.BoolAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.BoolAttribute{}, + other: testschema.AttributeWithBoolValidators{}, + expected: false, + }, + "equal": { + attribute: schema.BoolAttribute{}, + other: schema.BoolAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected string + }{ + "no-description": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "description": { + attribute: schema.BoolAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.BoolAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected attr.Type + }{ + "base": { + attribute: schema.BoolAttribute{}, + expected: types.BoolType, + }, + "custom-type": { + attribute: schema.BoolAttribute{ + CustomType: testtypes.BoolType{}, + }, + expected: testtypes.BoolType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-computed": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optional": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.BoolAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-required": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "required": { + attribute: schema.BoolAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/list_attribute.go b/action/schema/list_attribute.go new file mode 100644 index 000000000..180664e9e --- /dev/null +++ b/action/schema/list_attribute.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = ListAttribute{} + _ fwschema.AttributeWithValidateImplementation = ListAttribute{} +) + +// ListAttribute represents a schema attribute that is a list with a single +// element type. When retrieving the value for this attribute, use types.List +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use ListNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a list or directly via square brace syntax. +// +// # list of strings +// example_attribute = ["first", "second"] +// +// Terraform configurations reference this attribute using expressions that +// accept a list or an element directly via square brace 0-based index syntax: +// +// # first known element +// .example_attribute[0] +type ListAttribute struct { + // ElementType is the type for all elements of the list. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ListType. When retrieving data, the basetypes.ListValuable + // associated with this custom type must be used in place of types.List. + CustomType basetypes.ListTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a list +// index or an error. +func (a ListAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a ListAttribute +// and all fields are equal. +func (a ListAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ListAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a ListAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a ListAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ListAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.ListType or the CustomType field value if defined. +func (a ListAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ListType{ + ElemType: a.ElementType, + } +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a ListAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a ListAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ListAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a ListAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (a ListAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/action/schema/list_attribute_test.go b/action/schema/list_attribute_test.go new file mode 100644 index 000000000..52d8ec503 --- /dev/null +++ b/action/schema/list_attribute_test.go @@ -0,0 +1,522 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListType"), + }, + "ElementKeyInt": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyString": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListType"), + }, + "ElementKeyValue": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.ListAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: schema.ListAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: schema.ListAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected string + }{ + "no-description": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.ListAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.ListAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: types.ListType{ElemType: types.StringType}, + }, + "custom-type": { + attribute: schema.ListAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.ListAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-required": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.ListAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "elementtype": { + attribute: schema.ListAttribute{ + Required: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.ListAttribute{ + Required: true, + ElementType: types.DynamicType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.ListAttribute{ + Required: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/nested_attribute.go b/action/schema/nested_attribute.go new file mode 100644 index 000000000..31d2ee158 --- /dev/null +++ b/action/schema/nested_attribute.go @@ -0,0 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Nested attributes are only compatible with protocol version 6. +type NestedAttribute interface { + Attribute + fwschema.NestedAttribute +} diff --git a/action/schema/nested_attribute_object.go b/action/schema/nested_attribute_object.go new file mode 100644 index 000000000..b082a154f --- /dev/null +++ b/action/schema/nested_attribute_object.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ fwschema.NestedAttributeObject = NestedAttributeObject{} +) + +// NestedAttributeObject is the object containing the underlying attributes +// for a ListNestedAttribute, MapNestedAttribute, SetNestedAttribute, or +// SingleNestedAttribute (automatically generated). When retrieving the value +// for this attribute, use types.Object as the value type unless the CustomType +// field is set. The Attributes field must be set. Nested attributes are only +// compatible with protocol version 6. +// +// This object enables customizing and simplifying details within its parent +// NestedAttribute, therefore it cannot have Terraform schema fields such as +// Required, Description, etc. +type NestedAttributeObject struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. This field must be set. + Attributes map[string]Attribute + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedAttributeObject) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedAttributeObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given NestedAttributeObject is equivalent. +func (o NestedAttributeObject) Equal(other fwschema.NestedAttributeObject) bool { + if _, ok := other.(NestedAttributeObject); !ok { + return false + } + + return fwschema.NestedAttributeObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o NestedAttributeObject) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(o.Attributes) +} + +// Type returns the framework type of the NestedAttributeObject. +func (o NestedAttributeObject) Type() basetypes.ObjectTypable { + if o.CustomType != nil { + return o.CustomType + } + + return fwschema.NestedAttributeObjectType(o) +} diff --git a/action/schema/nested_attribute_object_test.go b/action/schema/nested_attribute_object_test.go new file mode 100644 index 000000000..8c39be9e5 --- /dev/null +++ b/action/schema/nested_attribute_object_test.go @@ -0,0 +1,233 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestNestedAttributeObjectApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute \"other\" on NestedAttributeObject"), + }, + "ElementKeyInt": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to NestedAttributeObject"), + }, + "ElementKeyString": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to NestedAttributeObject"), + }, + "ElementKeyValue": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to NestedAttributeObject"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.object.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + other fwschema.NestedAttributeObject + expected bool + }{ + "different-attributes": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + expected fwschema.UnderlyingAttributes + }{ + "no-attributes": { + object: schema.NestedAttributeObject{}, + expected: fwschema.UnderlyingAttributes{}, + }, + "attributes": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: fwschema.UnderlyingAttributes{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + expected attr.Type + }{ + "base": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + "custom-type": { + object: schema.NestedAttributeObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/nested_block_object.go b/action/schema/nested_block_object.go new file mode 100644 index 000000000..8193b6891 --- /dev/null +++ b/action/schema/nested_block_object.go @@ -0,0 +1,77 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ fwschema.NestedBlockObject = NestedBlockObject{} +) + +// NestedBlockObject is the object containing the underlying attributes and +// blocks for a ListNestedBlock or SetNestedBlock. When retrieving the value +// for this attribute, use types.Object as the value type unless the CustomType +// field is set. +// +// This object enables customizing and simplifying details within its parent +// Block, therefore it cannot have Terraform schema fields such as Description, +// etc. +type NestedBlockObject struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedBlockObject) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedBlockObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given NestedBlockObject is equivalent. +func (o NestedBlockObject) Equal(other fwschema.NestedBlockObject) bool { + if _, ok := other.(NestedBlockObject); !ok { + return false + } + + return fwschema.NestedBlockObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o NestedBlockObject) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(o.Attributes) +} + +// GetAttributes returns the Blocks field value. +func (o NestedBlockObject) GetBlocks() map[string]fwschema.Block { + return schemaBlocks(o.Blocks) +} + +// Type returns the framework type of the NestedBlockObject. +func (o NestedBlockObject) Type() basetypes.ObjectTypable { + if o.CustomType != nil { + return o.CustomType + } + + return fwschema.NestedBlockObjectType(o) +} diff --git a/action/schema/nested_block_object_test.go b/action/schema/nested_block_object_test.go new file mode 100644 index 000000000..2139384e1 --- /dev/null +++ b/action/schema/nested_block_object_test.go @@ -0,0 +1,317 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestNestedBlockObjectApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + object: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute or block \"other\" on NestedBlockObject"), + }, + "ElementKeyInt": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to NestedBlockObject"), + }, + "ElementKeyString": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to NestedBlockObject"), + }, + "ElementKeyValue": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to NestedBlockObject"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.object.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + other fwschema.NestedBlockObject + expected bool + }{ + "different-attributes": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected fwschema.UnderlyingAttributes + }{ + "no-attributes": { + object: schema.NestedBlockObject{}, + expected: fwschema.UnderlyingAttributes{}, + }, + "attributes": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: fwschema.UnderlyingAttributes{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectGetBlocks(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected map[string]fwschema.Block + }{ + "no-blocks": { + object: schema.NestedBlockObject{}, + expected: map[string]fwschema.Block{}, + }, + "blocks": { + object: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: map[string]fwschema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetBlocks() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected attr.Type + }{ + "base": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + "custom-type": { + object: schema.NestedBlockObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/schema_type.go b/action/schema/schema_type.go new file mode 100644 index 000000000..04122adf9 --- /dev/null +++ b/action/schema/schema_type.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + +// TODO:Actions: Implement lifecycle and linked schemas +// +// SchemaType is the interface that an action schema type must implement. Action +// schema types are statically definined in the protocol, so all implementations +// are defined in this package. +// +// SchemaType implementations define how a practitioner can trigger an action, as well +// as what effect the action can have on the state. There are currently three different +// types of actions: +// - [UnlinkedSchema] actions are actions that cannot cause changes to resource states. +// - [LifecycleSchema] actions are actions that can cause changes to exactly one resource state. +// - [LinkedSchema] actions are actions that can cause changes to one or more resource states. +type SchemaType interface { + fwschema.Schema + + // Action schema types are statically defined in the protocol, so this + // interface is not meant to be implemented outside of this package + isActionSchemaType() +} diff --git a/action/schema/single_nested_attribute.go b/action/schema/single_nested_attribute.go new file mode 100644 index 000000000..48903ab02 --- /dev/null +++ b/action/schema/single_nested_attribute.go @@ -0,0 +1,226 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = SingleNestedAttribute{} +) + +// SingleNestedAttribute represents an attribute that is a single object where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Object +// as the value type unless the CustomType field is set. The Attributes field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use ObjectAttribute if the underlying attributes do not require definition +// beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return an object or directly via curly brace syntax. +// +// # single object +// example_attribute = { +// nested_attribute = #... +// } +// +// Terraform configurations reference this attribute using expressions that +// accept an object or an attribute name directly via period syntax: +// +// # object nested_attribute value +// .example_attribute.nested_attribute +type SingleNestedAttribute struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. This field must be set. + Attributes map[string]Attribute + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is AttributeName, otherwise returns an error. +func (a SingleNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SingleNestedAttribute", step) + } + + attribute, ok := a.Attributes[string(name)] + + if !ok { + return nil, fmt.Errorf("no attribute %q on SingleNestedAttribute", name) + } + + return attribute, nil +} + +// Equal returns true if the given Attribute is a SingleNestedAttribute +// and all fields are equal. +func (a SingleNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(SingleNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetAttributes returns the Attributes field value. +func (a SingleNestedAttribute) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(a.Attributes) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a SingleNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a SingleNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SingleNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns a generated NestedAttributeObject from the +// Attributes, CustomType, and Validators field values. +func (a SingleNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return NestedAttributeObject{ + Attributes: a.Attributes, + CustomType: a.CustomType, + } +} + +// GetNestingMode always returns NestingModeSingle. +func (a SingleNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeSingle +} + +// GetType returns ListType of ObjectType or CustomType. +func (a SingleNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + attrTypes := make(map[string]attr.Type, len(a.Attributes)) + + for name, attribute := range a.Attributes { + attrTypes[name] = attribute.GetType() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a SingleNestedAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a SingleNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SingleNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a SingleNestedAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} diff --git a/action/schema/single_nested_attribute_test.go b/action/schema/single_nested_attribute_test.go new file mode 100644 index 000000000..a2522611d --- /dev/null +++ b/action/schema/single_nested_attribute_test.go @@ -0,0 +1,575 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute \"other\" on SingleNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SingleNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SingleNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to SingleNestedAttribute"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: testschema.AttributeWithObjectValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.SingleNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + attribute: schema.SingleNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.SingleNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + "custom-type": { + attribute: schema.SingleNestedAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.SingleNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "required": { + attribute: schema.SingleNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/single_nested_block.go b/action/schema/single_nested_block.go new file mode 100644 index 000000000..feb6f63d8 --- /dev/null +++ b/action/schema/single_nested_block.go @@ -0,0 +1,191 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Block = SingleNestedBlock{} +) + +// SingleNestedBlock represents a block that is a single object where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.Object +// as the value type unless the CustomType field is set. +// +// Prefer SingleNestedAttribute over SingleNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block only once using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # single block +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept an object or an attribute name directly via period syntax: +// +// # object nested_attribute value +// .example_block.nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type SingleNestedBlock struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is AttributeName, otherwise returns an error. +func (b SingleNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SingleNestedBlock", step) + } + + if attribute, ok := b.Attributes[string(name)]; ok { + return attribute, nil + } + + if block, ok := b.Blocks[string(name)]; ok { + return block, nil + } + + return nil, fmt.Errorf("no attribute or block %q on SingleNestedBlock", name) +} + +// Equal returns true if the given Attribute is b SingleNestedBlock +// and all fields are equal. +func (b SingleNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(SingleNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b SingleNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b SingleNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b SingleNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetNestedObject returns a generated NestedBlockObject from the +// Attributes, CustomType, and Validators field values. +func (b SingleNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return NestedBlockObject{ + Attributes: b.Attributes, + Blocks: b.Blocks, + CustomType: b.CustomType, + } +} + +// GetNestingMode always returns BlockNestingModeSingle. +func (b SingleNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSingle +} + +// Type returns ObjectType or CustomType. +func (b SingleNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + attrTypes := make(map[string]attr.Type, len(b.Attributes)+len(b.Blocks)) + + for name, attribute := range b.Attributes { + attrTypes[name] = attribute.GetType() + } + + for name, block := range b.Blocks { + attrTypes[name] = block.Type() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} diff --git a/action/schema/single_nested_block_test.go b/action/schema/single_nested_block_test.go new file mode 100644 index 000000000..a52addce1 --- /dev/null +++ b/action/schema/single_nested_block_test.go @@ -0,0 +1,433 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSingleNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + block: schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute or block \"other\" on SingleNestedBlock"), + }, + "ElementKeyInt": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SingleNestedBlock"), + }, + "ElementKeyString": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SingleNestedBlock"), + }, + "ElementKeyValue": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to SingleNestedBlock"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.SingleNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: testschema.BlockWithObjectValidators{}, + expected: false, + }, + "different-attributes-definitions": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "different-blocks-definitions": { + block: schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + other: schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-description": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + block: schema.SingleNestedBlock{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.SingleNestedBlock{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected attr.Type + }{ + "base": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + "custom-type": { + block: schema.SingleNestedBlock{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/string_attribute.go b/action/schema/string_attribute.go new file mode 100644 index 000000000..bbf03341a --- /dev/null +++ b/action/schema/string_attribute.go @@ -0,0 +1,169 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = StringAttribute{} +) + +// StringAttribute represents a schema attribute that is a string. When +// retrieving the value for this attribute, use types.String as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a string or directly via double quote syntax. +// +// example_attribute = "value" +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type StringAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.StringType. When retrieving data, the basetypes.StringValuable + // associated with this custom type must be used in place of types.String. + CustomType basetypes.StringTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a StringAttribute. +func (a StringAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a StringAttribute +// and all fields are equal. +func (a StringAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(StringAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a StringAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a StringAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a StringAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a StringAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.StringType +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a StringAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a StringAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a StringAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a StringAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} diff --git a/action/schema/string_attribute_test.go b/action/schema/string_attribute_test.go new file mode 100644 index 000000000..bbfc3cfff --- /dev/null +++ b/action/schema/string_attribute_test.go @@ -0,0 +1,437 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.StringAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.StringType"), + }, + "ElementKeyInt": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.StringType"), + }, + "ElementKeyString": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.StringType"), + }, + "ElementKeyValue": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.StringType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.StringAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.StringAttribute{}, + other: testschema.AttributeWithStringValidators{}, + expected: false, + }, + "equal": { + attribute: schema.StringAttribute{}, + other: schema.StringAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected string + }{ + "no-description": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "description": { + attribute: schema.StringAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.StringAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected attr.Type + }{ + "base": { + attribute: schema.StringAttribute{}, + expected: types.StringType, + }, + "custom-type": { + attribute: schema.StringAttribute{ + CustomType: testtypes.StringType{}, + }, + expected: testtypes.StringType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-computed": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optional": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.StringAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-required": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "required": { + attribute: schema.StringAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/unlinked_schema.go b/action/schema/unlinked_schema.go new file mode 100644 index 000000000..71af1a4f1 --- /dev/null +++ b/action/schema/unlinked_schema.go @@ -0,0 +1,164 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ SchemaType = UnlinkedSchema{} + +// UnlinkedSchema defines the structure and value types of an unlinked action. An unlinked action +// cannot cause changes to resource state. +type UnlinkedSchema struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this action is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this action is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this action. The warning diagnostic + // summary is automatically set to "Action Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Use examplecloud_do_thing action instead. This action + // will be removed in the next major version of the provider." + // - "Remove this action as it no longer is valid and + // will be removed in the next major version of the provider." + // + DeprecationMessage string +} + +func (s UnlinkedSchema) isActionSchemaType() {} + +// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the +// schema. +func (s UnlinkedSchema) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.SchemaApplyTerraform5AttributePathStep(s, step) +} + +// AttributeAtPath returns the Attribute at the passed path. If the path points +// to an element or attribute of a complex type, rather than to an Attribute, +// it will return an ErrPathInsideAtomicAttribute error. +func (s UnlinkedSchema) AttributeAtPath(ctx context.Context, p path.Path) (fwschema.Attribute, diag.Diagnostics) { + return fwschema.SchemaAttributeAtPath(ctx, s, p) +} + +// AttributeAtPath returns the Attribute at the passed path. If the path points +// to an element or attribute of a complex type, rather than to an Attribute, +// it will return an ErrPathInsideAtomicAttribute error. +func (s UnlinkedSchema) AttributeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (fwschema.Attribute, error) { + return fwschema.SchemaAttributeAtTerraformPath(ctx, s, p) +} + +// GetAttributes returns the Attributes field value. +func (s UnlinkedSchema) GetAttributes() map[string]fwschema.Attribute { + return schemaAttributes(s.Attributes) +} + +// GetBlocks returns the Blocks field value. +func (s UnlinkedSchema) GetBlocks() map[string]fwschema.Block { + return schemaBlocks(s.Blocks) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (s UnlinkedSchema) GetDeprecationMessage() string { + return s.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (s UnlinkedSchema) GetDescription() string { + return s.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (s UnlinkedSchema) GetMarkdownDescription() string { + return s.MarkdownDescription +} + +// GetVersion always returns 0 as action schemas cannot be versioned. +func (s UnlinkedSchema) GetVersion() int64 { + return 0 +} + +// Type returns the framework type of the schema. +func (s UnlinkedSchema) Type() attr.Type { + return fwschema.SchemaType(s) +} + +// TypeAtPath returns the framework type at the given schema path. +func (s UnlinkedSchema) TypeAtPath(ctx context.Context, p path.Path) (attr.Type, diag.Diagnostics) { + return fwschema.SchemaTypeAtPath(ctx, s, p) +} + +// TypeAtTerraformPath returns the framework type at the given tftypes path. +func (s UnlinkedSchema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (attr.Type, error) { + return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) +} + +// ValidateImplementation contains logic for validating the provider-defined +// implementation of the schema and underlying attributes and blocks to prevent +// unexpected errors or panics. This logic runs during the GetProviderSchema RPC, +// or via provider-defined unit testing, and should never include false positives. +func (s UnlinkedSchema) ValidateImplementation(ctx context.Context) diag.Diagnostics { + var diags diag.Diagnostics + + for attributeName, attribute := range s.GetAttributes() { + req := fwschema.ValidateImplementationRequest{ + Name: attributeName, + Path: path.Root(attributeName), + } + + // TODO:Actions: We should confirm with core, but we should be able to remove this next line. + // + // Action schemas define a specific "config" nested block in the action block, which means there + // shouldn't be any conflict with existing or future Terraform core attributes. + diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...) + diags.Append(fwschema.ValidateAttributeImplementation(ctx, attribute, req)...) + } + + for blockName, block := range s.GetBlocks() { + req := fwschema.ValidateImplementationRequest{ + Name: blockName, + Path: path.Root(blockName), + } + + // TODO:Actions: We should confirm with core, but we should be able to remove this next line. + // + // Action schemas define a specific "config" nested block in the action block, which means there + // shouldn't be any conflict with existing or future Terraform core attributes. + diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...) + diags.Append(fwschema.ValidateBlockImplementation(ctx, block, req)...) + } + + return diags +} diff --git a/action/schema/unlinked_schema_test.go b/action/schema/unlinked_schema_test.go new file mode 100644 index 000000000..1af3164f7 --- /dev/null +++ b/action/schema/unlinked_schema_test.go @@ -0,0 +1,1250 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("could not find attribute or block \"other\" in schema"), + }, + "ElementKeyInt": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + path path.Path + expected fwschema.Attribute + expectedDiags diag.Diagnostics + }{ + "empty-root": { + schema: schema.UnlinkedSchema{}, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: \n"+ + "Original Error: got unexpected type schema.UnlinkedSchema", + ), + }, + }, + "root": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: \n"+ + "Original Error: got unexpected type schema.UnlinkedSchema", + ), + }, + }, + "WithAttributeName-attribute": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "other": schema.BoolAttribute{}, + "test": schema.StringAttribute{}, + }, + }, + path: path.Root("test"), + expected: schema.StringAttribute{}, + }, + "WithAttributeName-block": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "other": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "otherattr": schema.StringAttribute{}, + }, + }, + "test": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + path: path.Root("test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: test\n"+ + "Original Error: "+fwschema.ErrPathIsBlock.Error(), + ), + }, + }, + "WithElementKeyInt": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty().AtListIndex(0), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "WithElementKeyString": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty().AtMapKey("test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("test"), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [\"test\"]\n"+ + "Original Error: ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "WithElementKeyValue": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty().AtSetValue(types.StringValue("test")), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringValue("test")), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [Value(\"test\")]\n"+ + "Original Error: ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, tc := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := tc.schema.AttributeAtPath(context.Background(), tc.path) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + path *tftypes.AttributePath + expected fwschema.Attribute + expectedErr string + }{ + "empty-root": { + schema: schema.UnlinkedSchema{}, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type schema.UnlinkedSchema", + }, + "empty-nil": { + schema: schema.UnlinkedSchema{}, + path: nil, + expected: nil, + expectedErr: "got unexpected type schema.UnlinkedSchema", + }, + "root": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type schema.UnlinkedSchema", + }, + "nil": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: nil, + expected: nil, + expectedErr: "got unexpected type schema.UnlinkedSchema", + }, + "WithAttributeName-attribute": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "other": schema.BoolAttribute{}, + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: schema.StringAttribute{}, + }, + "WithAttributeName-block": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "other": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "otherattr": schema.StringAttribute{}, + }, + }, + "test": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: nil, + expectedErr: fwschema.ErrPathIsBlock.Error(), + }, + "WithElementKeyInt": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expected: nil, + expectedErr: "ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + }, + "WithElementKeyString": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyString("test"), + expected: nil, + expectedErr: "ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + }, + "WithElementKeyValue": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedErr: "ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + }, + } + + for name, tc := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := tc.schema.AttributeAtTerraformPath(context.Background(), tc.path) + + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + expected map[string]fwschema.Attribute + }{ + "no-attributes": { + schema: schema.UnlinkedSchema{}, + expected: map[string]fwschema.Attribute{}, + }, + "attributes": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: map[string]fwschema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetBlocks(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + expected map[string]fwschema.Block + }{ + "no-blocks": { + schema: schema.UnlinkedSchema{}, + expected: map[string]fwschema.Block{}, + }, + "blocks": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: map[string]fwschema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetBlocks() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + expected string + }{ + "no-deprecation-message": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + schema: schema.UnlinkedSchema{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + expected string + }{ + "no-description": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + schema: schema.UnlinkedSchema{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + expected string + }{ + "no-markdown-description": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + schema: schema.UnlinkedSchema{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetVersion(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + expected int64 + }{ + "no-version": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: 0, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetVersion() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + expected attr.Type + }{ + "base": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + path path.Path + expected attr.Type + expectedDiags diag.Diagnostics + }{ + "empty-schema-empty-path": { + schema: schema.UnlinkedSchema{}, + path: path.Empty(), + expected: types.ObjectType{}, + }, + "empty-path": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: path.Root("string"), + expected: types.StringType, + }, + "AttributeName-Block": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "single_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_block_nested": schema.StringAttribute{}, + }, + }, + }, + }, + path: path.Root("single_block"), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "single_block_nested": types.StringType, + }, + }, + }, + "AttributeName-non-existent": { + schema: schema.UnlinkedSchema{}, + path: path.Root("non-existent"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("non-existent"), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: non-existent\n"+ + "Original Error: AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema", + ), + }, + }, + "ElementKeyInt": { + schema: schema.UnlinkedSchema{}, + path: path.Empty().AtListIndex(0), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "ElementKeyString": { + schema: schema.UnlinkedSchema{}, + path: path.Empty().AtMapKey("invalid"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("invalid"), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [\"invalid\"]\n"+ + "Original Error: ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "ElementKeyValue": { + schema: schema.UnlinkedSchema{}, + path: path.Empty().AtSetValue(types.StringNull()), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringNull()), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [Value()]\n"+ + "Original Error: ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.schema.TypeAtPath(context.Background(), testCase.path) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + path *tftypes.AttributePath + expected attr.Type + expectedError error + }{ + "empty-schema-nil-path": { + schema: schema.UnlinkedSchema{}, + path: nil, + expected: types.ObjectType{}, + }, + "empty-schema-empty-path": { + schema: schema.UnlinkedSchema{}, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{}, + }, + "nil-path": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: nil, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "empty-path": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("string"), + expected: types.StringType, + }, + "AttributeName-Block": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "single_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_block_nested": schema.StringAttribute{}, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("single_block"), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "single_block_nested": types.StringType, + }, + }, + }, + "AttributeName-non-existent": { + schema: schema.UnlinkedSchema{}, + path: tftypes.NewAttributePath().WithAttributeName("non-existent"), + expectedError: fmt.Errorf("AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema"), + }, + "ElementKeyInt": { + schema: schema.UnlinkedSchema{}, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expectedError: fmt.Errorf("ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: schema.UnlinkedSchema{}, + path: tftypes.NewAttributePath().WithElementKeyString("invalid"), + expectedError: fmt.Errorf("ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: schema.UnlinkedSchema{}, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, nil)), + expectedError: fmt.Errorf("ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.TypeAtTerraformPath(context.Background(), testCase.path) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.UnlinkedSchema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: schema.UnlinkedSchema{}, + }, + "attribute-using-reserved-field-name": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "block-using-reserved-field-name": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "connection": schema.SingleNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"connection\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "nested-attribute-using-nested-reserved-field-name": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "nested-block-using-nested-reserved-field-name": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "connection": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "attribute-and-blocks-using-reserved-field-names": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "connection": schema.SingleNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"connection\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "attribute-using-invalid-field-name": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "^": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "block-using-invalid-field-name": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "^": schema.SingleNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "nested-attribute-using-nested-invalid-field-name": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"single_nested_attribute.^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "nested-block-using-nested-invalid-field-name": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"single_nested_block.^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "nested-block-with-nested-block-using-invalid-field-names": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "$": schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "^": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "!": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"$\" at schema path \"$\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"$.^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"!\" at schema path \"$.^.!\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "attribute-with-validate-attribute-implementation-error": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Required: true, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + "nested-attribute-with-validate-attribute-implementation-error": { + schema: schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Required: true, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"single_nested_attribute.test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + "nested-block-attribute-with-validate-attribute-implementation-error": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Required: true, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"single_nested_block.test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + "nested-nested-block-attribute-with-validate-attribute-implementation-error": { + schema: schema.UnlinkedSchema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "single_nested_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"single_nested_block.single_nested_nested_block.test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.schema.ValidateImplementation(context.Background()) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} From 0e5df9cca30cd61fc5cea206704acf738d1da501 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 10 Jul 2025 16:37:34 -0400 Subject: [PATCH 06/21] implement unlinked schemas, some attributes, and the rpcs --- action/schema.go | 15 +- action/schema/schema_type.go | 17 +- internal/fwserver/server.go | 3 +- internal/fwserver/server_actions.go | 61 +++- internal/fwserver/server_getmetadata.go | 13 + internal/fwserver/server_getmetadata_test.go | 184 +++++++++++ internal/fwserver/server_getproviderschema.go | 9 + .../fwserver/server_getproviderschema_test.go | 311 ++++++++++++++++++ internal/testing/testprovider/action.go | 37 +++ internal/testing/testprovider/provider.go | 11 + internal/toproto5/action_schema.go | 42 +++ internal/toproto5/action_schema_test.go | 125 +++++++ internal/toproto5/actionmetadata.go | 19 ++ internal/toproto5/actionmetadata_test.go | 44 +++ internal/toproto5/getmetadata.go | 5 + internal/toproto5/getmetadata_test.go | 34 ++ internal/toproto5/getproviderschema.go | 15 + internal/toproto5/getproviderschema_test.go | 250 ++++++++++++++ internal/toproto6/action_schema.go | 42 +++ internal/toproto6/action_schema_test.go | 125 +++++++ internal/toproto6/actionmetadata.go | 19 ++ internal/toproto6/actionmetadata_test.go | 44 +++ internal/toproto6/getmetadata.go | 5 + internal/toproto6/getmetadata_test.go | 34 ++ internal/toproto6/getproviderschema.go | 15 + internal/toproto6/getproviderschema_test.go | 262 +++++++++++++++ 26 files changed, 1731 insertions(+), 10 deletions(-) create mode 100644 internal/testing/testprovider/action.go create mode 100644 internal/toproto5/action_schema.go create mode 100644 internal/toproto5/action_schema_test.go create mode 100644 internal/toproto5/actionmetadata.go create mode 100644 internal/toproto5/actionmetadata_test.go create mode 100644 internal/toproto6/action_schema.go create mode 100644 internal/toproto6/action_schema_test.go create mode 100644 internal/toproto6/actionmetadata.go create mode 100644 internal/toproto6/actionmetadata_test.go diff --git a/action/schema.go b/action/schema.go index 9eb52bf7b..d8bc1b72d 100644 --- a/action/schema.go +++ b/action/schema.go @@ -4,8 +4,8 @@ package action import ( + "github.com/hashicorp/terraform-plugin-framework/action/schema" "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. @@ -17,10 +17,15 @@ type SchemaRequest struct{} // 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 + + // Schema is the schema of the action. + // + // There are three different types of actions, which define how a practitioner can trigger an action, + // as well as what effect the action can have on the state. + // - [schema.UnlinkedSchema] actions are actions that cannot cause changes to resource states. + // - [schema.LifecycleSchema] actions are actions that can cause changes to exactly one resource state. + // - [schema.LinkedSchema] actions are actions that can cause changes to one or more resource states. + Schema schema.SchemaType // Diagnostics report errors or warnings related to retrieving the action schema. // An empty slice indicates success, with no warnings or errors generated. diff --git a/action/schema/schema_type.go b/action/schema/schema_type.go index 04122adf9..f9c5d689a 100644 --- a/action/schema/schema_type.go +++ b/action/schema/schema_type.go @@ -3,7 +3,12 @@ package schema -import "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) // TODO:Actions: Implement lifecycle and linked schemas // @@ -20,6 +25,16 @@ import "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" type SchemaType interface { fwschema.Schema + // MAINTAINER NOTE: Action schemas are unique to other schema types in framework in that the + // exported methods all return a schema interface ([SchemaType]) rather than a schema struct, + // due to the multiple different types of action schema implementations. + // + // As a result, there are certain methods that all schema structs implement that aren't defined in + // the [fwschema.Schema] interface, such as the ValidateImplementation method. So we are adding that + // here to the action schema interface to avoid additional internal interfaces and unnecessary + // type assertions. + ValidateImplementation(context.Context) diag.Diagnostics + // Action schema types are statically defined in the protocol, so this // interface is not meant to be implemented outside of this package isActionSchemaType() diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 3fef10f28..22e2c2eb2 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -9,6 +9,7 @@ import ( "sync" "github.com/hashicorp/terraform-plugin-framework/action" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" @@ -44,7 +45,7 @@ type Server struct { // 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 + actionSchemas map[string]actionschema.SchemaType // actionSchemasMutex is a mutex to protect concurrent actionSchemas // access from race conditions. diff --git a/internal/fwserver/server_actions.go b/internal/fwserver/server_actions.go index 73f2b68a8..13d2e381f 100644 --- a/internal/fwserver/server_actions.go +++ b/internal/fwserver/server_actions.go @@ -8,8 +8,8 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework/action" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "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" ) @@ -94,9 +94,25 @@ func (s *Server) ActionFuncs(ctx context.Context) (map[string]func() action.Acti return s.actionFuncs, s.actionFuncsDiags } +// ActionMetadatas returns a slice of ActionMetadata for the GetMetadata +// RPC. +func (s *Server) ActionMetadatas(ctx context.Context) ([]ActionMetadata, diag.Diagnostics) { + actionFuncs, diags := s.ActionFuncs(ctx) + + actionMetadatas := make([]ActionMetadata, 0, len(actionFuncs)) + + for typeName := range actionFuncs { + actionMetadatas = append(actionMetadatas, ActionMetadata{ + TypeName: typeName, + }) + } + + return actionMetadatas, diags +} + // 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) { +func (s *Server) ActionSchema(ctx context.Context, actionType string) (actionschema.SchemaType, diag.Diagnostics) { s.actionSchemasMutex.RLock() actionSchema, ok := s.actionSchemas[actionType] s.actionSchemasMutex.RUnlock() @@ -131,7 +147,7 @@ func (s *Server) ActionSchema(ctx context.Context, actionType string) (fwschema. s.actionSchemasMutex.Lock() if s.actionSchemas == nil { - s.actionSchemas = make(map[string]fwschema.Schema) + s.actionSchemas = make(map[string]actionschema.SchemaType) } s.actionSchemas[actionType] = schemaResp.Schema @@ -140,3 +156,42 @@ func (s *Server) ActionSchema(ctx context.Context, actionType string) (fwschema. return schemaResp.Schema, diags } + +// ActionSchemas returns a map of Action Schemas for the +// GetProviderSchema RPC without caching since not all schemas are guaranteed to +// be necessary for later provider operations. The schema implementations are +// also validated. +func (s *Server) ActionSchemas(ctx context.Context) (map[string]actionschema.SchemaType, diag.Diagnostics) { + actionSchemas := make(map[string]actionschema.SchemaType) + + actionFuncs, diags := s.ActionFuncs(ctx) + + for typeName, actionFunc := range actionFuncs { + actionImpl := actionFunc() + + schemaReq := action.SchemaRequest{} + schemaResp := action.SchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Action Schema", map[string]interface{}{logging.KeyActionType: typeName}) + actionImpl.Schema(ctx, schemaReq, &schemaResp) + logging.FrameworkTrace(ctx, "Called provider defined Action Schema", map[string]interface{}{logging.KeyActionType: typeName}) + + diags.Append(schemaResp.Diagnostics...) + + if schemaResp.Diagnostics.HasError() { + continue + } + + validateDiags := schemaResp.Schema.ValidateImplementation(ctx) + + diags.Append(validateDiags...) + + if validateDiags.HasError() { + continue + } + + actionSchemas[typeName] = schemaResp.Schema + } + + return actionSchemas, diags +} diff --git a/internal/fwserver/server_getmetadata.go b/internal/fwserver/server_getmetadata.go index 33b675865..ea75b7651 100644 --- a/internal/fwserver/server_getmetadata.go +++ b/internal/fwserver/server_getmetadata.go @@ -16,6 +16,7 @@ type GetMetadataRequest struct{} // GetMetadataResponse is the framework server response for the // GetMetadata RPC. type GetMetadataResponse struct { + Actions []ActionMetadata DataSources []DataSourceMetadata Diagnostics diag.Diagnostics EphemeralResources []EphemeralResourceMetadata @@ -60,8 +61,16 @@ type ListResourceMetadata struct { TypeName string } +// ActionMetadata is the framework server equivalent of the +// tfprotov5.ActionMetadata and tfprotov6.ActionMetadata types. +type ActionMetadata struct { + // TypeName is the name of the action. + TypeName string +} + // GetMetadata implements the framework server GetMetadata RPC. func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp *GetMetadataResponse) { + resp.Actions = []ActionMetadata{} resp.DataSources = []DataSourceMetadata{} resp.EphemeralResources = []EphemeralResourceMetadata{} resp.Functions = []FunctionMetadata{} @@ -70,6 +79,9 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp resp.ServerCapabilities = s.ServerCapabilities() + actionMetadatas, diags := s.ActionMetadatas(ctx) + resp.Diagnostics.Append(diags...) + datasourceMetadatas, diags := s.DataSourceMetadatas(ctx) resp.Diagnostics.Append(diags...) @@ -92,6 +104,7 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp return } + resp.Actions = actionMetadatas resp.DataSources = datasourceMetadatas resp.EphemeralResources = ephemeralResourceMetadatas resp.Functions = functionMetadatas diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index dff3b7378..798a1a77a 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "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" @@ -35,6 +36,170 @@ func TestServerGetMetadata(t *testing.T) { Provider: &testprovider.Provider{}, }, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "actions": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action1" + }, + } + }, + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{ + { + TypeName: "test_action1", + }, + { + TypeName: "test_action2", + }, + }, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "actions-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Action Defined", + "The test_action action type was returned for multiple actions. "+ + "Action types must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "actions-empty-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Action Type Missing", + "The *testprovider.Action Action returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "actions-provider-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + MetadataMethod: func(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "testprovidertype" + }, + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, req action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_action" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{ + { + TypeName: "testprovidertype_action", + }, + }, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{}, @@ -71,6 +236,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{ { TypeName: "test_data_source1", @@ -114,6 +280,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -151,6 +318,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -190,6 +358,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{ { TypeName: "testprovidertype_data_source", @@ -230,6 +399,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{ { @@ -273,6 +443,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -310,6 +481,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -349,6 +521,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{ { @@ -389,6 +562,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{ @@ -432,6 +606,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -469,6 +644,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -516,6 +692,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{}, @@ -551,6 +728,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -605,6 +783,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -653,6 +832,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -696,6 +876,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{}, @@ -739,6 +920,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -776,6 +958,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ @@ -815,6 +998,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{}, DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{}, diff --git a/internal/fwserver/server_getproviderschema.go b/internal/fwserver/server_getproviderschema.go index c695a7530..74a25446e 100644 --- a/internal/fwserver/server_getproviderschema.go +++ b/internal/fwserver/server_getproviderschema.go @@ -6,6 +6,7 @@ package fwserver import ( "context" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -21,6 +22,7 @@ type GetProviderSchemaResponse struct { ServerCapabilities *ServerCapabilities Provider fwschema.Schema ProviderMeta fwschema.Schema + ActionSchemas map[string]actionschema.SchemaType ResourceSchemas map[string]fwschema.Schema DataSourceSchemas map[string]fwschema.Schema EphemeralResourceSchemas map[string]fwschema.Schema @@ -84,4 +86,11 @@ func (s *Server) GetProviderSchema(ctx context.Context, req *GetProviderSchemaRe return } resp.ListResourceSchemas = listResourceSchemas + + actionSchemas, diags := s.ActionSchemas(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + resp.ActionSchemas = actionSchemas } diff --git a/internal/fwserver/server_getproviderschema_test.go b/internal/fwserver/server_getproviderschema_test.go index 6caecd976..014ce72f3 100644 --- a/internal/fwserver/server_getproviderschema_test.go +++ b/internal/fwserver/server_getproviderschema_test.go @@ -9,6 +9,8 @@ import ( "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -40,6 +42,305 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: &testprovider.Provider{}, }, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "actionschemas": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test1": actionschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action1" + }, + } + }, + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test2": actionschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{ + "test_action1": actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test1": actionschema.StringAttribute{ + Required: true, + }, + }, + }, + "test_action2": actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test2": actionschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + ProviderMeta: nil, + DataSourceSchemas: map[string]fwschema.Schema{}, + Diagnostics: diag.Diagnostics{}, + }, + }, + "actionschemas-invalid-attribute-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "$": actionschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action1" + }, + } + }, + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test2": actionschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"$\" at schema path \"$\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + ProviderMeta: nil, + ResourceSchemas: map[string]fwschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, + }, + }, + "actionschemas-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test1": actionschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test2": actionschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: nil, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Action Defined", + "The test_action action type was returned for multiple actions. "+ + "Action types must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + ProviderMeta: nil, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, + }, + }, + "actionschemas-empty-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: nil, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Action Type Missing", + "The *testprovider.Action Action returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + ProviderMeta: nil, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, + }, + }, + "actionschemas-provider-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + MetadataMethod: func(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "testprovidertype" + }, + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test": actionschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, req action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_action" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{ + "testprovidertype_action": actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test": actionschema.StringAttribute{ + Required: true, + }, + }, + }, + }, DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, @@ -96,6 +397,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, DataSourceSchemas: map[string]fwschema.Schema{ "test_data_source1": datasourceschema.Schema{ Attributes: map[string]datasourceschema.Attribute{ @@ -311,6 +613,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, DataSourceSchemas: map[string]fwschema.Schema{ "testprovidertype_data_source": datasourceschema.Schema{ Attributes: map[string]datasourceschema.Attribute{ @@ -375,6 +678,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{ "test_ephemeral_resource1": ephemeralschema.Schema{ @@ -596,6 +900,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{ "testprovidertype_ephemeral_resource": ephemeralschema.Schema{ @@ -652,6 +957,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{ @@ -863,6 +1169,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, @@ -984,6 +1291,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, @@ -1052,6 +1360,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, @@ -1149,6 +1458,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, @@ -1363,6 +1673,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{}, DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, diff --git a/internal/testing/testprovider/action.go b/internal/testing/testprovider/action.go new file mode 100644 index 000000000..610ee8326 --- /dev/null +++ b/internal/testing/testprovider/action.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/action" +) + +var _ action.Action = &Action{} + +// Declarative action.Action for unit testing. +type Action struct { + // Action interface methods + MetadataMethod func(context.Context, action.MetadataRequest, *action.MetadataResponse) + SchemaMethod func(context.Context, action.SchemaRequest, *action.SchemaResponse) +} + +// Metadata satisfies the action.Action interface. +func (d *Action) Metadata(ctx context.Context, req action.MetadataRequest, resp *action.MetadataResponse) { + if d.MetadataMethod == nil { + return + } + + d.MetadataMethod(ctx, req, resp) +} + +// Schema satisfies the action.Action interface. +func (d *Action) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) { + if d.SchemaMethod == nil { + return + } + + d.SchemaMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/provider.go b/internal/testing/testprovider/provider.go index efad3b8a4..b039be76d 100644 --- a/internal/testing/testprovider/provider.go +++ b/internal/testing/testprovider/provider.go @@ -6,6 +6,7 @@ package testprovider 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/list" @@ -21,6 +22,7 @@ type Provider struct { MetadataMethod func(context.Context, provider.MetadataRequest, *provider.MetadataResponse) ConfigureMethod func(context.Context, provider.ConfigureRequest, *provider.ConfigureResponse) SchemaMethod func(context.Context, provider.SchemaRequest, *provider.SchemaResponse) + ActionsMethod func(context.Context) []func() action.Action DataSourcesMethod func(context.Context) []func() datasource.DataSource EphemeralResourcesMethod func(context.Context) []func() ephemeral.EphemeralResource ListResourcesMethod func(context.Context) []func() list.ListResource @@ -36,6 +38,15 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, p.ConfigureMethod(ctx, req, resp) } +// Actions satisfies the provider.Provider interface. +func (p *Provider) Actions(ctx context.Context) []func() action.Action { + if p == nil || p.ActionsMethod == nil { + return nil + } + + return p.ActionsMethod(ctx) +} + // DataSources satisfies the provider.Provider interface. func (p *Provider) DataSources(ctx context.Context) []func() datasource.DataSource { if p == nil || p.DataSourcesMethod == nil { diff --git a/internal/toproto5/action_schema.go b/internal/toproto5/action_schema.go new file mode 100644 index 000000000..3b3296668 --- /dev/null +++ b/internal/toproto5/action_schema.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + "fmt" + + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ActionSchema returns the *tfprotov5.ActionSchema equivalent of a ActionSchema. +func ActionSchema(ctx context.Context, s actionschema.SchemaType) (*tfprotov5.ActionSchema, error) { + if s == nil { + return nil, nil + } + + configSchema, err := Schema(ctx, s) + if err != nil { + return nil, err + } + + result := &tfprotov5.ActionSchema{ + Schema: configSchema, + } + + // TODO:Actions: Implement linked and lifecycle action schema types + switch s.(type) { + case actionschema.UnlinkedSchema: + result.Type = tfprotov5.UnlinkedActionSchemaType{} + default: + // It is not currently possible to create [actionschema.SchemaType] + // implementations outside the "action/schema" package. If this error was reached, + // it implies that a new event type was introduced and needs to be implemented + // as a new case above. + return nil, fmt.Errorf("unimplemented schema.SchemaType type: %T", s) + } + + return result, nil +} diff --git a/internal/toproto5/action_schema_test.go b/internal/toproto5/action_schema_test.go new file mode 100644 index 000000000..717d19162 --- /dev/null +++ b/internal/toproto5/action_schema_test.go @@ -0,0 +1,125 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestActionSchema(t *testing.T) { + t.Parallel() + + type testCase struct { + input actionschema.SchemaType + expected *tfprotov5.ActionSchema + expectedErr string + } + + tests := map[string]testCase{ + "nil": { + input: nil, + expected: nil, + }, + "unlinked": { + input: actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "bool": actionschema.BoolAttribute{ + Optional: true, + }, + "string": actionschema.StringAttribute{ + Required: true, + }, + }, + Blocks: map[string]actionschema.Block{ + "single_block": schema.SingleNestedBlock{ + Attributes: map[string]actionschema.Attribute{ + "bool": actionschema.BoolAttribute{ + Required: true, + }, + "string": actionschema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.ActionSchema{ + Type: tfprotov5.UnlinkedActionSchemaType{}, + Schema: &tfprotov5.Schema{ + Version: 0, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Optional: true, + }, + { + Name: "string", + Type: tftypes.String, + Required: true, + }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "single_block", + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + { + Name: "string", + Type: tftypes.String, + Optional: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + }, + }, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := toproto5.ActionSchema(context.Background(), tc.input) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/toproto5/actionmetadata.go b/internal/toproto5/actionmetadata.go new file mode 100644 index 000000000..7f7fb5a7b --- /dev/null +++ b/internal/toproto5/actionmetadata.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ActionMetadata returns the tfprotov5.ActionMetadata for a +// fwserver.ActionMetadata. +func ActionMetadata(ctx context.Context, fw fwserver.ActionMetadata) tfprotov5.ActionMetadata { + return tfprotov5.ActionMetadata{ + TypeName: fw.TypeName, + } +} diff --git a/internal/toproto5/actionmetadata_test.go b/internal/toproto5/actionmetadata_test.go new file mode 100644 index 000000000..d3707f46f --- /dev/null +++ b/internal/toproto5/actionmetadata_test.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestActionMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.ActionMetadata + expected tfprotov5.ActionMetadata + }{ + "TypeName": { + fw: fwserver.ActionMetadata{ + TypeName: "test", + }, + expected: tfprotov5.ActionMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.ActionMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/getmetadata.go b/internal/toproto5/getmetadata.go index 8039a892c..608aab93e 100644 --- a/internal/toproto5/getmetadata.go +++ b/internal/toproto5/getmetadata.go @@ -18,6 +18,7 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) } protov5 := &tfprotov5.GetMetadataResponse{ + Actions: make([]tfprotov5.ActionMetadata, 0, len(fw.Actions)), DataSources: make([]tfprotov5.DataSourceMetadata, 0, len(fw.DataSources)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), EphemeralResources: make([]tfprotov5.EphemeralResourceMetadata, 0, len(fw.EphemeralResources)), @@ -27,6 +28,10 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } + for _, action := range fw.Actions { + protov5.Actions = append(protov5.Actions, ActionMetadata(ctx, action)) + } + for _, datasource := range fw.DataSources { protov5.DataSources = append(protov5.DataSources, DataSourceMetadata(ctx, datasource)) } diff --git a/internal/toproto5/getmetadata_test.go b/internal/toproto5/getmetadata_test.go index 2180ec781..e0c4c1960 100644 --- a/internal/toproto5/getmetadata_test.go +++ b/internal/toproto5/getmetadata_test.go @@ -25,6 +25,33 @@ func TestGetMetadataResponse(t *testing.T) { input: nil, expected: nil, }, + "actions": { + input: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{ + { + TypeName: "test_action_1", + }, + { + TypeName: "test_action_2", + }, + }, + }, + expected: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{ + { + TypeName: "test_action_1", + }, + { + TypeName: "test_action_2", + }, + }, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, + ListResources: []tfprotov5.ListResourceMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + }, + }, "datasources": { input: &fwserver.GetMetadataResponse{ DataSources: []fwserver.DataSourceMetadata{ @@ -37,6 +64,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{ { TypeName: "test_data_source_1", @@ -63,6 +91,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{}, EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ { @@ -89,6 +118,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { @@ -117,6 +147,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{}, EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{ @@ -143,6 +174,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{}, EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{}, @@ -169,6 +201,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{}, EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{}, @@ -191,6 +224,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{}, EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{}, diff --git a/internal/toproto5/getproviderschema.go b/internal/toproto5/getproviderschema.go index be12254dc..98e6bee24 100644 --- a/internal/toproto5/getproviderschema.go +++ b/internal/toproto5/getproviderschema.go @@ -18,6 +18,7 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } protov5 := &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: make(map[string]*tfprotov5.ActionSchema, len(fw.ActionSchemas)), DataSourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.DataSourceSchemas)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), EphemeralResourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.EphemeralResourceSchemas)), @@ -53,6 +54,20 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche return protov5 } + for actionType, actionSchema := range fw.ActionSchemas { + protov5.ActionSchemas[actionType], err = ActionSchema(ctx, actionSchema) + + if err != nil { + protov5.Diagnostics = append(protov5.Diagnostics, &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting action schema", + Detail: "The schema for the action \"" + actionType + "\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\n" + err.Error(), + }) + + return protov5 + } + } + for dataSourceType, dataSourceSchema := range fw.DataSourceSchemas { protov5.DataSourceSchemas[dataSourceType], err = Schema(ctx, dataSourceSchema) diff --git a/internal/toproto5/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index f23925017..807850a07 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/attr" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -38,6 +39,102 @@ func TestGetProviderSchemaResponse(t *testing.T) { input: nil, expected: nil, }, + "action-multiple-actions": { + input: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{ + "test_action_1": actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test_attribute": actionschema.StringAttribute{ + Required: true, + }, + }, + }, + "test_action_2": actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test_attribute": actionschema.StringAttribute{ + Optional: true, + DeprecationMessage: "deprecated", + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{ + "test_action_1": { + Type: tfprotov5.UnlinkedActionSchemaType{}, + Schema: &tfprotov5.Schema{ + Version: 0, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + "test_action_2": { + Type: tfprotov5.UnlinkedActionSchemaType{}, + Schema: &tfprotov5.Schema{ + Version: 0, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Optional: true, + Deprecated: true, + }, + }, + }, + }, + }, + }, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ListResourceSchemas: map[string]*tfprotov5.Schema{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "action-type-invalid-proto-6-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{ + "test_action": actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test_attribute": actionschema.SingleNestedAttribute{ + Attributes: map[string]actionschema.Attribute{ + "test_nested_attribute": actionschema.StringAttribute{ + Required: true, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{ + "test_action": nil, + }, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting action schema", + Detail: "The schema for the action \"test_action\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ListResourceSchemas: map[string]*tfprotov5.Schema{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "data-source-multiple-data-sources": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -58,6 +155,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source_1": { Block: &tfprotov5.SchemaBlock{ @@ -101,6 +199,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -134,6 +233,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -167,6 +267,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -200,6 +301,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -233,6 +335,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -266,6 +369,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -299,6 +403,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -332,6 +437,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -364,6 +470,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -396,6 +503,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -428,6 +536,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -460,6 +569,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -495,6 +605,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -538,6 +649,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": nil, }, @@ -572,6 +684,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -611,6 +724,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -652,6 +766,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": nil, }, @@ -682,6 +797,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -716,6 +832,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -751,6 +868,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -794,6 +912,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": nil, }, @@ -828,6 +947,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -869,6 +989,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -906,6 +1027,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -945,6 +1067,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": nil, }, @@ -974,6 +1097,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -1006,6 +1130,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -1044,6 +1169,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -1090,6 +1216,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -1134,6 +1261,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{ "test_data_source": { Block: &tfprotov5.SchemaBlock{ @@ -1181,6 +1309,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource_1": { @@ -1224,6 +1353,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1257,6 +1387,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1290,6 +1421,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1323,6 +1455,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1356,6 +1489,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1389,6 +1523,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1422,6 +1557,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1455,6 +1591,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1487,6 +1624,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1519,6 +1657,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1551,6 +1690,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1583,6 +1723,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1618,6 +1759,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1661,6 +1803,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": nil, @@ -1695,6 +1838,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1734,6 +1878,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1775,6 +1920,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": nil, @@ -1805,6 +1951,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1839,6 +1986,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1874,6 +2022,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1917,6 +2066,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": nil, @@ -1951,6 +2101,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -1992,6 +2143,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -2029,6 +2181,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -2068,6 +2221,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": nil, @@ -2097,6 +2251,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -2129,6 +2284,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -2167,6 +2323,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -2213,6 +2370,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -2257,6 +2415,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": { @@ -2296,6 +2455,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ @@ -2326,6 +2486,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ @@ -2351,6 +2512,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ @@ -2380,6 +2542,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ @@ -2413,6 +2576,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ @@ -2437,6 +2601,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ @@ -2462,6 +2627,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ @@ -2499,6 +2665,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2543,6 +2710,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2576,6 +2744,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2608,6 +2777,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2640,6 +2810,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2671,6 +2842,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2701,6 +2873,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2730,6 +2903,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2760,6 +2934,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2790,6 +2965,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2820,6 +2996,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2849,6 +3026,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2878,6 +3056,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2907,6 +3086,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2936,6 +3116,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -2968,6 +3149,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3008,6 +3190,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -3038,6 +3221,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3074,6 +3258,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3112,6 +3297,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -3138,6 +3324,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3169,6 +3356,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3201,6 +3389,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3241,6 +3430,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -3271,6 +3461,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3309,6 +3500,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3343,6 +3535,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3379,6 +3572,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -3404,6 +3598,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3433,6 +3628,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3468,6 +3664,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3511,6 +3708,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3552,6 +3750,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3589,6 +3788,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3618,6 +3818,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3647,6 +3848,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3676,6 +3878,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3705,6 +3908,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3737,6 +3941,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3777,6 +3982,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -3807,6 +4013,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, ProviderMeta: &tfprotov5.Schema{ @@ -3843,6 +4050,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3881,6 +4089,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -3907,6 +4116,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3938,6 +4148,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -3970,6 +4181,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4010,6 +4222,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -4040,6 +4253,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4078,6 +4292,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4112,6 +4327,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4148,6 +4364,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -4173,6 +4390,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4211,6 +4429,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4254,6 +4473,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4287,6 +4507,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4320,6 +4541,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4353,6 +4575,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4386,6 +4609,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4419,6 +4643,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4453,6 +4678,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4486,6 +4712,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4518,6 +4745,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4550,6 +4778,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4582,6 +4811,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4614,6 +4844,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4649,6 +4880,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4692,6 +4924,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -4726,6 +4959,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4765,6 +4999,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4806,6 +5041,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -4836,6 +5072,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4870,6 +5107,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4905,6 +5143,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -4948,6 +5187,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -4982,6 +5222,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -5023,6 +5264,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -5060,6 +5302,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -5099,6 +5342,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ @@ -5128,6 +5372,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -5160,6 +5405,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -5198,6 +5444,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -5244,6 +5491,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -5288,6 +5536,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -5324,6 +5573,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, diff --git a/internal/toproto6/action_schema.go b/internal/toproto6/action_schema.go new file mode 100644 index 000000000..94aaa7416 --- /dev/null +++ b/internal/toproto6/action_schema.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + "fmt" + + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ActionSchema returns the *tfprotov6.ActionSchema equivalent of a ActionSchema. +func ActionSchema(ctx context.Context, s actionschema.SchemaType) (*tfprotov6.ActionSchema, error) { + if s == nil { + return nil, nil + } + + configSchema, err := Schema(ctx, s) + if err != nil { + return nil, err + } + + result := &tfprotov6.ActionSchema{ + Schema: configSchema, + } + + // TODO:Actions: Implement linked and lifecycle action schema types + switch s.(type) { + case actionschema.UnlinkedSchema: + result.Type = tfprotov6.UnlinkedActionSchemaType{} + default: + // It is not currently possible to create [actionschema.SchemaType] + // implementations outside the "action/schema" package. If this error was reached, + // it implies that a new event type was introduced and needs to be implemented + // as a new case above. + return nil, fmt.Errorf("unimplemented schema.SchemaType type: %T", s) + } + + return result, nil +} diff --git a/internal/toproto6/action_schema_test.go b/internal/toproto6/action_schema_test.go new file mode 100644 index 000000000..20de2d0f6 --- /dev/null +++ b/internal/toproto6/action_schema_test.go @@ -0,0 +1,125 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestActionSchema(t *testing.T) { + t.Parallel() + + type testCase struct { + input actionschema.SchemaType + expected *tfprotov6.ActionSchema + expectedErr string + } + + tests := map[string]testCase{ + "nil": { + input: nil, + expected: nil, + }, + "unlinked": { + input: actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "bool": actionschema.BoolAttribute{ + Optional: true, + }, + "string": actionschema.StringAttribute{ + Required: true, + }, + }, + Blocks: map[string]actionschema.Block{ + "single_block": schema.SingleNestedBlock{ + Attributes: map[string]actionschema.Attribute{ + "bool": actionschema.BoolAttribute{ + Required: true, + }, + "string": actionschema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.ActionSchema{ + Type: tfprotov6.UnlinkedActionSchemaType{}, + Schema: &tfprotov6.Schema{ + Version: 0, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Optional: true, + }, + { + Name: "string", + Type: tftypes.String, + Required: true, + }, + }, + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + TypeName: "single_block", + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + Required: true, + }, + { + Name: "string", + Type: tftypes.String, + Optional: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + }, + }, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := toproto6.ActionSchema(context.Background(), tc.input) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/toproto6/actionmetadata.go b/internal/toproto6/actionmetadata.go new file mode 100644 index 000000000..fb527d867 --- /dev/null +++ b/internal/toproto6/actionmetadata.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ActionMetadata returns the tfprotov6.ActionMetadata for a +// fwserver.ActionMetadata. +func ActionMetadata(ctx context.Context, fw fwserver.ActionMetadata) tfprotov6.ActionMetadata { + return tfprotov6.ActionMetadata{ + TypeName: fw.TypeName, + } +} diff --git a/internal/toproto6/actionmetadata_test.go b/internal/toproto6/actionmetadata_test.go new file mode 100644 index 000000000..d28e02dde --- /dev/null +++ b/internal/toproto6/actionmetadata_test.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestActionMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.ActionMetadata + expected tfprotov6.ActionMetadata + }{ + "TypeName": { + fw: fwserver.ActionMetadata{ + TypeName: "test", + }, + expected: tfprotov6.ActionMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.ActionMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/getmetadata.go b/internal/toproto6/getmetadata.go index bf235fe21..faedce552 100644 --- a/internal/toproto6/getmetadata.go +++ b/internal/toproto6/getmetadata.go @@ -18,6 +18,7 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) } protov6 := &tfprotov6.GetMetadataResponse{ + Actions: make([]tfprotov6.ActionMetadata, 0, len(fw.Actions)), DataSources: make([]tfprotov6.DataSourceMetadata, 0, len(fw.DataSources)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), EphemeralResources: make([]tfprotov6.EphemeralResourceMetadata, 0, len(fw.EphemeralResources)), @@ -27,6 +28,10 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } + for _, action := range fw.Actions { + protov6.Actions = append(protov6.Actions, ActionMetadata(ctx, action)) + } + for _, datasource := range fw.DataSources { protov6.DataSources = append(protov6.DataSources, DataSourceMetadata(ctx, datasource)) } diff --git a/internal/toproto6/getmetadata_test.go b/internal/toproto6/getmetadata_test.go index ee601823d..e4877f12e 100644 --- a/internal/toproto6/getmetadata_test.go +++ b/internal/toproto6/getmetadata_test.go @@ -25,6 +25,33 @@ func TestGetMetadataResponse(t *testing.T) { input: nil, expected: nil, }, + "actions": { + input: &fwserver.GetMetadataResponse{ + Actions: []fwserver.ActionMetadata{ + { + TypeName: "test_action_1", + }, + { + TypeName: "test_action_2", + }, + }, + }, + expected: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{ + { + TypeName: "test_action_1", + }, + { + TypeName: "test_action_2", + }, + }, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + ListResources: []tfprotov6.ListResourceMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + }, + }, "datasources": { input: &fwserver.GetMetadataResponse{ DataSources: []fwserver.DataSourceMetadata{ @@ -37,6 +64,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{ { TypeName: "test_data_source_1", @@ -63,6 +91,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{}, Diagnostics: []*tfprotov6.Diagnostic{ { @@ -91,6 +120,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{}, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ { @@ -117,6 +147,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{}, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{ @@ -143,6 +174,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{}, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{}, @@ -169,6 +201,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{}, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{}, @@ -191,6 +224,7 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{}, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{}, diff --git a/internal/toproto6/getproviderschema.go b/internal/toproto6/getproviderschema.go index e72a9d612..4fe9de985 100644 --- a/internal/toproto6/getproviderschema.go +++ b/internal/toproto6/getproviderschema.go @@ -18,6 +18,7 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } protov6 := &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: make(map[string]*tfprotov6.ActionSchema, len(fw.ActionSchemas)), DataSourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.DataSourceSchemas)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), EphemeralResourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.EphemeralResourceSchemas)), @@ -53,6 +54,20 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche return protov6 } + for actionType, actionSchema := range fw.ActionSchemas { + protov6.ActionSchemas[actionType], err = ActionSchema(ctx, actionSchema) + + if err != nil { + protov6.Diagnostics = append(protov6.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error converting action schema", + Detail: "The schema for the action \"" + actionType + "\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\n" + err.Error(), + }) + + return protov6 + } + } + for dataSourceType, dataSourceSchema := range fw.DataSourceSchemas { protov6.DataSourceSchemas[dataSourceType], err = Schema(ctx, dataSourceSchema) diff --git a/internal/toproto6/getproviderschema_test.go b/internal/toproto6/getproviderschema_test.go index ed57d5f97..5c0037b82 100644 --- a/internal/toproto6/getproviderschema_test.go +++ b/internal/toproto6/getproviderschema_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/attr" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -38,6 +39,118 @@ func TestGetProviderSchemaResponse(t *testing.T) { input: nil, expected: nil, }, + "action-multiple-actions": { + input: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{ + "test_action_1": actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test_attribute": actionschema.StringAttribute{ + Required: true, + }, + }, + }, + "test_action_2": actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test_attribute": actionschema.StringAttribute{ + Optional: true, + DeprecationMessage: "deprecated", + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{ + "test_action_1": { + Type: tfprotov6.UnlinkedActionSchemaType{}, + Schema: &tfprotov6.Schema{ + Version: 0, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + "test_action_2": { + Type: tfprotov6.UnlinkedActionSchemaType{}, + Schema: &tfprotov6.Schema{ + Version: 0, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Optional: true, + Deprecated: true, + }, + }, + }, + }, + }, + }, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + ListResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "action-type-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + ActionSchemas: map[string]actionschema.SchemaType{ + "test_action": actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test_attribute": actionschema.SingleNestedAttribute{ + Attributes: map[string]actionschema.Attribute{ + "test_nested_attribute": actionschema.StringAttribute{ + Required: true, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{ + "test_action": { + Type: tfprotov6.UnlinkedActionSchemaType{}, + Schema: &tfprotov6.Schema{ + Version: 0, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_nested_attribute", + Type: tftypes.String, + Required: true, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + }, + Required: true, + }, + }, + }, + }, + }, + }, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + ListResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "data-source-multiple-data-sources": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -58,6 +171,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source_1": { Block: &tfprotov6.SchemaBlock{ @@ -101,6 +215,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -134,6 +249,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -167,6 +283,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -200,6 +317,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -233,6 +351,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -266,6 +385,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -299,6 +419,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -332,6 +453,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -364,6 +486,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -396,6 +519,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -428,6 +552,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -463,6 +588,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -506,6 +632,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -552,6 +679,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -591,6 +719,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -632,6 +761,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -674,6 +804,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -708,6 +839,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -743,6 +875,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -786,6 +919,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -832,6 +966,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -873,6 +1008,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -910,6 +1046,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -949,6 +1086,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -990,6 +1128,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -1022,6 +1161,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -1060,6 +1200,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -1106,6 +1247,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -1150,6 +1292,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_data_source": { Block: &tfprotov6.SchemaBlock{ @@ -1197,6 +1340,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource_1": { @@ -1240,6 +1384,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1273,6 +1418,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1306,6 +1452,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1339,6 +1486,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1372,6 +1520,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1405,6 +1554,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1438,6 +1588,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1470,6 +1621,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1502,6 +1654,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1534,6 +1687,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1566,6 +1720,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1601,6 +1756,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1644,6 +1800,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1690,6 +1847,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1729,6 +1887,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1770,6 +1929,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1812,6 +1972,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1846,6 +2007,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1881,6 +2043,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1924,6 +2087,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -1970,6 +2134,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -2011,6 +2176,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -2048,6 +2214,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -2087,6 +2254,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -2128,6 +2296,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -2160,6 +2329,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -2198,6 +2368,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -2244,6 +2415,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -2288,6 +2460,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": { @@ -2327,6 +2500,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ @@ -2357,6 +2531,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ @@ -2382,6 +2557,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ @@ -2411,6 +2587,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ @@ -2444,6 +2621,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ @@ -2468,6 +2646,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ @@ -2493,6 +2672,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ @@ -2530,6 +2710,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2574,6 +2755,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2607,6 +2789,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2639,6 +2822,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2671,6 +2855,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2702,6 +2887,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2732,6 +2918,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2761,6 +2948,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2791,6 +2979,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2821,6 +3010,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2851,6 +3041,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2880,6 +3071,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2909,6 +3101,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2938,6 +3131,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -2970,6 +3164,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3010,6 +3205,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3053,6 +3249,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3089,6 +3286,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3127,6 +3325,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3166,6 +3365,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3197,6 +3397,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3229,6 +3430,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3269,6 +3471,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3312,6 +3515,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3350,6 +3554,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3384,6 +3589,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3420,6 +3626,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3458,6 +3665,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3487,6 +3695,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3522,6 +3731,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3565,6 +3775,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3606,6 +3817,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3643,6 +3855,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3672,6 +3885,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3701,6 +3915,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3730,6 +3945,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3759,6 +3975,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3791,6 +4008,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3831,6 +4049,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3874,6 +4093,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3910,6 +4130,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3948,6 +4169,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -3987,6 +4209,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4018,6 +4241,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4050,6 +4274,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4090,6 +4315,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4133,6 +4359,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4171,6 +4398,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4205,6 +4433,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4241,6 +4470,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4279,6 +4509,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4317,6 +4548,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4360,6 +4592,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4393,6 +4626,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4426,6 +4660,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4459,6 +4694,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4492,6 +4728,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4525,6 +4762,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4559,6 +4797,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4592,6 +4831,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4624,6 +4864,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4656,6 +4897,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4688,6 +4930,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4723,6 +4966,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4766,6 +5010,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4812,6 +5057,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4851,6 +5097,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4892,6 +5139,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4934,6 +5182,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -4968,6 +5217,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5003,6 +5253,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5046,6 +5297,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5092,6 +5344,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5133,6 +5386,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5170,6 +5424,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5209,6 +5464,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5250,6 +5506,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5282,6 +5539,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5320,6 +5578,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5366,6 +5625,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5410,6 +5670,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -5446,6 +5707,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, From 32b20839418a691e4d3025b2914d6924599ec0e2 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 10 Jul 2025 16:55:49 -0400 Subject: [PATCH 07/21] the rest of the tests --- internal/fwserver/server_actions.go | 4 +- .../proto5server/server_getmetadata_test.go | 503 +---------- .../server_getproviderschema_test.go | 821 +++--------------- .../proto6server/server_getmetadata_test.go | 503 +---------- .../server_getproviderschema_test.go | 821 +++--------------- 5 files changed, 338 insertions(+), 2314 deletions(-) diff --git a/internal/fwserver/server_actions.go b/internal/fwserver/server_actions.go index 13d2e381f..4fd25ae87 100644 --- a/internal/fwserver/server_actions.go +++ b/internal/fwserver/server_actions.go @@ -53,9 +53,9 @@ func (s *Server) ActionFuncs(ctx context.Context) (map[string]func() action.Acti return s.actionFuncs, s.actionFuncsDiags } - logging.FrameworkTrace(ctx, "Calling provider defined Provider Actions") + logging.FrameworkTrace(ctx, "Calling provider defined Actions") actionFuncsSlice := provider.Actions(ctx) - logging.FrameworkTrace(ctx, "Called provider defined Provider Actions") + logging.FrameworkTrace(ctx, "Called provider defined Actions") for _, actionFunc := range actionFuncsSlice { actionImpl := actionFunc() diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index 01e0dc43c..6f28652f4 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "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" @@ -28,23 +29,23 @@ func TestServerGetMetadata(t *testing.T) { expectedError error expectedResponse *tfprotov5.GetMetadataResponse }{ - "datasources": { + "actions": { server: &Server{ FrameworkServer: fwserver.Server{ Provider: &testprovider.Provider{ - DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - func() datasource.DataSource { - return &testprovider.DataSource{ - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source1" + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action1" }, } }, - func() datasource.DataSource { - return &testprovider.DataSource{ - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source2" + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action2" }, } }, @@ -55,14 +56,15 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{ + Actions: []tfprotov5.ActionMetadata{ { - TypeName: "test_data_source1", + TypeName: "test_action1", }, { - TypeName: "test_data_source2", + TypeName: "test_action2", }, }, + DataSources: []tfprotov5.DataSourceMetadata{}, EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{}, ListResources: []tfprotov5.ListResourceMetadata{}, @@ -74,7 +76,7 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, - "datasources-duplicate-type-name": { + "datasources": { server: &Server{ FrameworkServer: fwserver.Server{ Provider: &testprovider.Provider{ @@ -83,14 +85,14 @@ func TestServerGetMetadata(t *testing.T) { func() datasource.DataSource { return &testprovider.DataSource{ MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source" + resp.TypeName = "test_data_source1" }, } }, func() datasource.DataSource { return &testprovider.DataSource{ MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source" + resp.TypeName = "test_data_source2" }, } }, @@ -101,54 +103,13 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ + Actions: []tfprotov5.ActionMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{ { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Duplicate Data Source Type Defined", - Detail: "The test_data_source data source type name was returned for multiple data sources. " + - "Data source type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - ListResources: []tfprotov5.ListResourceMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "datasources-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - func() datasource.DataSource { - return &testprovider.DataSource{ - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, + TypeName: "test_data_source1", }, - }, - }, - request: &tfprotov5.GetMetadataRequest{}, - expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Data Source Type Name Missing", - Detail: "The *testprovider.DataSource DataSource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", + TypeName: "test_data_source2", }, }, EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, @@ -189,6 +150,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{}, EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ @@ -208,94 +170,6 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, - "ephemeralresources-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "test_ephemeral_resource" - }, - } - }, - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "test_ephemeral_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetMetadataRequest{}, - expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Duplicate Ephemeral Resource Type Defined", - Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + - "Ephemeral resource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - ListResources: []tfprotov5.ListResourceMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "ephemeralresources-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetMetadataRequest{}, - expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Ephemeral Resource Type Name Missing", - Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - ListResources: []tfprotov5.ListResourceMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, "functions": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -323,6 +197,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{ { @@ -342,94 +217,6 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, - "functions-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.ProviderWithFunctions{ - FunctionsMethod: func(_ context.Context) []func() function.Function { - return []func() function.Function{ - func() function.Function { - return &testprovider.Function{ - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "testfunction" // intentionally duplicate - }, - } - }, - func() function.Function { - return &testprovider.Function{ - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "testfunction" // intentionally duplicate - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetMetadataRequest{}, - expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Duplicate Function Name Defined", - Detail: "The testfunction function name was returned for multiple functions. " + - "Function names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - ListResources: []tfprotov5.ListResourceMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "functions-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.ProviderWithFunctions{ - FunctionsMethod: func(_ context.Context) []func() function.Function { - return []func() function.Function{ - func() function.Function { - return &testprovider.Function{ - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "" // intentionally empty - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetMetadataRequest{}, - expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Function Name Missing", - Detail: "The *testprovider.Function Function returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - ListResources: []tfprotov5.ListResourceMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, "listresources": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -475,6 +262,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{}, EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, @@ -501,156 +289,6 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, - "listresources-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - func() list.ListResource { - return &testprovider.ListResource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - } - }, - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetMetadataRequest{}, - expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Duplicate ListResource Type Defined", - Detail: "The test_list_resource ListResource type name was returned for multiple list resources. " + - "ListResource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - ListResources: []tfprotov5.ListResourceMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "listresources-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetMetadataRequest{}, - expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "ListResource Type Name Missing", - Detail: "The *testprovider.ListResource ListResource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - ListResources: []tfprotov5.ListResourceMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "listresources-missing-resource-definition": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetMetadataRequest{}, - expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "ListResource Type Defined without a Matching Managed Resource Type", - Detail: "The test_list_resource ListResource type name was returned, but no matching managed Resource type was defined. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - ListResources: []tfprotov5.ListResourceMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, "resources": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -678,6 +316,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ + Actions: []tfprotov5.ActionMetadata{}, DataSources: []tfprotov5.DataSourceMetadata{}, EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{}, @@ -697,94 +336,6 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, - "resources-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_resource" - }, - } - }, - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetMetadataRequest{}, - expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Duplicate Resource Type Defined", - Detail: "The test_resource resource type name was returned for multiple resources. " + - "Resource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - ListResources: []tfprotov5.ListResourceMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "resources-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetMetadataRequest{}, - expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Resource Type Name Missing", - Detail: "The *testprovider.Resource Resource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - ListResources: []tfprotov5.ListResourceMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, } for name, testCase := range testCases { diff --git a/internal/proto5server/server_getproviderschema_test.go b/internal/proto5server/server_getproviderschema_test.go index 90e31094e..409e06e9e 100644 --- a/internal/proto5server/server_getproviderschema_test.go +++ b/internal/proto5server/server_getproviderschema_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/ephemeral" @@ -38,41 +40,41 @@ func TestServerGetProviderSchema(t *testing.T) { expectedError error expectedResponse *tfprotov5.GetProviderSchemaResponse }{ - "datasourceschemas": { + "actionschemas": { server: &Server{ FrameworkServer: fwserver.Server{ Provider: &testprovider.Provider{ - DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - func() datasource.DataSource { - return &testprovider.DataSource{ - SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test1": datasourceschema.StringAttribute{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test1": actionschema.StringAttribute{ Required: true, }, }, } }, - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source1" + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action1" }, } }, - func() datasource.DataSource { - return &testprovider.DataSource{ - SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test2": datasourceschema.StringAttribute{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test2": actionschema.StringAttribute{ Required: true, }, }, } }, - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source2" + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action2" }, } }, @@ -83,30 +85,37 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source1": { - Block: &tfprotov5.SchemaBlock{ - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "test1", - Required: true, - Type: tftypes.String, + ActionSchemas: map[string]*tfprotov5.ActionSchema{ + "test_action1": { + Type: tfprotov5.UnlinkedActionSchemaType{}, + Schema: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test1", + Required: true, + Type: tftypes.String, + }, }, }, }, }, - "test_data_source2": { - Block: &tfprotov5.SchemaBlock{ - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "test2", - Required: true, - Type: tftypes.String, + "test_action2": { + Type: tfprotov5.UnlinkedActionSchemaType{}, + Schema: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, + }, }, }, }, }, }, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, ListResourceSchemas: map[string]*tfprotov5.Schema{}, @@ -121,7 +130,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - "datasourceschemas-duplicate-type-name": { + "datasourceschemas": { server: &Server{ FrameworkServer: fwserver.Server{ Provider: &testprovider.Provider{ @@ -139,7 +148,7 @@ func TestServerGetProviderSchema(t *testing.T) { } }, MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source" + resp.TypeName = "test_data_source1" }, } }, @@ -155,7 +164,7 @@ func TestServerGetProviderSchema(t *testing.T) { } }, MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source" + resp.TypeName = "test_data_source2" }, } }, @@ -166,57 +175,29 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Duplicate Data Source Type Defined", - Detail: "The test_data_source data source type name was returned for multiple data sources. " + - "Data source type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{}, - Provider: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov5.Schema{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "datasourceschemas-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - func() datasource.DataSource { - return &testprovider.DataSource{ - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "" - }, - } + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source1": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test1", + Required: true, + Type: tftypes.String, }, - } + }, }, }, - }, - }, - request: &tfprotov5.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Data Source Type Name Missing", - Detail: "The *testprovider.DataSource DataSource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", + "test_data_source2": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, + }, + }, + }, }, }, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, @@ -278,6 +259,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource1": { @@ -316,118 +298,6 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - "ephemeralschemas-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { - resp.Schema = ephemeralschema.Schema{ - Attributes: map[string]ephemeralschema.Attribute{ - "test1": ephemeralschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "test_ephemeral_resource" - }, - } - }, - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { - resp.Schema = ephemeralschema.Schema{ - Attributes: map[string]ephemeralschema.Attribute{ - "test2": ephemeralschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "test_ephemeral_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Duplicate Ephemeral Resource Type Defined", - Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + - "Ephemeral resource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - Functions: map[string]*tfprotov5.Function{}, - EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{}, - Provider: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov5.Schema{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "ephemeralschemas-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Ephemeral Resource Type Name Missing", - Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{}, - Provider: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov5.Schema{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, "functions": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -465,6 +335,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ @@ -493,110 +364,6 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - "functions-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.ProviderWithFunctions{ - FunctionsMethod: func(_ context.Context) []func() function.Function { - return []func() function.Function{ - func() function.Function { - return &testprovider.Function{ - DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { - resp.Definition = function.Definition{ - Return: function.StringReturn{}, - } - }, - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "testfunction" // intentionally duplicate - }, - } - }, - func() function.Function { - return &testprovider.Function{ - DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { - resp.Definition = function.Definition{ - Return: function.StringReturn{}, - } - }, - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "testfunction" // intentionally duplicate - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Duplicate Function Name Defined", - Detail: "The testfunction function name was returned for multiple functions. " + - "Function names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{}, - Provider: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov5.Schema{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "functions-empty-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.ProviderWithFunctions{ - FunctionsMethod: func(_ context.Context) []func() function.Function { - return []func() function.Function{ - func() function.Function { - return &testprovider.Function{ - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "" // intentionally empty - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Function Name Missing", - Detail: "The *testprovider.Function Function returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{}, - Provider: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov5.Schema{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, "listschemas": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -615,149 +382,7 @@ func TestServerGetProviderSchema(t *testing.T) { } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource1" - }, - } - }, - func() list.ListResource { - return &testprovider.ListResource{ - ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { - resp.Schema = listschema.Schema{ - Attributes: map[string]listschema.Attribute{ - "test2": listschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource2" - }, - } - }, - } - }, - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = resourceschema.Schema{ - Attributes: map[string]resourceschema.Attribute{ - "test1": resourceschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource1" - }, - } - }, - func() resource.Resource { - return &testprovider.Resource{ - SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = resourceschema.Schema{ - Attributes: map[string]resourceschema.Attribute{ - "test2": resourceschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource2" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{ - "test_list_resource1": { - Block: &tfprotov5.SchemaBlock{ - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "test1", - Required: true, - Type: tftypes.String, - }, - }, - }, - }, - "test_list_resource2": { - Block: &tfprotov5.SchemaBlock{ - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "test2", - Required: true, - Type: tftypes.String, - }, - }, - }, - }, - }, - Provider: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov5.Schema{ - "test_list_resource1": { - Block: &tfprotov5.SchemaBlock{ - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "test1", - Required: true, - Type: tftypes.String, - }, - }, - }, - }, - "test_list_resource2": { - Block: &tfprotov5.SchemaBlock{ - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "test2", - Required: true, - Type: tftypes.String, - }, - }, - }, - }, - }, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "listschemas-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { - resp.Schema = listschema.Schema{ - Attributes: map[string]listschema.Attribute{ - "test1": listschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" + resp.TypeName = "test_list_resource1" }, } }, @@ -773,7 +398,7 @@ func TestServerGetProviderSchema(t *testing.T) { } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" + resp.TypeName = "test_list_resource2" }, } }, @@ -793,83 +418,23 @@ func TestServerGetProviderSchema(t *testing.T) { } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Duplicate ListResource Type Defined", - Detail: "The test_list_resource ListResource type name was returned for multiple list resources. " + - "ListResource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - Functions: map[string]*tfprotov5.Function{}, - EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{}, - Provider: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov5.Schema{ - "test_list_resource": { - Block: &tfprotov5.SchemaBlock{ - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "test1", - Required: true, - Type: tftypes.String, - }, - }, - }, - }, - }, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "listschemas-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "" + resp.TypeName = "test_list_resource1" }, } }, - } - }, - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = resourceschema.Schema{ Attributes: map[string]resourceschema.Attribute{ - "test1": resourceschema.StringAttribute{ + "test2": resourceschema.StringAttribute{ Required: true, }, }, } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" + resp.TypeName = "test_list_resource2" }, } }, @@ -880,23 +445,39 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "ListResource Type Name Missing", - Detail: "The *testprovider.ListResource ListResource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{}, + ListResourceSchemas: map[string]*tfprotov5.Schema{ + "test_list_resource1": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test1", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + "test_list_resource2": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + }, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, ResourceSchemas: map[string]*tfprotov5.Schema{ - "test_list_resource": { + "test_list_resource1": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -907,59 +488,18 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - }, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "listschemas-missing-resource-definition": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { - resp.Schema = listschema.Schema{ - Attributes: map[string]listschema.Attribute{ - "test1": listschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } + "test_list_resource2": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, }, - } + }, }, }, }, - }, - request: &tfprotov5.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "ListResource Type Defined without a Matching Managed Resource Type", - Detail: "The test_list_resource ListResource type name was returned, but no matching managed Resource type was defined. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{}, - Provider: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -985,6 +525,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -1027,6 +568,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -1098,6 +640,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov5.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov5.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{}, @@ -1136,118 +679,6 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - "resourceschemas-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = resourceschema.Schema{ - Attributes: map[string]resourceschema.Attribute{ - "test1": resourceschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_resource" - }, - } - }, - func() resource.Resource { - return &testprovider.Resource{ - SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = resourceschema.Schema{ - Attributes: map[string]resourceschema.Attribute{ - "test2": resourceschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Duplicate Resource Type Defined", - Detail: "The test_resource resource type name was returned for multiple resources. " + - "Resource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{}, - Provider: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov5.Schema{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "resourceschemas-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov5.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Resource Type Name Missing", - Detail: "The *testprovider.Resource Resource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, - ListResourceSchemas: map[string]*tfprotov5.Schema{}, - Provider: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov5.Schema{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, } for name, testCase := range testCases { @@ -1434,6 +865,36 @@ func TestServerGetProviderSchema_logging(t *testing.T) { "@message": string("Called provider defined ListResources"), "@module": string("sdk.framework"), }, + { + "@level": string("trace"), + "@message": string("Checking ActionFuncs lock"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Checking ProviderTypeName lock"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Calling provider defined Provider Metadata"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Called provider defined Provider Metadata"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Calling provider defined Actions"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Called provider defined Actions"), + "@module": string("sdk.framework"), + }, } if diff := cmp.Diff(entries, expectedEntries); diff != "" { diff --git a/internal/proto6server/server_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go index 36c0f39d0..090091f27 100644 --- a/internal/proto6server/server_getmetadata_test.go +++ b/internal/proto6server/server_getmetadata_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "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" @@ -28,23 +29,23 @@ func TestServerGetMetadata(t *testing.T) { expectedError error expectedResponse *tfprotov6.GetMetadataResponse }{ - "datasources": { + "actions": { server: &Server{ FrameworkServer: fwserver.Server{ Provider: &testprovider.Provider{ - DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - func() datasource.DataSource { - return &testprovider.DataSource{ - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source1" + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action1" }, } }, - func() datasource.DataSource { - return &testprovider.DataSource{ - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source2" + func() action.Action { + return &testprovider.Action{ + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action2" }, } }, @@ -55,14 +56,15 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{ + Actions: []tfprotov6.ActionMetadata{ { - TypeName: "test_data_source1", + TypeName: "test_action1", }, { - TypeName: "test_data_source2", + TypeName: "test_action2", }, }, + DataSources: []tfprotov6.DataSourceMetadata{}, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{}, ListResources: []tfprotov6.ListResourceMetadata{}, @@ -74,7 +76,7 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, - "datasources-duplicate-type-name": { + "datasources": { server: &Server{ FrameworkServer: fwserver.Server{ Provider: &testprovider.Provider{ @@ -83,14 +85,14 @@ func TestServerGetMetadata(t *testing.T) { func() datasource.DataSource { return &testprovider.DataSource{ MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source" + resp.TypeName = "test_data_source1" }, } }, func() datasource.DataSource { return &testprovider.DataSource{ MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source" + resp.TypeName = "test_data_source2" }, } }, @@ -101,54 +103,13 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ + Actions: []tfprotov6.ActionMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{ { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Duplicate Data Source Type Defined", - Detail: "The test_data_source data source type name was returned for multiple data sources. " + - "Data source type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - ListResources: []tfprotov6.ListResourceMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "datasources-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - func() datasource.DataSource { - return &testprovider.DataSource{ - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, + TypeName: "test_data_source1", }, - }, - }, - request: &tfprotov6.GetMetadataRequest{}, - expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Data Source Type Name Missing", - Detail: "The *testprovider.DataSource DataSource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", + TypeName: "test_data_source2", }, }, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, @@ -189,6 +150,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{}, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ { @@ -208,94 +170,6 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, - "ephemeralresources-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "test_ephemeral_resource" - }, - } - }, - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "test_ephemeral_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetMetadataRequest{}, - expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Duplicate Ephemeral Resource Type Defined", - Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + - "Ephemeral resource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - ListResources: []tfprotov6.ListResourceMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "ephemeralresources-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetMetadataRequest{}, - expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Ephemeral Resource Type Name Missing", - Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - ListResources: []tfprotov6.ListResourceMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, "functions": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -323,6 +197,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{}, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{ @@ -342,94 +217,6 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, - "functions-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.ProviderWithFunctions{ - FunctionsMethod: func(_ context.Context) []func() function.Function { - return []func() function.Function{ - func() function.Function { - return &testprovider.Function{ - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "testfunction" // intentionally duplicate - }, - } - }, - func() function.Function { - return &testprovider.Function{ - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "testfunction" // intentionally duplicate - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetMetadataRequest{}, - expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Duplicate Function Name Defined", - Detail: "The testfunction function name was returned for multiple functions. " + - "Function names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - ListResources: []tfprotov6.ListResourceMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "functions-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.ProviderWithFunctions{ - FunctionsMethod: func(_ context.Context) []func() function.Function { - return []func() function.Function{ - func() function.Function { - return &testprovider.Function{ - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "" // intentionally empty - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetMetadataRequest{}, - expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Function Name Missing", - Detail: "The *testprovider.Function Function returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - ListResources: []tfprotov6.ListResourceMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, "listresources": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -475,6 +262,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{}, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{}, @@ -501,156 +289,6 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, - "listresources-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - func() list.ListResource { - return &testprovider.ListResource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - } - }, - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetMetadataRequest{}, - expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Duplicate ListResource Type Defined", - Detail: "The test_list_resource ListResource type name was returned for multiple list resources. " + - "ListResource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - ListResources: []tfprotov6.ListResourceMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "listresources-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetMetadataRequest{}, - expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "ListResource Type Name Missing", - Detail: "The *testprovider.ListResource ListResource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - ListResources: []tfprotov6.ListResourceMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "listresources-missing-resource-definition": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetMetadataRequest{}, - expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "ListResource Type Defined without a Matching Managed Resource Type", - Detail: "The test_list_resource ListResource type name was returned, but no matching managed Resource type was defined. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - ListResources: []tfprotov6.ListResourceMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, "resources": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -678,6 +316,7 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ + Actions: []tfprotov6.ActionMetadata{}, DataSources: []tfprotov6.DataSourceMetadata{}, EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{}, @@ -697,94 +336,6 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, - "resources-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_resource" - }, - } - }, - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetMetadataRequest{}, - expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Duplicate Resource Type Defined", - Detail: "The test_resource resource type name was returned for multiple resources. " + - "Resource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - ListResources: []tfprotov6.ListResourceMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "resources-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetMetadataRequest{}, - expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Resource Type Name Missing", - Detail: "The *testprovider.Resource Resource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - ListResources: []tfprotov6.ListResourceMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, } for name, testCase := range testCases { diff --git a/internal/proto6server/server_getproviderschema_test.go b/internal/proto6server/server_getproviderschema_test.go index a1010f555..e93e75b70 100644 --- a/internal/proto6server/server_getproviderschema_test.go +++ b/internal/proto6server/server_getproviderschema_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/ephemeral" @@ -38,41 +40,41 @@ func TestServerGetProviderSchema(t *testing.T) { expectedError error expectedResponse *tfprotov6.GetProviderSchemaResponse }{ - "datasourceschemas": { + "actionschemas": { server: &Server{ FrameworkServer: fwserver.Server{ Provider: &testprovider.Provider{ - DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - func() datasource.DataSource { - return &testprovider.DataSource{ - SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test1": datasourceschema.StringAttribute{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test1": actionschema.StringAttribute{ Required: true, }, }, } }, - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source1" + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action1" }, } }, - func() datasource.DataSource { - return &testprovider.DataSource{ - SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test2": datasourceschema.StringAttribute{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = actionschema.UnlinkedSchema{ + Attributes: map[string]actionschema.Attribute{ + "test2": actionschema.StringAttribute{ Required: true, }, }, } }, - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source2" + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action2" }, } }, @@ -83,30 +85,37 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source1": { - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "test1", - Required: true, - Type: tftypes.String, + ActionSchemas: map[string]*tfprotov6.ActionSchema{ + "test_action1": { + Type: tfprotov6.UnlinkedActionSchemaType{}, + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test1", + Required: true, + Type: tftypes.String, + }, }, }, }, }, - "test_data_source2": { - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "test2", - Required: true, - Type: tftypes.String, + "test_action2": { + Type: tfprotov6.UnlinkedActionSchemaType{}, + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, + }, }, }, }, }, }, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, ListResourceSchemas: map[string]*tfprotov6.Schema{}, @@ -121,7 +130,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - "datasourceschemas-duplicate-type-name": { + "datasourceschemas": { server: &Server{ FrameworkServer: fwserver.Server{ Provider: &testprovider.Provider{ @@ -139,7 +148,7 @@ func TestServerGetProviderSchema(t *testing.T) { } }, MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source" + resp.TypeName = "test_data_source1" }, } }, @@ -155,7 +164,7 @@ func TestServerGetProviderSchema(t *testing.T) { } }, MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "test_data_source" + resp.TypeName = "test_data_source2" }, } }, @@ -166,57 +175,29 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Duplicate Data Source Type Defined", - Detail: "The test_data_source data source type name was returned for multiple data sources. " + - "Data source type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{}, - Provider: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov6.Schema{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "datasourceschemas-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - func() datasource.DataSource { - return &testprovider.DataSource{ - MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = "" - }, - } + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source1": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test1", + Required: true, + Type: tftypes.String, }, - } + }, }, }, - }, - }, - request: &tfprotov6.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Data Source Type Name Missing", - Detail: "The *testprovider.DataSource DataSource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", + "test_data_source2": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, + }, + }, + }, }, }, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, @@ -278,6 +259,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource1": { @@ -316,118 +298,6 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - "ephemeralschemas-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { - resp.Schema = ephemeralschema.Schema{ - Attributes: map[string]ephemeralschema.Attribute{ - "test1": ephemeralschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "test_ephemeral_resource" - }, - } - }, - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { - resp.Schema = ephemeralschema.Schema{ - Attributes: map[string]ephemeralschema.Attribute{ - "test2": ephemeralschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "test_ephemeral_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Duplicate Ephemeral Resource Type Defined", - Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + - "Ephemeral resource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{}, - Provider: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov6.Schema{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "ephemeralschemas-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - func() ephemeral.EphemeralResource { - return &testprovider.EphemeralResource{ - MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Ephemeral Resource Type Name Missing", - Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{}, - Provider: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov6.Schema{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, "functions": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -465,6 +335,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ @@ -493,110 +364,6 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - "functions-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.ProviderWithFunctions{ - FunctionsMethod: func(_ context.Context) []func() function.Function { - return []func() function.Function{ - func() function.Function { - return &testprovider.Function{ - DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { - resp.Definition = function.Definition{ - Return: function.StringReturn{}, - } - }, - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "testfunction" // intentionally duplicate - }, - } - }, - func() function.Function { - return &testprovider.Function{ - DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { - resp.Definition = function.Definition{ - Return: function.StringReturn{}, - } - }, - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "testfunction" // intentionally duplicate - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Duplicate Function Name Defined", - Detail: "The testfunction function name was returned for multiple functions. " + - "Function names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{}, - Provider: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov6.Schema{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "functions-empty-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.ProviderWithFunctions{ - FunctionsMethod: func(_ context.Context) []func() function.Function { - return []func() function.Function{ - func() function.Function { - return &testprovider.Function{ - MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { - resp.Name = "" // intentionally empty - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Function Name Missing", - Detail: "The *testprovider.Function Function returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{}, - Provider: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov6.Schema{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, "listschemas": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -615,149 +382,7 @@ func TestServerGetProviderSchema(t *testing.T) { } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource1" - }, - } - }, - func() list.ListResource { - return &testprovider.ListResource{ - ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { - resp.Schema = listschema.Schema{ - Attributes: map[string]listschema.Attribute{ - "test2": listschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource2" - }, - } - }, - } - }, - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = resourceschema.Schema{ - Attributes: map[string]resourceschema.Attribute{ - "test1": resourceschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource1" - }, - } - }, - func() resource.Resource { - return &testprovider.Resource{ - SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = resourceschema.Schema{ - Attributes: map[string]resourceschema.Attribute{ - "test2": resourceschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource2" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{ - "test_list_resource1": { - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "test1", - Required: true, - Type: tftypes.String, - }, - }, - }, - }, - "test_list_resource2": { - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "test2", - Required: true, - Type: tftypes.String, - }, - }, - }, - }, - }, - Provider: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov6.Schema{ - "test_list_resource1": { - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "test1", - Required: true, - Type: tftypes.String, - }, - }, - }, - }, - "test_list_resource2": { - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "test2", - Required: true, - Type: tftypes.String, - }, - }, - }, - }, - }, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "listschemas-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { - resp.Schema = listschema.Schema{ - Attributes: map[string]listschema.Attribute{ - "test1": listschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" + resp.TypeName = "test_list_resource1" }, } }, @@ -773,7 +398,7 @@ func TestServerGetProviderSchema(t *testing.T) { } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" + resp.TypeName = "test_list_resource2" }, } }, @@ -793,83 +418,23 @@ func TestServerGetProviderSchema(t *testing.T) { } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Duplicate ListResource Type Defined", - Detail: "The test_list_resource ListResource type name was returned for multiple list resources. " + - "ListResource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{}, - Provider: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov6.Schema{ - "test_list_resource": { - Block: &tfprotov6.SchemaBlock{ - Attributes: []*tfprotov6.SchemaAttribute{ - { - Name: "test1", - Required: true, - Type: tftypes.String, - }, - }, - }, - }, - }, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "listschemas-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "" + resp.TypeName = "test_list_resource1" }, } }, - } - }, - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ func() resource.Resource { return &testprovider.Resource{ SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = resourceschema.Schema{ Attributes: map[string]resourceschema.Attribute{ - "test1": resourceschema.StringAttribute{ + "test2": resourceschema.StringAttribute{ Required: true, }, }, } }, MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" + resp.TypeName = "test_list_resource2" }, } }, @@ -880,23 +445,39 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "ListResource Type Name Missing", - Detail: "The *testprovider.ListResource ListResource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{}, + ListResourceSchemas: map[string]*tfprotov6.Schema{ + "test_list_resource1": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test1", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + "test_list_resource2": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + }, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, ResourceSchemas: map[string]*tfprotov6.Schema{ - "test_list_resource": { + "test_list_resource1": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -907,59 +488,18 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - }, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "listschemas-missing-resource-definition": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ListResourcesMethod: func(_ context.Context) []func() list.ListResource { - return []func() list.ListResource{ - func() list.ListResource { - return &testprovider.ListResource{ - ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { - resp.Schema = listschema.Schema{ - Attributes: map[string]listschema.Attribute{ - "test1": listschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_list_resource" - }, - } + "test_list_resource2": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, }, - } + }, }, }, }, - }, - request: &tfprotov6.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "ListResource Type Defined without a Matching Managed Resource Type", - Detail: "The test_list_resource ListResource type name was returned, but no matching managed Resource type was defined. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{}, - Provider: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -985,6 +525,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -1027,6 +568,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -1098,6 +640,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + ActionSchemas: map[string]*tfprotov6.ActionSchema{}, DataSourceSchemas: map[string]*tfprotov6.Schema{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{}, @@ -1136,118 +679,6 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - "resourceschemas-duplicate-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = resourceschema.Schema{ - Attributes: map[string]resourceschema.Attribute{ - "test1": resourceschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_resource" - }, - } - }, - func() resource.Resource { - return &testprovider.Resource{ - SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = resourceschema.Schema{ - Attributes: map[string]resourceschema.Attribute{ - "test2": resourceschema.StringAttribute{ - Required: true, - }, - }, - } - }, - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "test_resource" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Duplicate Resource Type Defined", - Detail: "The test_resource resource type name was returned for multiple resources. " + - "Resource type names must be unique. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{}, - Provider: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov6.Schema{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, - "resourceschemas-empty-type-name": { - server: &Server{ - FrameworkServer: fwserver.Server{ - Provider: &testprovider.Provider{ - ResourcesMethod: func(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{ - func() resource.Resource { - return &testprovider.Resource{ - MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = "" - }, - } - }, - } - }, - }, - }, - }, - request: &tfprotov6.GetProviderSchemaRequest{}, - expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Resource Type Name Missing", - Detail: "The *testprovider.Resource Resource returned an empty string from the Metadata method. " + - "This is always an issue with the provider and should be reported to the provider developers.", - }, - }, - EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, - ListResourceSchemas: map[string]*tfprotov6.Schema{}, - Provider: &tfprotov6.Schema{ - Block: &tfprotov6.SchemaBlock{}, - }, - ResourceSchemas: map[string]*tfprotov6.Schema{}, - ServerCapabilities: &tfprotov6.ServerCapabilities{ - GetProviderSchemaOptional: true, - MoveResourceState: true, - PlanDestroy: true, - }, - }, - }, } for name, testCase := range testCases { @@ -1434,6 +865,36 @@ func TestServerGetProviderSchema_logging(t *testing.T) { "@message": string("Called provider defined ListResources"), "@module": string("sdk.framework"), }, + { + "@level": string("trace"), + "@message": string("Checking ActionFuncs lock"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Checking ProviderTypeName lock"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Calling provider defined Provider Metadata"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Called provider defined Provider Metadata"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Calling provider defined Actions"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Called provider defined Actions"), + "@module": string("sdk.framework"), + }, } if diff := cmp.Diff(entries, expectedEntries); diff != "" { From 3a1048c60d18617eee24a25689d6268e523a87c8 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 11 Jul 2025 09:36:44 -0400 Subject: [PATCH 08/21] fix double import --- internal/toproto5/action_schema_test.go | 3 +-- internal/toproto6/action_schema_test.go | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/toproto5/action_schema_test.go b/internal/toproto5/action_schema_test.go index 717d19162..39b7e7c7f 100644 --- a/internal/toproto5/action_schema_test.go +++ b/internal/toproto5/action_schema_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/action/schema" actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-go/tfprotov5" @@ -40,7 +39,7 @@ func TestActionSchema(t *testing.T) { }, }, Blocks: map[string]actionschema.Block{ - "single_block": schema.SingleNestedBlock{ + "single_block": actionschema.SingleNestedBlock{ Attributes: map[string]actionschema.Attribute{ "bool": actionschema.BoolAttribute{ Required: true, diff --git a/internal/toproto6/action_schema_test.go b/internal/toproto6/action_schema_test.go index 20de2d0f6..ab145a2d7 100644 --- a/internal/toproto6/action_schema_test.go +++ b/internal/toproto6/action_schema_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/action/schema" actionschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -40,7 +39,7 @@ func TestActionSchema(t *testing.T) { }, }, Blocks: map[string]actionschema.Block{ - "single_block": schema.SingleNestedBlock{ + "single_block": actionschema.SingleNestedBlock{ Attributes: map[string]actionschema.Attribute{ "bool": actionschema.BoolAttribute{ Required: true, From 7e5fd9f37161b70a451d21d3e9cf2b34e9d5e20d Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 11 Jul 2025 16:04:11 -0400 Subject: [PATCH 09/21] external interfaces for plan / configure --- action/action.go | 29 +++++++++ action/configure.go | 32 ++++++++++ action/deferred.go | 50 +++++++++++++++ action/modify_plan.go | 62 +++++++++++++++++++ internal/fromproto5/client_capabilities.go | 14 +++++ internal/fromproto5/planaction.go | 5 +- internal/fromproto6/client_capabilities.go | 14 +++++ internal/fromproto6/planaction.go | 5 +- internal/fwserver/server.go | 5 ++ internal/fwserver/server_configureprovider.go | 1 + .../fwserver/server_configureprovider_test.go | 14 +++++ internal/fwserver/server_planaction.go | 7 ++- internal/toproto5/deferred.go | 10 +++ internal/toproto5/planaction.go | 3 +- internal/toproto6/deferred.go | 10 +++ internal/toproto6/planaction.go | 3 +- provider/configure.go | 5 ++ 17 files changed, 261 insertions(+), 8 deletions(-) create mode 100644 action/configure.go create mode 100644 action/deferred.go create mode 100644 action/modify_plan.go diff --git a/action/action.go b/action/action.go index f172651f9..ec07a084d 100644 --- a/action/action.go +++ b/action/action.go @@ -14,3 +14,32 @@ type Action interface { // TODO:Actions: Eventual landing place for all required methods to implement for an action } + +// ActionWithConfigure is an interface type that extends Action to +// include a method which the framework will automatically call so provider +// developers have the opportunity to setup any necessary provider-level data +// or clients in the Action type. +type ActionWithConfigure interface { + Action + + // Configure enables provider-level data or clients to be set in the + // provider-defined Action type. + Configure(context.Context, ConfigureRequest, *ConfigureResponse) +} + +// ActionWithModifyPlan represents an action with a ModifyPlan function. +type ActionWithModifyPlan interface { + Action + + // ModifyPlan is called when the provider has an opportunity to modify + // the plan for an action: once during the plan phase, and once + // during the apply phase with any unknown values from configuration + // filled in with their final values. + // + // Actions do not have computed attributes that can be modified during the plan, + // but linked and lifecycle actions can modify the plan of linked resources. + // + // All action schema types can use the plan as an opportunity to raise early + // diagnostics to practitioners, such as validation errors. + ModifyPlan(context.Context, ModifyPlanRequest, *ModifyPlanResponse) +} diff --git a/action/configure.go b/action/configure.go new file mode 100644 index 000000000..f9d8d2440 --- /dev/null +++ b/action/configure.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package action + +import "github.com/hashicorp/terraform-plugin-framework/diag" + +// ConfigureRequest represents a request for the provider to configure an +// action, i.e., set provider-level data or clients. An instance of this +// request struct is supplied as an argument to the Action type Configure +// method. +type ConfigureRequest struct { + // ProviderData is the data set in the + // [provider.ConfigureResponse.ActionData] field. This data is + // provider-specifc and therefore can contain any necessary remote system + // clients, custom provider data, or anything else pertinent to the + // functionality of the Action. + // + // This data is only set after the ConfigureProvider RPC has been called + // by Terraform. + ProviderData any +} + +// ConfigureResponse represents a response to a ConfigureRequest. An +// instance of this response struct is supplied as an argument to the +// Action type Configure method. +type ConfigureResponse struct { + // Diagnostics report errors or warnings related to configuring of the + // Datasource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/action/deferred.go b/action/deferred.go new file mode 100644 index 000000000..fb852d88c --- /dev/null +++ b/action/deferred.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package action + +const ( + // DeferredReasonUnknown is used to indicate an invalid `DeferredReason`. + // Provider developers should not use it. + DeferredReasonUnknown DeferredReason = 0 + + // DeferredReasonActionConfigUnknown is used to indicate that the action configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonActionConfigUnknown DeferredReason = 1 + + // DeferredReasonProviderConfigUnknown is used to indicate that the provider configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonProviderConfigUnknown DeferredReason = 2 + + // DeferredReasonAbsentPrereq is used to indicate that a hard dependency has not been satisfied. + DeferredReasonAbsentPrereq DeferredReason = 3 +) + +// Deferred is used to indicate to Terraform that a change needs to be deferred for a reason. +// +// NOTE: This functionality is related to deferred action support, which is currently experimental and is subject +// to change or break without warning. It is not protected by version compatibility guarantees. +type Deferred struct { + // Reason is the reason for deferring the change. + Reason DeferredReason +} + +// DeferredReason represents different reasons for deferring a change. +// +// NOTE: This functionality is related to deferred action support, which is currently experimental and is subject +// to change or break without warning. It is not protected by version compatibility guarantees. +type DeferredReason int32 + +func (d DeferredReason) String() string { + switch d { + case 0: + return "Unknown" + case 1: + return "Action Config Unknown" + case 2: + return "Provider Config Unknown" + case 3: + return "Absent Prerequisite" + } + return "Unknown" +} diff --git a/action/modify_plan.go b/action/modify_plan.go new file mode 100644 index 000000000..708656f33 --- /dev/null +++ b/action/modify_plan.go @@ -0,0 +1,62 @@ +// 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/tfsdk" +) + +// ModifyPlanClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the PlanAction RPC, +// such as forward-compatible Terraform behavior changes. +type ModifyPlanClientCapabilities struct { + // DeferralAllowed indicates whether the Terraform client initiating + // the request allows a deferral response. + // + // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject + // to change or break without warning. It is not protected by version compatibility guarantees. + DeferralAllowed bool +} + +// ModifyPlanRequest represents a request for the provider to modify the +// planned new state that Terraform has generated for any linked resources. +type ModifyPlanRequest struct { + // Config is the configuration the user supplied for the action. + // + // 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. + Config tfsdk.Config + + // TODO:Actions: Add linked resources once lifecycle/linked actions are implemented + + // ClientCapabilities defines optionally supported protocol features for the + // PlanAction RPC, such as forward-compatible Terraform behavior changes. + ClientCapabilities ModifyPlanClientCapabilities +} + +// ModifyPlanResponse represents a response to a +// ModifyPlanRequest. An instance of this response struct is supplied +// as an argument to the action's ModifyPlan function, in which the provider +// should modify the Plan of any linked resources as appropriate. +type ModifyPlanResponse struct { + // Diagnostics report errors or warnings related to determining the + // planned state of the requested action's linked resources. Returning an empty slice + // indicates a successful plan modification with no warnings or errors + // generated. + Diagnostics diag.Diagnostics + + // TODO:Actions: Add linked resources once lifecycle/linked actions are implemented + + // Deferred indicates that Terraform should defer planning this + // action until a follow-up apply operation. + // + // This field can only be set if + // `(action.ModifyPlanRequest).ClientCapabilities.DeferralAllowed` is true. + // + // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject + // to change or break without warning. It is not protected by version compatibility guarantees. + Deferred *Deferred +} diff --git a/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go index 737354888..89efd44f5 100644 --- a/internal/fromproto5/client_capabilities.go +++ b/internal/fromproto5/client_capabilities.go @@ -6,6 +6,7 @@ package fromproto5 import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "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/provider" @@ -102,3 +103,16 @@ func ValidateResourceTypeConfigClientCapabilities(in *tfprotov5.ValidateResource WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, } } + +func ModifyPlanActionClientCapabilities(in *tfprotov5.PlanActionClientCapabilities) action.ModifyPlanClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return action.ModifyPlanClientCapabilities{ + DeferralAllowed: false, + } + } + + return action.ModifyPlanClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } +} diff --git a/internal/fromproto5/planaction.go b/internal/fromproto5/planaction.go index bb71e5815..25fa10afe 100644 --- a/internal/fromproto5/planaction.go +++ b/internal/fromproto5/planaction.go @@ -37,7 +37,8 @@ func PlanActionRequest(ctx context.Context, proto5 *tfprotov5.PlanActionRequest, } fw := &fwserver.PlanActionRequest{ - ActionSchema: actionSchema, + ActionSchema: actionSchema, + ClientCapabilities: ModifyPlanActionClientCapabilities(proto5.ClientCapabilities), } config, configDiags := Config(ctx, proto5.Config, actionSchema) @@ -46,7 +47,7 @@ func PlanActionRequest(ctx context.Context, proto5 *tfprotov5.PlanActionRequest, fw.Config = config - // TODO:Actions: Here we need to retrieve client capabilities and linked resource data + // TODO:Actions: Here we need to retrieve linked resource data return fw, diags } diff --git a/internal/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go index d22d81623..337b8227c 100644 --- a/internal/fromproto6/client_capabilities.go +++ b/internal/fromproto6/client_capabilities.go @@ -6,6 +6,7 @@ package fromproto6 import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "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/provider" @@ -102,3 +103,16 @@ func ValidateResourceConfigClientCapabilities(in *tfprotov6.ValidateResourceConf WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, } } + +func ModifyPlanActionClientCapabilities(in *tfprotov6.PlanActionClientCapabilities) action.ModifyPlanClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return action.ModifyPlanClientCapabilities{ + DeferralAllowed: false, + } + } + + return action.ModifyPlanClientCapabilities{ + DeferralAllowed: in.DeferralAllowed, + } +} diff --git a/internal/fromproto6/planaction.go b/internal/fromproto6/planaction.go index 7715037c9..352269a54 100644 --- a/internal/fromproto6/planaction.go +++ b/internal/fromproto6/planaction.go @@ -37,7 +37,8 @@ func PlanActionRequest(ctx context.Context, proto6 *tfprotov6.PlanActionRequest, } fw := &fwserver.PlanActionRequest{ - ActionSchema: actionSchema, + ActionSchema: actionSchema, + ClientCapabilities: ModifyPlanActionClientCapabilities(proto6.ClientCapabilities), } config, configDiags := Config(ctx, proto6.Config, actionSchema) @@ -46,7 +47,7 @@ func PlanActionRequest(ctx context.Context, proto6 *tfprotov6.PlanActionRequest, fw.Config = config - // TODO:Actions: Here we need to retrieve client capabilities and linked resource data + // TODO:Actions: Here we need to retrieve linked resource data return fw, diags } diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 22e2c2eb2..ba7c74823 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -42,6 +42,11 @@ type Server struct { // to [ephemeral.ConfigureRequest.ProviderData]. EphemeralResourceConfigureData any + // ActionConfigureData is the + // [provider.ConfigureResponse.ActionData] field value which is passed + // to [action.ConfigureRequest.ProviderData]. + ActionConfigureData 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. diff --git a/internal/fwserver/server_configureprovider.go b/internal/fwserver/server_configureprovider.go index 0b1807bce..d11c022fc 100644 --- a/internal/fwserver/server_configureprovider.go +++ b/internal/fwserver/server_configureprovider.go @@ -38,4 +38,5 @@ func (s *Server) ConfigureProvider(ctx context.Context, req *provider.ConfigureR s.DataSourceConfigureData = resp.DataSourceData s.ResourceConfigureData = resp.ResourceData s.EphemeralResourceConfigureData = resp.EphemeralResourceData + s.ActionConfigureData = resp.ActionData } diff --git a/internal/fwserver/server_configureprovider_test.go b/internal/fwserver/server_configureprovider_test.go index 96d5ac63a..83741e8cc 100644 --- a/internal/fwserver/server_configureprovider_test.go +++ b/internal/fwserver/server_configureprovider_test.go @@ -192,6 +192,20 @@ func TestServerConfigureProvider(t *testing.T) { EphemeralResourceData: "test-provider-configure-value", }, }, + "response-actiondata": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.ActionData = "test-provider-configure-value" + }, + }, + }, + request: &provider.ConfigureRequest{}, + expectedResponse: &provider.ConfigureResponse{ + ActionData: "test-provider-configure-value", + }, + }, "response-invalid-deferral-diagnostic": { server: &fwserver.Server{ Provider: &testprovider.Provider{ diff --git a/internal/fwserver/server_planaction.go b/internal/fwserver/server_planaction.go index 1a5bc192f..a8efe366f 100644 --- a/internal/fwserver/server_planaction.go +++ b/internal/fwserver/server_planaction.go @@ -6,6 +6,7 @@ package fwserver import ( "context" + "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/tfsdk" @@ -13,12 +14,14 @@ import ( // PlanActionRequest is the framework server request for the PlanAction RPC. type PlanActionRequest struct { - ActionSchema fwschema.Schema - Config *tfsdk.Config + ClientCapabilities action.ModifyPlanClientCapabilities + ActionSchema fwschema.Schema + Config *tfsdk.Config } // PlanActionResponse is the framework server response for the PlanAction RPC. type PlanActionResponse struct { + Deferred *action.Deferred Diagnostics diag.Diagnostics } diff --git a/internal/toproto5/deferred.go b/internal/toproto5/deferred.go index 42049a4da..c5f35d0c7 100644 --- a/internal/toproto5/deferred.go +++ b/internal/toproto5/deferred.go @@ -6,6 +6,7 @@ package toproto5 import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "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/resource" @@ -37,3 +38,12 @@ func EphemeralResourceDeferred(fw *ephemeral.Deferred) *tfprotov5.Deferred { Reason: tfprotov5.DeferredReason(fw.Reason), } } + +func ActionDeferred(fw *action.Deferred) *tfprotov5.Deferred { + if fw == nil { + return nil + } + return &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(fw.Reason), + } +} diff --git a/internal/toproto5/planaction.go b/internal/toproto5/planaction.go index b55fd4557..06f12faaf 100644 --- a/internal/toproto5/planaction.go +++ b/internal/toproto5/planaction.go @@ -19,9 +19,10 @@ func PlanActionResponse(ctx context.Context, fw *fwserver.PlanActionResponse) *t proto5 := &tfprotov5.PlanActionResponse{ Diagnostics: Diagnostics(ctx, fw.Diagnostics), + Deferred: ActionDeferred(fw.Deferred), } - // TODO:Actions: Here we need to set deferred and linked resource data + // TODO:Actions: Here we need to set linked resource data return proto5 } diff --git a/internal/toproto6/deferred.go b/internal/toproto6/deferred.go index fab64fe05..362c843bb 100644 --- a/internal/toproto6/deferred.go +++ b/internal/toproto6/deferred.go @@ -6,6 +6,7 @@ package toproto6 import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "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/resource" @@ -37,3 +38,12 @@ func EphemeralResourceDeferred(fw *ephemeral.Deferred) *tfprotov6.Deferred { Reason: tfprotov6.DeferredReason(fw.Reason), } } + +func ActionDeferred(fw *action.Deferred) *tfprotov6.Deferred { + if fw == nil { + return nil + } + return &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReason(fw.Reason), + } +} diff --git a/internal/toproto6/planaction.go b/internal/toproto6/planaction.go index 9d4f86ac8..6411005f4 100644 --- a/internal/toproto6/planaction.go +++ b/internal/toproto6/planaction.go @@ -19,9 +19,10 @@ func PlanActionResponse(ctx context.Context, fw *fwserver.PlanActionResponse) *t proto6 := &tfprotov6.PlanActionResponse{ Diagnostics: Diagnostics(ctx, fw.Diagnostics), + Deferred: ActionDeferred(fw.Deferred), } - // TODO:Actions: Here we need to set deferred and linked resource data + // TODO:Actions: Here we need to set linked resource data return proto6 } diff --git a/provider/configure.go b/provider/configure.go index 59e9ead44..49e2f5203 100644 --- a/provider/configure.go +++ b/provider/configure.go @@ -67,6 +67,11 @@ type ConfigureResponse struct { // EphemeralResource type that implements the Configure method. EphemeralResourceData any + // ActionData is provider-defined data, clients, etc. that is + // passed to [action.ConfigureRequest.ProviderData] for each + // Action type that implements the Configure method. + ActionData any + // Deferred indicates that Terraform should automatically defer // all resources and data sources for this provider. // From 95e76735bd6cfa0bd2e0c2bb3c47eef2c44ec6e4 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 14 Jul 2025 07:12:12 -0400 Subject: [PATCH 10/21] plan action impl and fwserver tests --- internal/fromproto5/planaction.go | 1 + internal/fromproto6/planaction.go | 1 + internal/fwserver/server_planaction.go | 73 ++++- internal/fwserver/server_planaction_test.go | 258 +++++++++++++++++- .../actionwithconfigureandmodifyplan.go | 43 +++ .../testprovider/actionwithmodifyplan.go | 30 ++ 6 files changed, 400 insertions(+), 6 deletions(-) create mode 100644 internal/testing/testprovider/actionwithconfigureandmodifyplan.go create mode 100644 internal/testing/testprovider/actionwithmodifyplan.go diff --git a/internal/fromproto5/planaction.go b/internal/fromproto5/planaction.go index 25fa10afe..b01b0d410 100644 --- a/internal/fromproto5/planaction.go +++ b/internal/fromproto5/planaction.go @@ -37,6 +37,7 @@ func PlanActionRequest(ctx context.Context, proto5 *tfprotov5.PlanActionRequest, } fw := &fwserver.PlanActionRequest{ + Action: reqAction, ActionSchema: actionSchema, ClientCapabilities: ModifyPlanActionClientCapabilities(proto5.ClientCapabilities), } diff --git a/internal/fromproto6/planaction.go b/internal/fromproto6/planaction.go index 352269a54..838698a87 100644 --- a/internal/fromproto6/planaction.go +++ b/internal/fromproto6/planaction.go @@ -37,6 +37,7 @@ func PlanActionRequest(ctx context.Context, proto6 *tfprotov6.PlanActionRequest, } fw := &fwserver.PlanActionRequest{ + Action: reqAction, ActionSchema: actionSchema, ClientCapabilities: ModifyPlanActionClientCapabilities(proto6.ClientCapabilities), } diff --git a/internal/fwserver/server_planaction.go b/internal/fwserver/server_planaction.go index a8efe366f..4ed956e20 100644 --- a/internal/fwserver/server_planaction.go +++ b/internal/fwserver/server_planaction.go @@ -9,13 +9,16 @@ import ( "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/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) // PlanActionRequest is the framework server request for the PlanAction RPC. type PlanActionRequest struct { ClientCapabilities action.ModifyPlanClientCapabilities ActionSchema fwschema.Schema + Action action.Action Config *tfsdk.Config } @@ -27,9 +30,69 @@ type PlanActionResponse struct { // 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.", - ) + if req == nil { + return + } + + // TODO:Actions: When linked resources are introduced, pass-through proposed -> planned state similar to + // how normal resource planning works. + + if s.deferred != nil { + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.deferred.Reason.String(), + }, + ) + + resp.Deferred = &action.Deferred{ + Reason: action.DeferredReason(s.deferred.Reason), + } + return + } + + if actionWithConfigure, ok := req.Action.(action.ActionWithConfigure); ok { + logging.FrameworkTrace(ctx, "Action implements ActionWithConfigure") + + configureReq := action.ConfigureRequest{ + ProviderData: s.ActionConfigureData, + } + configureResp := action.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Action Configure") + actionWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined Action Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + if req.Config == nil { + req.Config = &tfsdk.Config{ + Raw: tftypes.NewValue(req.ActionSchema.Type().TerraformType(ctx), nil), + Schema: req.ActionSchema, + } + } + + if actionWithModifyPlan, ok := req.Action.(action.ActionWithModifyPlan); ok { + logging.FrameworkTrace(ctx, "Action implements ActionWithModifyPlan") + + modifyPlanReq := action.ModifyPlanRequest{ + ClientCapabilities: req.ClientCapabilities, + Config: *req.Config, + } + + modifyPlanResp := action.ModifyPlanResponse{ + Diagnostics: resp.Diagnostics, + } + + logging.FrameworkTrace(ctx, "Calling provider defined Action ModifyPlan") + actionWithModifyPlan.ModifyPlan(ctx, modifyPlanReq, &modifyPlanResp) + logging.FrameworkTrace(ctx, "Called provider defined Action ModifyPlan") + + resp.Diagnostics = modifyPlanResp.Diagnostics + resp.Deferred = modifyPlanResp.Deferred + } } diff --git a/internal/fwserver/server_planaction_test.go b/internal/fwserver/server_planaction_test.go index 1ecb5c4f3..d922eb844 100644 --- a/internal/fwserver/server_planaction_test.go +++ b/internal/fwserver/server_planaction_test.go @@ -3,4 +3,260 @@ package fwserver_test -// TODO:Actions: Add unit tests once PlanAction is implemented +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerPlanAction(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + }, + } + + testConfigValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testUnlinkedSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testUnlinkedConfig := &tfsdk.Config{ + Raw: testConfigValue, + Schema: testUnlinkedSchema, + } + + testDeferralAllowed := action.ModifyPlanClientCapabilities{ + DeferralAllowed: true, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.PlanActionRequest + expectedResponse *fwserver.PlanActionResponse + configureProviderReq *provider.ConfigureRequest + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.PlanActionResponse{}, + }, + "unlinked-nil-config-no-modifyplan": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanActionRequest{ + ActionSchema: testUnlinkedSchema, + Action: &testprovider.Action{}, + }, + expectedResponse: &fwserver.PlanActionResponse{}, + }, + "request-client-capabilities-deferral-allowed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanActionRequest{ + ClientCapabilities: testDeferralAllowed, + Config: testUnlinkedConfig, + ActionSchema: testUnlinkedSchema, + Action: &testprovider.ActionWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + if req.ClientCapabilities.DeferralAllowed != true { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: true but got: false") + } + + var config struct { + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + }, + }, + }, + expectedResponse: &fwserver.PlanActionResponse{}, + }, + "request-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanActionRequest{ + Config: testUnlinkedConfig, + ActionSchema: testUnlinkedSchema, + Action: &testprovider.ActionWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + var config struct { + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.PlanActionResponse{}, + }, + "action-configure-data": { + server: &fwserver.Server{ + ActionConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanActionRequest{ + Config: testUnlinkedConfig, + ActionSchema: testUnlinkedSchema, + Action: &testprovider.ActionWithConfigureAndModifyPlan{ + ConfigureMethod: func(ctx context.Context, req action.ConfigureRequest, resp *action.ConfigureResponse) { + providerData, ok := req.ProviderData.(string) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected string, got: %T", req.ProviderData), + ) + return + } + + if providerData != "test-provider-configure-value" { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected test-provider-configure-value, got: %q", providerData), + ) + } + }, + ModifyPlanMethod: func(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + // In practice, the Configure method would save the + // provider data to the Action implementation and + // use it here. The fact that Configure is able to + // read the data proves this can work. + }, + }, + }, + expectedResponse: &fwserver.PlanActionResponse{}, + }, + "response-deferral-automatic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} + }, + }, + }, + configureProviderReq: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + request: &fwserver.PlanActionRequest{ + Config: testUnlinkedConfig, + ActionSchema: testUnlinkedSchema, + Action: &testprovider.ActionWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + resp.Diagnostics.AddError("Test assertion failed: ", "ModifyPlan shouldn't be called") + }, + }, + ClientCapabilities: testDeferralAllowed, + }, + expectedResponse: &fwserver.PlanActionResponse{ + Deferred: &action.Deferred{Reason: action.DeferredReasonProviderConfigUnknown}, + }, + }, + "response-deferral-manual": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanActionRequest{ + Config: testUnlinkedConfig, + ActionSchema: testUnlinkedSchema, + Action: &testprovider.ActionWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + var config struct { + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + resp.Deferred = &action.Deferred{Reason: action.DeferredReasonAbsentPrereq} + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + ClientCapabilities: testDeferralAllowed, + }, + expectedResponse: &fwserver.PlanActionResponse{ + Deferred: &action.Deferred{Reason: action.DeferredReasonAbsentPrereq}, + }, + }, + "response-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanActionRequest{ + Config: testUnlinkedConfig, + ActionSchema: testUnlinkedSchema, + Action: &testprovider.ActionWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.PlanActionResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + + response := &fwserver.PlanActionResponse{} + testCase.server.PlanAction(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/testing/testprovider/actionwithconfigureandmodifyplan.go b/internal/testing/testprovider/actionwithconfigureandmodifyplan.go new file mode 100644 index 000000000..f4ed4963d --- /dev/null +++ b/internal/testing/testprovider/actionwithconfigureandmodifyplan.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/action" +) + +var _ action.Action = &ActionWithConfigureAndModifyPlan{} +var _ action.ActionWithConfigure = &ActionWithConfigureAndModifyPlan{} +var _ action.ActionWithModifyPlan = &ActionWithConfigureAndModifyPlan{} + +// Declarative action.ActionWithConfigureAndModifyPlan for unit testing. +type ActionWithConfigureAndModifyPlan struct { + *Action + + // ActionWithConfigureAndModifyPlan interface methods + ConfigureMethod func(context.Context, action.ConfigureRequest, *action.ConfigureResponse) + + // ActionWithModifyPlan interface methods + ModifyPlanMethod func(context.Context, action.ModifyPlanRequest, *action.ModifyPlanResponse) +} + +// Configure satisfies the action.ActionWithConfigureAndModifyPlan interface. +func (r *ActionWithConfigureAndModifyPlan) Configure(ctx context.Context, req action.ConfigureRequest, resp *action.ConfigureResponse) { + if r.ConfigureMethod == nil { + return + } + + r.ConfigureMethod(ctx, req, resp) +} + +// ModifyPlan satisfies the action.ActionWithModifyPlan interface. +func (r *ActionWithConfigureAndModifyPlan) ModifyPlan(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + if r.ModifyPlanMethod == nil { + return + } + + r.ModifyPlanMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/actionwithmodifyplan.go b/internal/testing/testprovider/actionwithmodifyplan.go new file mode 100644 index 000000000..5f8cd3863 --- /dev/null +++ b/internal/testing/testprovider/actionwithmodifyplan.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/action" +) + +var _ action.Action = &ActionWithModifyPlan{} +var _ action.ActionWithModifyPlan = &ActionWithModifyPlan{} + +// Declarative action.ActionWithModifyPlan for unit testing. +type ActionWithModifyPlan struct { + *Action + + // ActionWithModifyPlan interface methods + ModifyPlanMethod func(context.Context, action.ModifyPlanRequest, *action.ModifyPlanResponse) +} + +// ModifyPlan satisfies the action.ActionWithModifyPlan interface. +func (p *ActionWithModifyPlan) ModifyPlan(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + if p.ModifyPlanMethod == nil { + return + } + + p.ModifyPlanMethod(ctx, req, resp) +} From 5d5960af32daeceaeaf78e4b83efae7424264e7a Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 14 Jul 2025 07:24:30 -0400 Subject: [PATCH 11/21] proto server tests --- .../proto5server/server_planaction_test.go | 177 +++++++++++++++++- .../proto6server/server_planaction_test.go | 177 +++++++++++++++++- 2 files changed, 350 insertions(+), 4 deletions(-) diff --git a/internal/proto5server/server_planaction_test.go b/internal/proto5server/server_planaction_test.go index ccf86051e..d3017e4a9 100644 --- a/internal/proto5server/server_planaction_test.go +++ b/internal/proto5server/server_planaction_test.go @@ -1,6 +1,179 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package proto5server_test +package proto5server -// TODO:Actions: Add unit tests once PlanAction is implemented +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerPlanAction(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + }, + } + + testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testUnlinkedSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.PlanActionRequest + expectedError error + expectedResponse *tfprotov5.PlanActionResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = schema.UnlinkedSchema{} + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.PlanActionRequest{ + Config: testEmptyDynamicValue, + ActionType: "test_action", + }, + expectedResponse: &tfprotov5.PlanActionResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.ActionWithModifyPlan{ + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + }, + ModifyPlanMethod: func(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + var config struct { + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.PlanActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedResponse: &tfprotov5.PlanActionResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.ActionWithModifyPlan{ + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + }, + ModifyPlanMethod: func(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.PlanActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedResponse: &tfprotov5.PlanActionResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.PlanAction(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_planaction_test.go b/internal/proto6server/server_planaction_test.go index 0ff1b5e7c..8d4093e2f 100644 --- a/internal/proto6server/server_planaction_test.go +++ b/internal/proto6server/server_planaction_test.go @@ -1,6 +1,179 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package proto6server_test +package proto6server -// TODO:Actions: Add unit tests once PlanAction is implemented +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerPlanAction(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + }, + } + + testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testUnlinkedSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.PlanActionRequest + expectedError error + expectedResponse *tfprotov6.PlanActionResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = schema.UnlinkedSchema{} + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.PlanActionRequest{ + Config: testEmptyDynamicValue, + ActionType: "test_action", + }, + expectedResponse: &tfprotov6.PlanActionResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.ActionWithModifyPlan{ + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + }, + ModifyPlanMethod: func(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + var config struct { + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.PlanActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedResponse: &tfprotov6.PlanActionResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.ActionWithModifyPlan{ + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + }, + ModifyPlanMethod: func(ctx context.Context, req action.ModifyPlanRequest, resp *action.ModifyPlanResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.PlanActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedResponse: &tfprotov6.PlanActionResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.PlanAction(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} From 7ec37c2dd320d325e0cbef06ba646cbf3e557907 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 14 Jul 2025 08:04:32 -0400 Subject: [PATCH 12/21] from/to plan tests --- internal/fromproto5/planaction_test.go | 140 ++++++++++++++++++++++++- internal/fromproto6/planaction_test.go | 140 ++++++++++++++++++++++++- internal/toproto5/planaction_test.go | 81 +++++++++++++- internal/toproto6/planaction_test.go | 81 +++++++++++++- 4 files changed, 438 insertions(+), 4 deletions(-) diff --git a/internal/fromproto5/planaction_test.go b/internal/fromproto5/planaction_test.go index c5b20a804..294792d58 100644 --- a/internal/fromproto5/planaction_test.go +++ b/internal/fromproto5/planaction_test.go @@ -3,4 +3,142 @@ package fromproto5_test -// TODO:Actions: Add unit tests once this mapping logic is complete +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestPlanActionRequest(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testUnlinkedSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.PlanActionRequest + actionSchema fwschema.Schema + actionImpl action.Action + providerMetaSchema fwschema.Schema + expected *fwserver.PlanActionRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.PlanActionRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "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.", + ), + }, + }, + "config-missing-schema": { + input: &tfprotov5.PlanActionRequest{ + Config: &testProto5DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "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.", + ), + }, + }, + "config": { + input: &tfprotov5.PlanActionRequest{ + Config: &testProto5DynamicValue, + }, + actionSchema: testUnlinkedSchema, + expected: &fwserver.PlanActionRequest{ + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testUnlinkedSchema, + }, + ActionSchema: testUnlinkedSchema, + }, + }, + "client-capabilities": { + input: &tfprotov5.PlanActionRequest{ + ClientCapabilities: &tfprotov5.PlanActionClientCapabilities{ + DeferralAllowed: true, + }, + }, + actionSchema: testUnlinkedSchema, + expected: &fwserver.PlanActionRequest{ + ActionSchema: testUnlinkedSchema, + ClientCapabilities: action.ModifyPlanClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, + "client-capabilities-unset": { + input: &tfprotov5.PlanActionRequest{}, + actionSchema: testUnlinkedSchema, + expected: &fwserver.PlanActionRequest{ + ActionSchema: testUnlinkedSchema, + ClientCapabilities: action.ModifyPlanClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.PlanActionRequest(context.Background(), testCase.input, testCase.actionImpl, testCase.actionSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/planaction_test.go b/internal/fromproto6/planaction_test.go index 5c9fa3702..6bc0fa7fe 100644 --- a/internal/fromproto6/planaction_test.go +++ b/internal/fromproto6/planaction_test.go @@ -3,4 +3,142 @@ package fromproto6_test -// TODO:Actions: Add unit tests once this mapping logic is complete +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestPlanActionRequest(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testUnlinkedSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.PlanActionRequest + actionSchema fwschema.Schema + actionImpl action.Action + providerMetaSchema fwschema.Schema + expected *fwserver.PlanActionRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.PlanActionRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "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.", + ), + }, + }, + "config-missing-schema": { + input: &tfprotov6.PlanActionRequest{ + Config: &testProto6DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "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.", + ), + }, + }, + "config": { + input: &tfprotov6.PlanActionRequest{ + Config: &testProto6DynamicValue, + }, + actionSchema: testUnlinkedSchema, + expected: &fwserver.PlanActionRequest{ + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testUnlinkedSchema, + }, + ActionSchema: testUnlinkedSchema, + }, + }, + "client-capabilities": { + input: &tfprotov6.PlanActionRequest{ + ClientCapabilities: &tfprotov6.PlanActionClientCapabilities{ + DeferralAllowed: true, + }, + }, + actionSchema: testUnlinkedSchema, + expected: &fwserver.PlanActionRequest{ + ActionSchema: testUnlinkedSchema, + ClientCapabilities: action.ModifyPlanClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, + "client-capabilities-unset": { + input: &tfprotov6.PlanActionRequest{}, + actionSchema: testUnlinkedSchema, + expected: &fwserver.PlanActionRequest{ + ActionSchema: testUnlinkedSchema, + ClientCapabilities: action.ModifyPlanClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.PlanActionRequest(context.Background(), testCase.input, testCase.actionImpl, testCase.actionSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/planaction_test.go b/internal/toproto5/planaction_test.go index 41e665462..44aca43ae 100644 --- a/internal/toproto5/planaction_test.go +++ b/internal/toproto5/planaction_test.go @@ -3,4 +3,83 @@ package toproto5_test -// TODO:Actions: Add unit tests once this mapping logic is complete +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" +) + +func TestPlanActionResponse(t *testing.T) { + t.Parallel() + + testDeferral := &action.Deferred{ + Reason: action.DeferredReasonAbsentPrereq, + } + + testProto5Deferred := &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonAbsentPrereq, + } + + testCases := map[string]struct { + input *fwserver.PlanActionResponse + expected *tfprotov5.PlanActionResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.PlanActionResponse{}, + expected: &tfprotov5.PlanActionResponse{}, + }, + "diagnostics": { + input: &fwserver.PlanActionResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.PlanActionResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "deferral": { + input: &fwserver.PlanActionResponse{ + Deferred: testDeferral, + }, + expected: &tfprotov5.PlanActionResponse{ + Deferred: testProto5Deferred, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.PlanActionResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/planaction_test.go b/internal/toproto6/planaction_test.go index 29cade55e..7818597fb 100644 --- a/internal/toproto6/planaction_test.go +++ b/internal/toproto6/planaction_test.go @@ -3,4 +3,83 @@ package toproto6_test -// TODO:Actions: Add unit tests once this mapping logic is complete +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" +) + +func TestPlanActionResponse(t *testing.T) { + t.Parallel() + + testDeferral := &action.Deferred{ + Reason: action.DeferredReasonAbsentPrereq, + } + + testProto6Deferred := &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonAbsentPrereq, + } + + testCases := map[string]struct { + input *fwserver.PlanActionResponse + expected *tfprotov6.PlanActionResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.PlanActionResponse{}, + expected: &tfprotov6.PlanActionResponse{}, + }, + "diagnostics": { + input: &fwserver.PlanActionResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.PlanActionResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "deferral": { + input: &fwserver.PlanActionResponse{ + Deferred: testDeferral, + }, + expected: &tfprotov6.PlanActionResponse{ + Deferred: testProto6Deferred, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.PlanActionResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From e43f5769dd73a6e88aeff437c4def56cf5e983b9 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 14 Jul 2025 08:10:58 -0400 Subject: [PATCH 13/21] from invoke --- internal/fromproto5/invokeaction_test.go | 116 ++++++++++++++++++++++- internal/fromproto6/invokeaction_test.go | 116 ++++++++++++++++++++++- 2 files changed, 230 insertions(+), 2 deletions(-) diff --git a/internal/fromproto5/invokeaction_test.go b/internal/fromproto5/invokeaction_test.go index c5b20a804..6f74fcf83 100644 --- a/internal/fromproto5/invokeaction_test.go +++ b/internal/fromproto5/invokeaction_test.go @@ -3,4 +3,118 @@ package fromproto5_test -// TODO:Actions: Add unit tests once this mapping logic is complete +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestInvokeActionRequest(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testUnlinkedSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.InvokeActionRequest + actionSchema fwschema.Schema + actionImpl action.Action + providerMetaSchema fwschema.Schema + expected *fwserver.InvokeActionRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.InvokeActionRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "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.", + ), + }, + }, + "config-missing-schema": { + input: &tfprotov5.InvokeActionRequest{ + Config: &testProto5DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "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.", + ), + }, + }, + "config": { + input: &tfprotov5.InvokeActionRequest{ + Config: &testProto5DynamicValue, + }, + actionSchema: testUnlinkedSchema, + expected: &fwserver.InvokeActionRequest{ + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testUnlinkedSchema, + }, + ActionSchema: testUnlinkedSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.InvokeActionRequest(context.Background(), testCase.input, testCase.actionImpl, testCase.actionSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/invokeaction_test.go b/internal/fromproto6/invokeaction_test.go index 5c9fa3702..9772d420e 100644 --- a/internal/fromproto6/invokeaction_test.go +++ b/internal/fromproto6/invokeaction_test.go @@ -3,4 +3,118 @@ package fromproto6_test -// TODO:Actions: Add unit tests once this mapping logic is complete +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestInvokeActionRequest(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testUnlinkedSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.InvokeActionRequest + actionSchema fwschema.Schema + actionImpl action.Action + providerMetaSchema fwschema.Schema + expected *fwserver.InvokeActionRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.InvokeActionRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "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.", + ), + }, + }, + "config-missing-schema": { + input: &tfprotov6.InvokeActionRequest{ + Config: &testProto6DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "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.", + ), + }, + }, + "config": { + input: &tfprotov6.InvokeActionRequest{ + Config: &testProto6DynamicValue, + }, + actionSchema: testUnlinkedSchema, + expected: &fwserver.InvokeActionRequest{ + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testUnlinkedSchema, + }, + ActionSchema: testUnlinkedSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.InvokeActionRequest(context.Background(), testCase.input, testCase.actionImpl, testCase.actionSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} From 2f08b28fa62cc114f86de456f8e9577f55c8ecad Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 14 Jul 2025 10:46:44 -0400 Subject: [PATCH 14/21] add invoke impl with just completed event --- action/action.go | 5 +- action/invoke.go | 32 +++ internal/fromproto5/invokeaction.go | 1 + internal/fromproto6/invokeaction.go | 1 + internal/fwserver/server_invokeaction.go | 49 ++++- internal/fwserver/server_invokeaction_test.go | 181 ++++++++++++++++- internal/proto5server/server_invokeaction.go | 2 + .../proto5server/server_invokeaction_test.go | 186 +++++++++++++++++- internal/proto6server/server_invokeaction.go | 2 + .../proto6server/server_invokeaction_test.go | 186 +++++++++++++++++- internal/testing/testprovider/action.go | 10 + .../testprovider/actionwithconfigure.go | 30 +++ 12 files changed, 674 insertions(+), 11 deletions(-) create mode 100644 action/invoke.go create mode 100644 internal/testing/testprovider/actionwithconfigure.go diff --git a/action/action.go b/action/action.go index ec07a084d..42bb16026 100644 --- a/action/action.go +++ b/action/action.go @@ -12,7 +12,10 @@ type Action interface { // 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 + // Invoke is called to run the logic of the action and update linked resources if applicable. + // Config, linked resource planned state, and linked resource prior state values should + // be read from the InvokeRequest and new linked resource state values set on the InvokeResponse. + Invoke(context.Context, InvokeRequest, *InvokeResponse) } // ActionWithConfigure is an interface type that extends Action to diff --git a/action/invoke.go b/action/invoke.go new file mode 100644 index 000000000..b2a1a7911 --- /dev/null +++ b/action/invoke.go @@ -0,0 +1,32 @@ +// 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/tfsdk" +) + +// InvokeRequest represents a request for the provider to invoke the action and update +// the requested action's linked resources. +type InvokeRequest struct { + // Config is the configuration the user supplied for the action. + Config tfsdk.Config + + // TODO:Actions: Add linked resources once lifecycle/linked actions are implemented +} + +// InvokeResponse represents a response to an InvokeRequest. An +// instance of this response struct is supplied as +// an argument to the action's Invoke function, in which the provider +// should set values on the InvokeResponse as appropriate. +type InvokeResponse struct { + // Diagnostics report errors or warnings related to invoking the action or updating + // the state of the requested action's linked resources. Returning an empty slice + // indicates a successful invocation with no warnings or errors + // generated. + Diagnostics diag.Diagnostics + + // TODO:Actions: Add linked resources once lifecycle/linked actions are implemented +} diff --git a/internal/fromproto5/invokeaction.go b/internal/fromproto5/invokeaction.go index 6290147d3..85690b707 100644 --- a/internal/fromproto5/invokeaction.go +++ b/internal/fromproto5/invokeaction.go @@ -37,6 +37,7 @@ func InvokeActionRequest(ctx context.Context, proto5 *tfprotov5.InvokeActionRequ } fw := &fwserver.InvokeActionRequest{ + Action: reqAction, ActionSchema: actionSchema, } diff --git a/internal/fromproto6/invokeaction.go b/internal/fromproto6/invokeaction.go index 270a2bacb..04ca704b4 100644 --- a/internal/fromproto6/invokeaction.go +++ b/internal/fromproto6/invokeaction.go @@ -37,6 +37,7 @@ func InvokeActionRequest(ctx context.Context, proto6 *tfprotov6.InvokeActionRequ } fw := &fwserver.InvokeActionRequest{ + Action: reqAction, ActionSchema: actionSchema, } diff --git a/internal/fwserver/server_invokeaction.go b/internal/fwserver/server_invokeaction.go index 200a5b811..0a30c9085 100644 --- a/internal/fwserver/server_invokeaction.go +++ b/internal/fwserver/server_invokeaction.go @@ -6,13 +6,17 @@ package fwserver import ( "context" + "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/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) // InvokeActionRequest is the framework server request for the InvokeAction RPC. type InvokeActionRequest struct { + Action action.Action ActionSchema fwschema.Schema Config *tfsdk.Config } @@ -24,9 +28,44 @@ type InvokeActionResponse struct { // 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.", - ) + if req == nil { + return + } + + if actionWithConfigure, ok := req.Action.(action.ActionWithConfigure); ok { + logging.FrameworkTrace(ctx, "Action implements ActionWithConfigure") + + configureReq := action.ConfigureRequest{ + ProviderData: s.ActionConfigureData, + } + configureResp := action.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Action Configure") + actionWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined Action Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + if req.Config == nil { + req.Config = &tfsdk.Config{ + Raw: tftypes.NewValue(req.ActionSchema.Type().TerraformType(ctx), nil), + Schema: req.ActionSchema, + } + } + + invokeReq := action.InvokeRequest{ + Config: *req.Config, + } + invokeResp := action.InvokeResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Action Invoke") + req.Action.Invoke(ctx, invokeReq, &invokeResp) + logging.FrameworkTrace(ctx, "Called provider defined Action Invoke") + + resp.Diagnostics = invokeResp.Diagnostics } diff --git a/internal/fwserver/server_invokeaction_test.go b/internal/fwserver/server_invokeaction_test.go index 5879cddb3..ece66c657 100644 --- a/internal/fwserver/server_invokeaction_test.go +++ b/internal/fwserver/server_invokeaction_test.go @@ -3,4 +3,183 @@ package fwserver_test -// TODO:Actions: Add unit tests once InvokeAction is implemented +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerInvokeAction(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + }, + } + + testConfigValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testUnlinkedSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testUnlinkedConfig := &tfsdk.Config{ + Raw: testConfigValue, + Schema: testUnlinkedSchema, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.InvokeActionRequest + expectedResponse *fwserver.InvokeActionResponse + configureProviderReq *provider.ConfigureRequest + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.InvokeActionResponse{}, + }, + "unlinked-nil-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.InvokeActionRequest{ + ActionSchema: testUnlinkedSchema, + Action: &testprovider.Action{ + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + if !req.Config.Raw.IsNull() { + resp.Diagnostics.AddError("Unexpected Config in action Invoke", "Expected Config to be null") + } + }, + }, + }, + expectedResponse: &fwserver.InvokeActionResponse{}, + }, + "request-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.InvokeActionRequest{ + Config: testUnlinkedConfig, + ActionSchema: testUnlinkedSchema, + Action: &testprovider.Action{ + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + var config struct { + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.InvokeActionResponse{}, + }, + "action-configure-data": { + server: &fwserver.Server{ + ActionConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.InvokeActionRequest{ + Config: testUnlinkedConfig, + ActionSchema: testUnlinkedSchema, + Action: &testprovider.ActionWithConfigure{ + ConfigureMethod: func(ctx context.Context, req action.ConfigureRequest, resp *action.ConfigureResponse) { + providerData, ok := req.ProviderData.(string) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected string, got: %T", req.ProviderData), + ) + return + } + + if providerData != "test-provider-configure-value" { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected test-provider-configure-value, got: %q", providerData), + ) + } + }, + Action: &testprovider.Action{ + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + // In practice, the Configure method would save the + // provider data to the Action implementation and + // use it here. The fact that Configure is able to + // read the data proves this can work. + }, + }, + }, + }, + expectedResponse: &fwserver.InvokeActionResponse{}, + }, + "response-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.InvokeActionRequest{ + Config: testUnlinkedConfig, + ActionSchema: testUnlinkedSchema, + Action: &testprovider.Action{ + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.InvokeActionResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + + response := &fwserver.InvokeActionResponse{} + testCase.server.InvokeAction(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/proto5server/server_invokeaction.go b/internal/proto5server/server_invokeaction.go index e7e2d6fd6..4cc2a2f97 100644 --- a/internal/proto5server/server_invokeaction.go +++ b/internal/proto5server/server_invokeaction.go @@ -60,6 +60,8 @@ func (s *Server) InvokeAction(ctx context.Context, proto5Req *tfprotov5.InvokeAc return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics) } + // TODO:Actions: Create messaging call back for progress updates + 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. diff --git a/internal/proto5server/server_invokeaction_test.go b/internal/proto5server/server_invokeaction_test.go index 4cc50c03a..d00050b3f 100644 --- a/internal/proto5server/server_invokeaction_test.go +++ b/internal/proto5server/server_invokeaction_test.go @@ -1,6 +1,188 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package proto5server_test +package proto5server -// TODO:Actions: Add unit tests once InvokeAction is implemented +import ( + "context" + "slices" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerInvokeAction(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + }, + } + + testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testUnlinkedSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.InvokeActionRequest + expectedError error + expectedEvents []tfprotov5.InvokeActionEvent + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = schema.UnlinkedSchema{} + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.InvokeActionRequest{ + Config: testEmptyDynamicValue, + ActionType: "test_action", + }, + expectedEvents: []tfprotov5.InvokeActionEvent{ + { + Type: tfprotov5.CompletedInvokeActionEventType{}, + }, + }, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + var config struct { + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.InvokeActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedEvents: []tfprotov5.InvokeActionEvent{ + { + Type: tfprotov5.CompletedInvokeActionEventType{}, + }, + }, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.InvokeActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedEvents: []tfprotov5.InvokeActionEvent{ + { + Type: tfprotov5.CompletedInvokeActionEventType{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.InvokeAction(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedEvents, slices.Collect(got.Events)); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_invokeaction.go b/internal/proto6server/server_invokeaction.go index 3a6d78cee..107ef856a 100644 --- a/internal/proto6server/server_invokeaction.go +++ b/internal/proto6server/server_invokeaction.go @@ -60,6 +60,8 @@ func (s *Server) InvokeAction(ctx context.Context, proto6Req *tfprotov6.InvokeAc return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics) } + // TODO:Actions: Create messaging call back for progress updates + 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. diff --git a/internal/proto6server/server_invokeaction_test.go b/internal/proto6server/server_invokeaction_test.go index 7bc3d2a68..3c9a81835 100644 --- a/internal/proto6server/server_invokeaction_test.go +++ b/internal/proto6server/server_invokeaction_test.go @@ -1,6 +1,188 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package proto6server_test +package proto6server -// TODO:Actions: Add unit tests once InvokeAction is implemented +import ( + "context" + "slices" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerInvokeAction(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + }, + } + + testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testUnlinkedSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.InvokeActionRequest + expectedError error + expectedEvents []tfprotov6.InvokeActionEvent + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = schema.UnlinkedSchema{} + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.InvokeActionRequest{ + Config: testEmptyDynamicValue, + ActionType: "test_action", + }, + expectedEvents: []tfprotov6.InvokeActionEvent{ + { + Type: tfprotov6.CompletedInvokeActionEventType{}, + }, + }, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + var config struct { + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.InvokeActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedEvents: []tfprotov6.InvokeActionEvent{ + { + Type: tfprotov6.CompletedInvokeActionEventType{}, + }, + }, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.InvokeActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedEvents: []tfprotov6.InvokeActionEvent{ + { + Type: tfprotov6.CompletedInvokeActionEventType{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.InvokeAction(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedEvents, slices.Collect(got.Events)); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/testing/testprovider/action.go b/internal/testing/testprovider/action.go index 610ee8326..19f0260bd 100644 --- a/internal/testing/testprovider/action.go +++ b/internal/testing/testprovider/action.go @@ -16,6 +16,7 @@ type Action struct { // Action interface methods MetadataMethod func(context.Context, action.MetadataRequest, *action.MetadataResponse) SchemaMethod func(context.Context, action.SchemaRequest, *action.SchemaResponse) + InvokeMethod func(context.Context, action.InvokeRequest, *action.InvokeResponse) } // Metadata satisfies the action.Action interface. @@ -35,3 +36,12 @@ func (d *Action) Schema(ctx context.Context, req action.SchemaRequest, resp *act d.SchemaMethod(ctx, req, resp) } + +// Invoke satisfies the action.Action interface. +func (d *Action) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + if d.InvokeMethod == nil { + return + } + + d.InvokeMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/actionwithconfigure.go b/internal/testing/testprovider/actionwithconfigure.go new file mode 100644 index 000000000..415301ed1 --- /dev/null +++ b/internal/testing/testprovider/actionwithconfigure.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/action" +) + +var _ action.Action = &ActionWithConfigure{} +var _ action.ActionWithConfigure = &ActionWithConfigure{} + +// Declarative action.ActionWithConfigure for unit testing. +type ActionWithConfigure struct { + *Action + + // ActionWithConfigure interface methods + ConfigureMethod func(context.Context, action.ConfigureRequest, *action.ConfigureResponse) +} + +// Configure satisfies the action.ActionWithConfigure interface. +func (r *ActionWithConfigure) Configure(ctx context.Context, req action.ConfigureRequest, resp *action.ConfigureResponse) { + if r.ConfigureMethod == nil { + return + } + + r.ConfigureMethod(ctx, req, resp) +} From 261ef7d67c4f3fb8aa0913ac128164d6ce2f48d5 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 14 Jul 2025 10:46:50 -0400 Subject: [PATCH 15/21] fix map access in unit tests --- internal/fwserver/server_getmetadata_test.go | 4 ++++ internal/proto5server/server_getmetadata_test.go | 4 ++++ internal/proto6server/server_getmetadata_test.go | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index 798a1a77a..96aebfc22 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -1024,6 +1024,10 @@ func TestServerGetMetadata(t *testing.T) { testCase.server.GetMetadata(context.Background(), testCase.request, response) // Prevent false positives with random map access in testing + sort.Slice(response.Actions, func(i int, j int) bool { + return response.Actions[i].TypeName < response.Actions[j].TypeName + }) + sort.Slice(response.DataSources, func(i int, j int) bool { return response.DataSources[i].TypeName < response.DataSources[j].TypeName }) diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index 6f28652f4..29fe8e50e 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -349,6 +349,10 @@ func TestServerGetMetadata(t *testing.T) { } // Prevent false positives with random map access in testing + sort.Slice(got.Actions, func(i int, j int) bool { + return got.Actions[i].TypeName < got.Actions[j].TypeName + }) + sort.Slice(got.DataSources, func(i int, j int) bool { return got.DataSources[i].TypeName < got.DataSources[j].TypeName }) diff --git a/internal/proto6server/server_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go index 090091f27..2209429da 100644 --- a/internal/proto6server/server_getmetadata_test.go +++ b/internal/proto6server/server_getmetadata_test.go @@ -349,6 +349,10 @@ func TestServerGetMetadata(t *testing.T) { } // Prevent false positives with random map access in testing + sort.Slice(got.Actions, func(i int, j int) bool { + return got.Actions[i].TypeName < got.Actions[j].TypeName + }) + sort.Slice(got.DataSources, func(i int, j int) bool { return got.DataSources[i].TypeName < got.DataSources[j].TypeName }) From 47f963179f6baf45d517f558511e0391d0688bdf Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 15 Jul 2025 12:17:41 -0400 Subject: [PATCH 16/21] implementation of sending progress events --- action/invoke.go | 13 ++ internal/fwserver/server_invokeaction.go | 20 ++- internal/proto5server/server_invokeaction.go | 39 ++++-- .../proto5server/server_invokeaction_test.go | 129 ++++++++++++++++++ internal/proto6server/server_invokeaction.go | 39 ++++-- .../proto6server/server_invokeaction_test.go | 129 ++++++++++++++++++ internal/toproto5/invoke_action_event.go | 28 ++++ internal/toproto5/invoke_action_event_test.go | 93 +++++++++++++ internal/toproto6/invoke_action_event.go | 28 ++++ internal/toproto6/invoke_action_event_test.go | 93 +++++++++++++ 10 files changed, 583 insertions(+), 28 deletions(-) create mode 100644 internal/toproto5/invoke_action_event.go create mode 100644 internal/toproto5/invoke_action_event_test.go create mode 100644 internal/toproto6/invoke_action_event.go create mode 100644 internal/toproto6/invoke_action_event_test.go diff --git a/action/invoke.go b/action/invoke.go index b2a1a7911..65be360fb 100644 --- a/action/invoke.go +++ b/action/invoke.go @@ -28,5 +28,18 @@ type InvokeResponse struct { // generated. Diagnostics diag.Diagnostics + // SendProgress will immediately send a progress update to Terraform core during action invocation. + // This function is provided by the framework and can be called multiple times while action logic is running. + // + // TODO:Actions: More documentation about when you should use this / when you shouldn't + SendProgress func(event InvokeProgressEvent) + // TODO:Actions: Add linked resources once lifecycle/linked actions are implemented } + +// InvokeProgressEvent is the event returned to Terraform while an action is being invoked. +type InvokeProgressEvent struct { + // Message is the string that will be presented to the practitioner either via the console + // or an external system like HCP Terraform. + Message string +} diff --git a/internal/fwserver/server_invokeaction.go b/internal/fwserver/server_invokeaction.go index 0a30c9085..153ac6f06 100644 --- a/internal/fwserver/server_invokeaction.go +++ b/internal/fwserver/server_invokeaction.go @@ -23,7 +23,21 @@ type InvokeActionRequest struct { // InvokeActionEventsStream is the framework server stream for the InvokeAction RPC. type InvokeActionResponse struct { - Diagnostics diag.Diagnostics + // ProgressEvents is a channel provided by the consuming proto{5/6}server implementation + // that allows the provider developers to return progress events while the action is being invoked. + ProgressEvents chan InvokeProgressEvent + Diagnostics diag.Diagnostics +} + +type InvokeProgressEvent struct { + Message string +} + +// SendProgress is injected into the action.InvokeResponse for use by the provider developer +func (r *InvokeActionResponse) SendProgress(event action.InvokeProgressEvent) { + r.ProgressEvents <- InvokeProgressEvent{ + Message: event.Message, + } } // InvokeAction implements the framework server InvokeAction RPC. @@ -61,7 +75,9 @@ func (s *Server) InvokeAction(ctx context.Context, req *InvokeActionRequest, res invokeReq := action.InvokeRequest{ Config: *req.Config, } - invokeResp := action.InvokeResponse{} + invokeResp := action.InvokeResponse{ + SendProgress: resp.SendProgress, + } logging.FrameworkTrace(ctx, "Calling provider defined Action Invoke") req.Action.Invoke(ctx, invokeReq, &invokeResp) diff --git a/internal/proto5server/server_invokeaction.go b/internal/proto5server/server_invokeaction.go index 4cc2a2f97..216a067b3 100644 --- a/internal/proto5server/server_invokeaction.go +++ b/internal/proto5server/server_invokeaction.go @@ -60,21 +60,34 @@ func (s *Server) InvokeAction(ctx context.Context, proto5Req *tfprotov5.InvokeAc return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics) } - // TODO:Actions: Create messaging call back for progress updates - - 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), - }, - }) + // Create a channel for framework to receive progress events + progressChan := make(chan fwserver.InvokeProgressEvent) + fwResp.ProgressEvents = progressChan + + // Create a channel to be triggered when the invoke action method has finished + completedChan := make(chan any) + go func() { + s.FrameworkServer.InvokeAction(ctx, fwReq, fwResp) + close(completedChan) + }() + + for { + select { + // Actions can only push one completed event and it's automatically handled by the framework + // by closing the completed channel above. + case <-completedChan: + push(toproto5.CompletedInvokeActionEventType(ctx, fwResp)) + return + + // Actions can push multiple progress events + case progressEvent := <-fwResp.ProgressEvents: + if !push(toproto5.ProgressInvokeActionEventType(ctx, progressEvent)) { + return + } + } + } }, } diff --git a/internal/proto5server/server_invokeaction_test.go b/internal/proto5server/server_invokeaction_test.go index d00050b3f..6d08c770d 100644 --- a/internal/proto5server/server_invokeaction_test.go +++ b/internal/proto5server/server_invokeaction_test.go @@ -5,6 +5,7 @@ package proto5server import ( "context" + "fmt" "slices" "testing" @@ -120,6 +121,57 @@ func TestServerInvokeAction(t *testing.T) { }, }, }, + "response-progress-events": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + resp.SendProgress(action.InvokeProgressEvent{Message: "progress event 1"}) + resp.SendProgress(action.InvokeProgressEvent{Message: "progress event 2"}) + resp.SendProgress(action.InvokeProgressEvent{Message: "progress event 3"}) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.InvokeActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedEvents: []tfprotov5.InvokeActionEvent{ + { + Type: tfprotov5.ProgressInvokeActionEventType{ + Message: "progress event 1", + }, + }, + { + Type: tfprotov5.ProgressInvokeActionEventType{ + Message: "progress event 2", + }, + }, + { + Type: tfprotov5.ProgressInvokeActionEventType{ + Message: "progress event 3", + }, + }, + { + Type: tfprotov5.CompletedInvokeActionEventType{}, + }, + }, + }, "response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -168,6 +220,83 @@ func TestServerInvokeAction(t *testing.T) { }, }, }, + "response-diagnostics-with-progress-events": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + for i := 0; i < 5; i++ { + resp.SendProgress(action.InvokeProgressEvent{Message: fmt.Sprintf("progress event %d", i+1)}) + } + + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.InvokeActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedEvents: []tfprotov5.InvokeActionEvent{ + { + Type: tfprotov5.ProgressInvokeActionEventType{ + Message: "progress event 1", + }, + }, + { + Type: tfprotov5.ProgressInvokeActionEventType{ + Message: "progress event 2", + }, + }, + { + Type: tfprotov5.ProgressInvokeActionEventType{ + Message: "progress event 3", + }, + }, + { + Type: tfprotov5.ProgressInvokeActionEventType{ + Message: "progress event 4", + }, + }, + { + Type: tfprotov5.ProgressInvokeActionEventType{ + Message: "progress event 5", + }, + }, + { + Type: tfprotov5.CompletedInvokeActionEventType{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/proto6server/server_invokeaction.go b/internal/proto6server/server_invokeaction.go index 107ef856a..3803dcdea 100644 --- a/internal/proto6server/server_invokeaction.go +++ b/internal/proto6server/server_invokeaction.go @@ -60,21 +60,34 @@ func (s *Server) InvokeAction(ctx context.Context, proto6Req *tfprotov6.InvokeAc return invokeActionErrorDiagnostics(ctx, fwResp.Diagnostics) } - // TODO:Actions: Create messaging call back for progress updates - - 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), - }, - }) + // Create a channel for framework to receive progress events + progressChan := make(chan fwserver.InvokeProgressEvent) + fwResp.ProgressEvents = progressChan + + // Create a channel to be triggered when the invoke action method has finished + completedChan := make(chan any) + go func() { + s.FrameworkServer.InvokeAction(ctx, fwReq, fwResp) + close(completedChan) + }() + + for { + select { + // Actions can only push one completed event and it's automatically handled by the framework + // by closing the completed channel above. + case <-completedChan: + push(toproto6.CompletedInvokeActionEventType(ctx, fwResp)) + return + + // Actions can push multiple progress events + case progressEvent := <-fwResp.ProgressEvents: + if !push(toproto6.ProgressInvokeActionEventType(ctx, progressEvent)) { + return + } + } + } }, } diff --git a/internal/proto6server/server_invokeaction_test.go b/internal/proto6server/server_invokeaction_test.go index 3c9a81835..0c36bf980 100644 --- a/internal/proto6server/server_invokeaction_test.go +++ b/internal/proto6server/server_invokeaction_test.go @@ -5,6 +5,7 @@ package proto6server import ( "context" + "fmt" "slices" "testing" @@ -120,6 +121,57 @@ func TestServerInvokeAction(t *testing.T) { }, }, }, + "response-progress-events": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + resp.SendProgress(action.InvokeProgressEvent{Message: "progress event 1"}) + resp.SendProgress(action.InvokeProgressEvent{Message: "progress event 2"}) + resp.SendProgress(action.InvokeProgressEvent{Message: "progress event 3"}) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.InvokeActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedEvents: []tfprotov6.InvokeActionEvent{ + { + Type: tfprotov6.ProgressInvokeActionEventType{ + Message: "progress event 1", + }, + }, + { + Type: tfprotov6.ProgressInvokeActionEventType{ + Message: "progress event 2", + }, + }, + { + Type: tfprotov6.ProgressInvokeActionEventType{ + Message: "progress event 3", + }, + }, + { + Type: tfprotov6.CompletedInvokeActionEventType{}, + }, + }, + }, "response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -168,6 +220,83 @@ func TestServerInvokeAction(t *testing.T) { }, }, }, + "response-diagnostics-with-progress-events": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testUnlinkedSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + InvokeMethod: func(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + for i := 0; i < 5; i++ { + resp.SendProgress(action.InvokeProgressEvent{Message: fmt.Sprintf("progress event %d", i+1)}) + } + + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.InvokeActionRequest{ + Config: testConfigDynamicValue, + ActionType: "test_action", + }, + expectedEvents: []tfprotov6.InvokeActionEvent{ + { + Type: tfprotov6.ProgressInvokeActionEventType{ + Message: "progress event 1", + }, + }, + { + Type: tfprotov6.ProgressInvokeActionEventType{ + Message: "progress event 2", + }, + }, + { + Type: tfprotov6.ProgressInvokeActionEventType{ + Message: "progress event 3", + }, + }, + { + Type: tfprotov6.ProgressInvokeActionEventType{ + Message: "progress event 4", + }, + }, + { + Type: tfprotov6.ProgressInvokeActionEventType{ + Message: "progress event 5", + }, + }, + { + Type: tfprotov6.CompletedInvokeActionEventType{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/toproto5/invoke_action_event.go b/internal/toproto5/invoke_action_event.go new file mode 100644 index 000000000..af625c9af --- /dev/null +++ b/internal/toproto5/invoke_action_event.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func ProgressInvokeActionEventType(ctx context.Context, event fwserver.InvokeProgressEvent) tfprotov5.InvokeActionEvent { + return tfprotov5.InvokeActionEvent{ + Type: tfprotov5.ProgressInvokeActionEventType{ + Message: event.Message, + }, + } +} + +func CompletedInvokeActionEventType(ctx context.Context, event *fwserver.InvokeActionResponse) tfprotov5.InvokeActionEvent { + return tfprotov5.InvokeActionEvent{ + Type: tfprotov5.CompletedInvokeActionEventType{ + // TODO:Actions: Add linked resources when they are implemented + Diagnostics: Diagnostics(ctx, event.Diagnostics), + }, + } +} diff --git a/internal/toproto5/invoke_action_event_test.go b/internal/toproto5/invoke_action_event_test.go new file mode 100644 index 000000000..7921e9b35 --- /dev/null +++ b/internal/toproto5/invoke_action_event_test.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestProgressInvokeActionEventType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.InvokeProgressEvent + expected tfprotov5.InvokeActionEvent + }{ + "message": { + fw: fwserver.InvokeProgressEvent{ + Message: "hello world", + }, + expected: tfprotov5.InvokeActionEvent{ + Type: tfprotov5.ProgressInvokeActionEventType{ + Message: "hello world", + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.ProgressInvokeActionEventType(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestCompletedInvokeActionEventType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw *fwserver.InvokeActionResponse + expected tfprotov5.InvokeActionEvent + }{ + "diagnostics": { + fw: &fwserver.InvokeActionResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: tfprotov5.InvokeActionEvent{ + Type: tfprotov5.CompletedInvokeActionEventType{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.CompletedInvokeActionEventType(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/invoke_action_event.go b/internal/toproto6/invoke_action_event.go new file mode 100644 index 000000000..db6e83fb4 --- /dev/null +++ b/internal/toproto6/invoke_action_event.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func ProgressInvokeActionEventType(ctx context.Context, event fwserver.InvokeProgressEvent) tfprotov6.InvokeActionEvent { + return tfprotov6.InvokeActionEvent{ + Type: tfprotov6.ProgressInvokeActionEventType{ + Message: event.Message, + }, + } +} + +func CompletedInvokeActionEventType(ctx context.Context, event *fwserver.InvokeActionResponse) tfprotov6.InvokeActionEvent { + return tfprotov6.InvokeActionEvent{ + Type: tfprotov6.CompletedInvokeActionEventType{ + // TODO:Actions: Add linked resources when they are implemented + Diagnostics: Diagnostics(ctx, event.Diagnostics), + }, + } +} diff --git a/internal/toproto6/invoke_action_event_test.go b/internal/toproto6/invoke_action_event_test.go new file mode 100644 index 000000000..6e4ad5893 --- /dev/null +++ b/internal/toproto6/invoke_action_event_test.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestProgressInvokeActionEventType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.InvokeProgressEvent + expected tfprotov6.InvokeActionEvent + }{ + "message": { + fw: fwserver.InvokeProgressEvent{ + Message: "hello world", + }, + expected: tfprotov6.InvokeActionEvent{ + Type: tfprotov6.ProgressInvokeActionEventType{ + Message: "hello world", + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.ProgressInvokeActionEventType(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestCompletedInvokeActionEventType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw *fwserver.InvokeActionResponse + expected tfprotov6.InvokeActionEvent + }{ + "diagnostics": { + fw: &fwserver.InvokeActionResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: tfprotov6.InvokeActionEvent{ + Type: tfprotov6.CompletedInvokeActionEventType{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.CompletedInvokeActionEventType(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 119f4c537583b8ad512522087ce00f01ef09ab60 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 15 Jul 2025 12:20:16 -0400 Subject: [PATCH 17/21] mention progress events --- action/action.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/action/action.go b/action/action.go index 42bb16026..fdec8e15b 100644 --- a/action/action.go +++ b/action/action.go @@ -15,6 +15,9 @@ type Action interface { // Invoke is called to run the logic of the action and update linked resources if applicable. // Config, linked resource planned state, and linked resource prior state values should // be read from the InvokeRequest and new linked resource state values set on the InvokeResponse. + // + // The [InvokeResponse.SendProgress] function can be called in the Invoke method to immediately + // report progress events related to the invocation of the action to Terraform. Invoke(context.Context, InvokeRequest, *InvokeResponse) } From 98f7232bce2af5b0e743aec1c9afd60e69cc944f Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 15 Jul 2025 12:21:19 -0400 Subject: [PATCH 18/21] comments --- internal/toproto5/invoke_action_event.go | 2 +- internal/toproto6/invoke_action_event.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/toproto5/invoke_action_event.go b/internal/toproto5/invoke_action_event.go index af625c9af..d52e8a6eb 100644 --- a/internal/toproto5/invoke_action_event.go +++ b/internal/toproto5/invoke_action_event.go @@ -21,7 +21,7 @@ func ProgressInvokeActionEventType(ctx context.Context, event fwserver.InvokePro func CompletedInvokeActionEventType(ctx context.Context, event *fwserver.InvokeActionResponse) tfprotov5.InvokeActionEvent { return tfprotov5.InvokeActionEvent{ Type: tfprotov5.CompletedInvokeActionEventType{ - // TODO:Actions: Add linked resources when they are implemented + // TODO:Actions: Add linked resources once lifecycle/linked actions are implemented Diagnostics: Diagnostics(ctx, event.Diagnostics), }, } diff --git a/internal/toproto6/invoke_action_event.go b/internal/toproto6/invoke_action_event.go index db6e83fb4..c4410ae4e 100644 --- a/internal/toproto6/invoke_action_event.go +++ b/internal/toproto6/invoke_action_event.go @@ -21,7 +21,7 @@ func ProgressInvokeActionEventType(ctx context.Context, event fwserver.InvokePro func CompletedInvokeActionEventType(ctx context.Context, event *fwserver.InvokeActionResponse) tfprotov6.InvokeActionEvent { return tfprotov6.InvokeActionEvent{ Type: tfprotov6.CompletedInvokeActionEventType{ - // TODO:Actions: Add linked resources when they are implemented + // TODO:Actions: Add linked resources once lifecycle/linked actions are implemented Diagnostics: Diagnostics(ctx, event.Diagnostics), }, } From ea2deb8e7de0033954ac8930c6a7f8192daa1ee6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 15 Jul 2025 14:20:28 -0400 Subject: [PATCH 19/21] all attributes (primitive, collection, nested) --- action/schema/bool_attribute.go | 3 +- action/schema/dynamic_attribute.go | 170 +++++ action/schema/dynamic_attribute_test.go | 436 ++++++++++++ action/schema/float32_attribute.go | 172 +++++ action/schema/float32_attribute_test.go | 437 ++++++++++++ action/schema/float64_attribute.go | 172 +++++ action/schema/float64_attribute_test.go | 437 ++++++++++++ action/schema/int32_attribute.go | 172 +++++ action/schema/int32_attribute_test.go | 437 ++++++++++++ action/schema/int64_attribute.go | 172 +++++ action/schema/int64_attribute_test.go | 437 ++++++++++++ action/schema/list_nested_attribute.go | 228 +++++++ action/schema/list_nested_attribute_test.go | 691 +++++++++++++++++++ action/schema/map_attribute.go | 207 ++++++ action/schema/map_attribute_test.go | 532 +++++++++++++++ action/schema/map_nested_attribute.go | 228 +++++++ action/schema/map_nested_attribute_test.go | 691 +++++++++++++++++++ action/schema/number_attribute.go | 173 +++++ action/schema/number_attribute_test.go | 437 ++++++++++++ action/schema/object_attribute.go | 206 ++++++ action/schema/object_attribute_test.go | 566 ++++++++++++++++ action/schema/set_attribute.go | 202 ++++++ action/schema/set_attribute_test.go | 532 +++++++++++++++ action/schema/set_nested_attribute.go | 223 +++++++ action/schema/set_nested_attribute_test.go | 692 ++++++++++++++++++++ 25 files changed, 8651 insertions(+), 2 deletions(-) create mode 100644 action/schema/dynamic_attribute.go create mode 100644 action/schema/dynamic_attribute_test.go create mode 100644 action/schema/float32_attribute.go create mode 100644 action/schema/float32_attribute_test.go create mode 100644 action/schema/float64_attribute.go create mode 100644 action/schema/float64_attribute_test.go create mode 100644 action/schema/int32_attribute.go create mode 100644 action/schema/int32_attribute_test.go create mode 100644 action/schema/int64_attribute.go create mode 100644 action/schema/int64_attribute_test.go create mode 100644 action/schema/list_nested_attribute.go create mode 100644 action/schema/list_nested_attribute_test.go create mode 100644 action/schema/map_attribute.go create mode 100644 action/schema/map_attribute_test.go create mode 100644 action/schema/map_nested_attribute.go create mode 100644 action/schema/map_nested_attribute_test.go create mode 100644 action/schema/number_attribute.go create mode 100644 action/schema/number_attribute_test.go create mode 100644 action/schema/object_attribute.go create mode 100644 action/schema/object_attribute_test.go create mode 100644 action/schema/set_attribute.go create mode 100644 action/schema/set_attribute_test.go create mode 100644 action/schema/set_nested_attribute.go create mode 100644 action/schema/set_nested_attribute_test.go diff --git a/action/schema/bool_attribute.go b/action/schema/bool_attribute.go index 5558946e6..48c8f46db 100644 --- a/action/schema/bool_attribute.go +++ b/action/schema/bool_attribute.go @@ -36,8 +36,7 @@ type BoolAttribute struct { CustomType basetypes.BoolTypable // Required indicates whether the practitioner must enter a value for - // this attribute or not. Required and Optional cannot both be true, - // and Required and Computed cannot both be true. + // this attribute or not. Required and Optional cannot both be true. Required bool // Optional indicates whether the practitioner can choose to enter a value diff --git a/action/schema/dynamic_attribute.go b/action/schema/dynamic_attribute.go new file mode 100644 index 000000000..e9178f8ff --- /dev/null +++ b/action/schema/dynamic_attribute.go @@ -0,0 +1,170 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = DynamicAttribute{} +) + +// DynamicAttribute represents a schema attribute that is a dynamic, rather +// than a single static type. Static types are always preferable over dynamic +// types in Terraform as practitioners will receive less helpful configuration +// assistance from validation error diagnostics and editor integrations. When +// retrieving the value for this attribute, use types.Dynamic as the value type +// unless the CustomType field is set. +// +// The concrete value type for a dynamic is determined at runtime in this order: +// 1. By Terraform, if defined in the configuration (if Required or Optional). +// 2. By the provider (if Computed). +// +// Once the concrete value type has been determined, it must remain consistent between +// plan and apply or Terraform will return an error. +type DynamicAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.DynamicType. When retrieving data, the basetypes.DynamicValuable + // associated with this custom type must be used in place of types.Dynamic. + CustomType basetypes.DynamicTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a DynamicAttribute. +func (a DynamicAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a DynamicAttribute +// and all fields are equal. +func (a DynamicAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(DynamicAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a DynamicAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a DynamicAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a DynamicAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.DynamicType or the CustomType field value if defined. +func (a DynamicAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.DynamicType +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a DynamicAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a DynamicAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a DynamicAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a DynamicAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a DynamicAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsOptionalForImport() bool { + return false +} diff --git a/action/schema/dynamic_attribute_test.go b/action/schema/dynamic_attribute_test.go new file mode 100644 index 000000000..7f3bb5a78 --- /dev/null +++ b/action/schema/dynamic_attribute_test.go @@ -0,0 +1,436 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.DynamicAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.DynamicType"), + }, + "ElementKeyInt": { + attribute: schema.DynamicAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.DynamicType"), + }, + "ElementKeyString": { + attribute: schema.DynamicAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.DynamicType"), + }, + "ElementKeyValue": { + attribute: schema.DynamicAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.DynamicType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.DynamicAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.DynamicAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.DynamicAttribute{}, + other: testschema.AttributeWithDynamicValidators{}, + expected: false, + }, + "equal": { + attribute: schema.DynamicAttribute{}, + other: schema.DynamicAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected string + }{ + "no-description": { + attribute: schema.DynamicAttribute{}, + expected: "", + }, + "description": { + attribute: schema.DynamicAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.DynamicAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.DynamicAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected attr.Type + }{ + "base": { + attribute: schema.DynamicAttribute{}, + expected: types.DynamicType, + }, + "custom-type": { + attribute: schema.DynamicAttribute{ + CustomType: testtypes.DynamicType{}, + }, + expected: testtypes.DynamicType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-computed": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optional": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.DynamicAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-required": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "required": { + attribute: schema.DynamicAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/float32_attribute.go b/action/schema/float32_attribute.go new file mode 100644 index 000000000..e357d1338 --- /dev/null +++ b/action/schema/float32_attribute.go @@ -0,0 +1,172 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Float32Attribute{} +) + +// Float32Attribute represents a schema attribute that is a 32-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float32 as the value type unless the CustomType field is set. +// +// Use Int32Attribute for 32-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Float32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float32Type. When retrieving data, the basetypes.Float32Valuable + // associated with this custom type must be used in place of types.Float32. + CustomType basetypes.Float32Typable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float32Attribute. +func (a Float32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float32Attribute +// and all fields are equal. +func (a Float32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Float32Type or the CustomType field value if defined. +func (a Float32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float32Type +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a Float32Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Float32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a Float32Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/action/schema/float32_attribute_test.go b/action/schema/float32_attribute_test.go new file mode 100644 index 000000000..1253d1611 --- /dev/null +++ b/action/schema/float32_attribute_test.go @@ -0,0 +1,437 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float32Type"), + }, + "ElementKeyInt": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float32Type"), + }, + "ElementKeyString": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float32Type"), + }, + "ElementKeyValue": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float32Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float32Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float32Attribute{}, + other: testschema.AttributeWithFloat32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float32Attribute{}, + other: schema.Float32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Float32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float32Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float32Attribute{}, + expected: types.Float32Type, + }, + "custom-type": { + attribute: schema.Float32Attribute{ + CustomType: testtypes.Float32Type{}, + }, + expected: testtypes.Float32Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float32Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float32Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/float64_attribute.go b/action/schema/float64_attribute.go new file mode 100644 index 000000000..42ed95d0f --- /dev/null +++ b/action/schema/float64_attribute.go @@ -0,0 +1,172 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Float64Attribute{} +) + +// Float64Attribute represents a schema attribute that is a 64-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float64 as the value type unless the CustomType field is set. +// +// Use Int64Attribute for 64-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Float64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float64Type. When retrieving data, the basetypes.Float64Valuable + // associated with this custom type must be used in place of types.Float64. + CustomType basetypes.Float64Typable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float64Attribute. +func (a Float64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float64Attribute +// and all fields are equal. +func (a Float64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float64Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float64Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Float64Type or the CustomType field value if defined. +func (a Float64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float64Type +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a Float64Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Float64Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float64Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a Float64Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/action/schema/float64_attribute_test.go b/action/schema/float64_attribute_test.go new file mode 100644 index 000000000..f413d37c1 --- /dev/null +++ b/action/schema/float64_attribute_test.go @@ -0,0 +1,437 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float64Type"), + }, + "ElementKeyInt": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float64Type"), + }, + "ElementKeyString": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float64Type"), + }, + "ElementKeyValue": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float64Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float64Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float64Attribute{}, + other: testschema.AttributeWithFloat64Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float64Attribute{}, + other: schema.Float64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected string + }{ + "no-description": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Float64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float64Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float64Attribute{}, + expected: types.Float64Type, + }, + "custom-type": { + attribute: schema.Float64Attribute{ + CustomType: testtypes.Float64Type{}, + }, + expected: testtypes.Float64Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float64Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float64Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat54AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/int32_attribute.go b/action/schema/int32_attribute.go new file mode 100644 index 000000000..4bae0215d --- /dev/null +++ b/action/schema/int32_attribute.go @@ -0,0 +1,172 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Int32Attribute{} +) + +// Int32Attribute represents a schema attribute that is a 32-bit integer. +// When retrieving the value for this attribute, use types.Int32 as the value +// type unless the CustomType field is set. +// +// Use Float32Attribute for 32-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Int32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int32Type. When retrieving data, the basetypes.Int32Valuable + // associated with this custom type must be used in place of types.Int32. + CustomType basetypes.Int32Typable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int32Attribute. +func (a Int32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int32Attribute +// and all fields are equal. +func (a Int32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Int32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Int32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Int32Type or the CustomType field value if defined. +func (a Int32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int32Type +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a Int32Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Int32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a Int32Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/action/schema/int32_attribute_test.go b/action/schema/int32_attribute_test.go new file mode 100644 index 000000000..9b2fac75e --- /dev/null +++ b/action/schema/int32_attribute_test.go @@ -0,0 +1,437 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int32Type"), + }, + "ElementKeyInt": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int32Type"), + }, + "ElementKeyString": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int32Type"), + }, + "ElementKeyValue": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int32Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int32Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int32Attribute{}, + other: testschema.AttributeWithInt32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int32Attribute{}, + other: schema.Int32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Int32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Int32Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int32Attribute{}, + expected: types.Int32Type, + }, + "custom-type": { + attribute: schema.Int32Attribute{ + CustomType: testtypes.Int32Type{}, + }, + expected: testtypes.Int32Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Int32Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Int32Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt2AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/int64_attribute.go b/action/schema/int64_attribute.go new file mode 100644 index 000000000..1b527eae4 --- /dev/null +++ b/action/schema/int64_attribute.go @@ -0,0 +1,172 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Int64Attribute{} +) + +// Int64Attribute represents a schema attribute that is a 64-bit integer. +// When retrieving the value for this attribute, use types.Int64 as the value +// type unless the CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Int64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int64Type. When retrieving data, the basetypes.Int64Valuable + // associated with this custom type must be used in place of types.Int64. + CustomType basetypes.Int64Typable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int64Attribute. +func (a Int64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int64Attribute +// and all fields are equal. +func (a Int64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Int64Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Int64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int64Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Int64Type or the CustomType field value if defined. +func (a Int64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int64Type +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a Int64Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Int64Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int64Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a Int64Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/action/schema/int64_attribute_test.go b/action/schema/int64_attribute_test.go new file mode 100644 index 000000000..30376be27 --- /dev/null +++ b/action/schema/int64_attribute_test.go @@ -0,0 +1,437 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int64Type"), + }, + "ElementKeyInt": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int64Type"), + }, + "ElementKeyString": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int64Type"), + }, + "ElementKeyValue": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int64Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int64Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int64Attribute{}, + other: testschema.AttributeWithInt64Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int64Attribute{}, + other: schema.Int64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected string + }{ + "no-description": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Int64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Int64Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int64Attribute{}, + expected: types.Int64Type, + }, + "custom-type": { + attribute: schema.Int64Attribute{ + CustomType: testtypes.Int64Type{}, + }, + expected: testtypes.Int64Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Int64Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Int64Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/list_nested_attribute.go b/action/schema/list_nested_attribute.go new file mode 100644 index 000000000..ce3157ded --- /dev/null +++ b/action/schema/list_nested_attribute.go @@ -0,0 +1,228 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = ListNestedAttribute{} + _ fwschema.AttributeWithValidateImplementation = ListNestedAttribute{} +) + +// ListNestedAttribute represents an attribute that is a list of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.List +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use ListAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a list of objects or directly via square and curly brace syntax. +// +// # list of objects +// example_attribute = [ +// { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a list of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_attribute[0] +// # first known object nested_attribute value +// .example_attribute[0].nested_attribute +type ListNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.ListType of types.ObjectType. When retrieving data, the + // basetypes.ListValuable associated with this custom type must be used in + // place of types.List. + CustomType basetypes.ListTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyInt, otherwise returns an error. +func (a ListNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyInt) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to ListNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a ListNestedAttribute +// and all fields are equal. +func (a ListNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(ListNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a ListNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a ListNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ListNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a ListNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeList. +func (a ListNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeList +} + +// GetType returns ListType of ObjectType or CustomType. +func (a ListNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ListType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a ListNestedAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a ListNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ListNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a ListNestedAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (a ListNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/action/schema/list_nested_attribute_test.go b/action/schema/list_nested_attribute_test.go new file mode 100644 index 000000000..9f8a7cbd6 --- /dev/null +++ b/action/schema/list_nested_attribute_test.go @@ -0,0 +1,691 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyString": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListNestedAttribute"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.ListNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.ListNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.ListNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.ListNestedAttribute{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.ListNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.ListNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.ListNestedAttribute{ + Required: true, + CustomType: testtypes.ListType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + Required: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/map_attribute.go b/action/schema/map_attribute.go new file mode 100644 index 000000000..80687fa09 --- /dev/null +++ b/action/schema/map_attribute.go @@ -0,0 +1,207 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = MapAttribute{} + _ fwschema.AttributeWithValidateImplementation = MapAttribute{} +) + +// MapAttribute represents a schema attribute that is a map with a single +// element type. When retrieving the value for this attribute, use types.Map +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use MapNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a map or directly via curly brace syntax. +// +// # map of strings +// example_attribute = { +// key1 = "first", +// key2 = "second", +// } +// +// Terraform configurations reference this attribute using expressions that +// accept a map or an element directly via square brace string syntax: +// +// # key1 known element +// .example_attribute["key1"] +type MapAttribute struct { + // ElementType is the type for all elements of the map. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.MapType. When retrieving data, the basetypes.MapValuable + // associated with this custom type must be used in place of types.Map. + CustomType basetypes.MapTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a map +// index or an error. +func (a MapAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a MapAttribute +// and all fields are equal. +func (a MapAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(MapAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a MapAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a MapAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a MapAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.MapType or the CustomType field value if defined. +func (a MapAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.MapType{ + ElemType: a.ElementType, + } +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a MapAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a MapAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a MapAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a MapAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC +// and should never include false positives. +func (a MapAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/action/schema/map_attribute_test.go b/action/schema/map_attribute_test.go new file mode 100644 index 000000000..5ca883ebf --- /dev/null +++ b/action/schema/map_attribute_test.go @@ -0,0 +1,532 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapType"), + }, + "ElementKeyInt": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapType"), + }, + "ElementKeyString": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to MapType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.MapAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: schema.MapAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: schema.MapAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-description": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.MapAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.MapAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected attr.Type + }{ + "base": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: types.MapType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.MapAttribute{ + // CustomType: testtypes.MapType{}, + // }, + // expected: testtypes.MapType{}, + // }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-computed": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optional": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.MapAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-required": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.MapAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.MapAttribute{ + Required: true, + CustomType: testtypes.MapType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype": { + attribute: schema.MapAttribute{ + Required: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.MapAttribute{ + Required: true, + ElementType: types.DynamicType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.MapAttribute{ + Required: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/map_nested_attribute.go b/action/schema/map_nested_attribute.go new file mode 100644 index 000000000..82598dc2f --- /dev/null +++ b/action/schema/map_nested_attribute.go @@ -0,0 +1,228 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = MapNestedAttribute{} + _ fwschema.AttributeWithValidateImplementation = MapNestedAttribute{} +) + +// MapNestedAttribute represents an attribute that is a map of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Map +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use MapAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a map of objects or directly via curly brace syntax. +// +// # map of objects +// example_attribute = { +// key = { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a map of objects or an element directly via square brace string +// syntax: +// +// # known object at key +// .example_attribute["key"] +// # known object nested_attribute value at key +// .example_attribute["key"].nested_attribute +type MapNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.MapType of types.ObjectType. When retrieving data, the + // basetypes.MapValuable associated with this custom type must be used in + // place of types.Map. + CustomType basetypes.MapTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyString, otherwise returns an error. +func (a MapNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyString) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to MapNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a MapNestedAttribute +// and all fields are equal. +func (a MapNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(MapNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a MapNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a MapNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a MapNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a MapNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeMap. +func (a MapNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeMap +} + +// GetType returns MapType of ObjectType or CustomType. +func (a MapNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.MapType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a MapNestedAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a MapNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a MapNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a MapNestedAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (a MapNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/action/schema/map_nested_attribute_test.go b/action/schema/map_nested_attribute_test.go new file mode 100644 index 000000000..5561db14c --- /dev/null +++ b/action/schema/map_nested_attribute_test.go @@ -0,0 +1,691 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to MapNestedAttribute"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.MapNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.MapNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.MapNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.MapNestedAttribute{ + // CustomType: testtypes.MapType{}, + // }, + // expected: testtypes.MapType{}, + // }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.MapNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.MapNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.MapNestedAttribute{ + Required: true, + CustomType: testtypes.MapType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + Required: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/number_attribute.go b/action/schema/number_attribute.go new file mode 100644 index 000000000..a4bd6cee6 --- /dev/null +++ b/action/schema/number_attribute.go @@ -0,0 +1,173 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = NumberAttribute{} +) + +// NumberAttribute represents a schema attribute that is a generic number with +// up to 512 bits of floating point or integer precision. When retrieving the +// value for this attribute, use types.Number as the value type unless the +// CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// Int64Attribute for 64-bit integer number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point or integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type NumberAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.NumberType. When retrieving data, the basetypes.NumberValuable + // associated with this custom type must be used in place of types.Number. + CustomType basetypes.NumberTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a NumberAttribute. +func (a NumberAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a NumberAttribute +// and all fields are equal. +func (a NumberAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(NumberAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a NumberAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a NumberAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a NumberAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.NumberType or the CustomType field value if defined. +func (a NumberAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.NumberType +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a NumberAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a NumberAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a NumberAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a NumberAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} diff --git a/action/schema/number_attribute_test.go b/action/schema/number_attribute_test.go new file mode 100644 index 000000000..1ac07f668 --- /dev/null +++ b/action/schema/number_attribute_test.go @@ -0,0 +1,437 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.NumberAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.NumberType"), + }, + "ElementKeyInt": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.NumberType"), + }, + "ElementKeyString": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.NumberType"), + }, + "ElementKeyValue": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.NumberType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.NumberAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.NumberAttribute{}, + other: testschema.AttributeWithNumberValidators{}, + expected: false, + }, + "equal": { + attribute: schema.NumberAttribute{}, + other: schema.NumberAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected string + }{ + "no-description": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "description": { + attribute: schema.NumberAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.NumberAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected attr.Type + }{ + "base": { + attribute: schema.NumberAttribute{}, + expected: types.NumberType, + }, + "custom-type": { + attribute: schema.NumberAttribute{ + CustomType: testtypes.NumberType{}, + }, + expected: testtypes.NumberType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-computed": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optional": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.NumberAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-required": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "required": { + attribute: schema.NumberAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/object_attribute.go b/action/schema/object_attribute.go new file mode 100644 index 000000000..a8e54f96b --- /dev/null +++ b/action/schema/object_attribute.go @@ -0,0 +1,206 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = ObjectAttribute{} + _ fwschema.AttributeWithValidateImplementation = ObjectAttribute{} +) + +// ObjectAttribute represents a schema attribute that is an object with only +// type information for underlying attributes. When retrieving the value for +// this attribute, use types.Object as the value type unless the CustomType +// field is set. The AttributeTypes field must be set. +// +// Prefer SingleNestedAttribute over ObjectAttribute if the provider is +// using protocol version 6 and full attribute functionality is needed. +// +// Terraform configurations configure this attribute using expressions that +// return an object or directly via curly brace syntax. +// +// # object with one attribute +// example_attribute = { +// underlying_attribute = #... +// } +// +// Terraform configurations reference this attribute using expressions that +// accept an object or an attribute directly via period syntax: +// +// # underlying attribute +// .example_attribute.underlying_attribute +type ObjectAttribute struct { + // AttributeTypes is the mapping of underlying attribute names to attribute + // types. This field must be set. + // + // Attribute types that contain a collection with a nested dynamic type (i.e. types.List[types.Dynamic]) are not supported. + // If underlying dynamic collection values are required, replace this attribute definition with + // DynamicAttribute instead. + AttributeTypes map[string]attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into an +// attribute name or an error. +func (a ObjectAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a ObjectAttribute +// and all fields are equal. +func (a ObjectAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ObjectAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a ObjectAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a ObjectAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ObjectAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.ObjectType or the CustomType field value if defined. +func (a ObjectAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ObjectType{ + AttrTypes: a.AttributeTypes, + } +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a ObjectAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a ObjectAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ObjectAttribute) IsRequired() bool { + return a.Required +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a ObjectAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC +// and should never include false positives. +func (a ObjectAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.AttributeTypes == nil && a.CustomType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingAttributeTypesDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/action/schema/object_attribute_test.go b/action/schema/object_attribute_test.go new file mode 100644 index 000000000..1e04fdb66 --- /dev/null +++ b/action/schema/object_attribute_test.go @@ -0,0 +1,566 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.AttributeName("testattr"), + expected: types.StringType, + expectedError: nil, + }, + "AttributeName-missing": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("undefined attribute name other in ObjectType"), + }, + "ElementKeyInt": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to ObjectType"), + }, + "ElementKeyString": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ObjectType"), + }, + "ElementKeyValue": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ObjectType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "deprecation-message": { + attribute: schema.ObjectAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: testschema.AttributeWithObjectValidators{}, + expected: false, + }, + "different-attribute-type": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.BoolType}}, + expected: false, + }, + "equal": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected string + }{ + "no-description": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "description": { + attribute: schema.ObjectAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "markdown-description": { + attribute: schema.ObjectAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, + }, + "custom-type": { + attribute: schema.ObjectAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "optional": { + attribute: schema.ObjectAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-required": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "required": { + attribute: schema.ObjectAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "attributetypes": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + Required: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "attributetypes-dynamic": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.DynamicType, + "test_list": types.ListType{ + ElemType: types.StringType, + }, + "test_obj": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr": types.DynamicType, + }, + }, + }, + Required: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "attributetypes-nested-collection-dynamic": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.ListType{ + ElemType: types.DynamicType, + }, + }, + Required: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "attributetypes-missing": { + attribute: schema.ObjectAttribute{ + Required: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the AttributeTypes or CustomType field on an object Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + "customtype": { + attribute: schema.ObjectAttribute{ + Required: true, + CustomType: testtypes.ObjectType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/set_attribute.go b/action/schema/set_attribute.go new file mode 100644 index 000000000..449af04f5 --- /dev/null +++ b/action/schema/set_attribute.go @@ -0,0 +1,202 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = SetAttribute{} + _ fwschema.AttributeWithValidateImplementation = SetAttribute{} +) + +// SetAttribute represents a schema attribute that is a set with a single +// element type. When retrieving the value for this attribute, use types.Set +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use SetNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a set or directly via square brace syntax. +// +// # set of strings +// example_attribute = ["first", "second"] +// +// Terraform configurations reference this attribute using expressions that +// accept a set. Sets cannot be indexed in Terraform, therefore an expression +// is required to access an explicit element. +type SetAttribute struct { + // ElementType is the type for all elements of the set. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.SetType. When retrieving data, the basetypes.SetValuable + // associated with this custom type must be used in place of types.Set. + CustomType basetypes.SetTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a set +// index or an error. +func (a SetAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a SetAttribute +// and all fields are equal. +func (a SetAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(SetAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a SetAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a SetAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SetAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.SetType or the CustomType field value if defined. +func (a SetAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.SetType{ + ElemType: a.ElementType, + } +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a SetAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a SetAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SetAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a SetAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC +// and should never include false positives. +func (a SetAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/action/schema/set_attribute_test.go b/action/schema/set_attribute_test.go new file mode 100644 index 000000000..81602b855 --- /dev/null +++ b/action/schema/set_attribute_test.go @@ -0,0 +1,532 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetType"), + }, + "ElementKeyInt": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetType"), + }, + "ElementKeyString": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetType"), + }, + "ElementKeyValue": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: types.StringType, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.SetAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithSetValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: schema.SetAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: schema.SetAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected string + }{ + "no-description": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.SetAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.SetAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: types.SetType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.SetAttribute{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.SetAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-required": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.SetAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.SetAttribute{ + CustomType: testtypes.SetType{}, + Optional: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype": { + attribute: schema.SetAttribute{ + Required: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.SetAttribute{ + Required: true, + ElementType: types.DynamicType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.SetAttribute{ + Required: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/set_nested_attribute.go b/action/schema/set_nested_attribute.go new file mode 100644 index 000000000..5569ee6aa --- /dev/null +++ b/action/schema/set_nested_attribute.go @@ -0,0 +1,223 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = SetNestedAttribute{} + _ fwschema.AttributeWithValidateImplementation = SetNestedAttribute{} +) + +// SetNestedAttribute represents an attribute that is a set of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Set +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use SetAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a set of objects or directly via square and curly brace syntax. +// +// # set of objects +// example_attribute = [ +// { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a set of objects. Sets cannot be indexed in Terraform, therefore +// an expression is required to access an explicit element. +type SetNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.SetType of types.ObjectType. When retrieving data, the + // basetypes.SetValuable associated with this custom type must be used in + // place of types.Set. + CustomType basetypes.SetTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyValue, otherwise returns an error. +func (a SetNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyValue) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SetNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a SetNestedAttribute +// and all fields are equal. +func (a SetNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(SetNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a SetNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a SetNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SetNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a SetNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeSet. +func (a SetNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeSet +} + +// GetType returns SetType of ObjectType or CustomType. +func (a SetNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.SetType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed always returns false as action schema attributes cannot be Computed. +func (a SetNestedAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a SetNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SetNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive always returns false as action schema attributes cannot be Sensitive. +func (a SetNestedAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly always returns false as action schema attributes cannot be WriteOnly. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (a SetNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/action/schema/set_nested_attribute_test.go b/action/schema/set_nested_attribute_test.go new file mode 100644 index 000000000..007f8f5eb --- /dev/null +++ b/action/schema/set_nested_attribute_test.go @@ -0,0 +1,692 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.SetNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithSetValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.SetNestedAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.SetNestedAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.SetNestedAttribute{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.SetNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.SetNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.SetNestedAttribute{ + Required: true, + CustomType: testtypes.SetType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + Required: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 21ffee73a58e346ae2e77a4e0d4b54c45a55788f Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 15 Jul 2025 14:50:56 -0400 Subject: [PATCH 20/21] add blocks and docs --- action/schema/attribute.go | 2 - action/schema/block.go | 2 - action/schema/doc.go | 13 + action/schema/list_nested_block.go | 184 +++++++++ action/schema/list_nested_block_test.go | 513 ++++++++++++++++++++++++ action/schema/set_nested_block.go | 184 +++++++++ action/schema/set_nested_block_test.go | 513 ++++++++++++++++++++++++ 7 files changed, 1407 insertions(+), 4 deletions(-) create mode 100644 action/schema/doc.go create mode 100644 action/schema/list_nested_block.go create mode 100644 action/schema/list_nested_block_test.go create mode 100644 action/schema/set_nested_block.go create mode 100644 action/schema/set_nested_block_test.go diff --git a/action/schema/attribute.go b/action/schema/attribute.go index 1364b42bc..7384dc6e9 100644 --- a/action/schema/attribute.go +++ b/action/schema/attribute.go @@ -7,8 +7,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" ) -// TODO:Actions: Add all of the attribute and nested attribute types listed below -// // Attribute define a value field inside an action type schema. Implementations in this // package include: // - BoolAttribute diff --git a/action/schema/block.go b/action/schema/block.go index ecc291cf0..9760423a1 100644 --- a/action/schema/block.go +++ b/action/schema/block.go @@ -7,8 +7,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" ) -// TODO:Actions: Add all of the block and nested block types listed below -// // Block defines a structural field inside an action type schema. Implementations in this // package include: // - ListNestedBlock diff --git a/action/schema/doc.go b/action/schema/doc.go new file mode 100644 index 000000000..2a8503c1a --- /dev/null +++ b/action/schema/doc.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package schema contains all available schema functionality for actions. +// Action schemas define the structure and value types for configuration data. +// Schemas are implemented via the action.Action type Schema method. +// +// There are three different types of action schemas, which define how a practitioner can trigger an action, +// as well as what effect the action can have on the state. +// - [UnlinkedSchema] actions are actions that cannot cause changes to resource states. +// - [LifecycleSchema] actions are actions that can cause changes to exactly one resource state. +// - [LinkedSchema] actions are actions that can cause changes to one or more resource states. +package schema diff --git a/action/schema/list_nested_block.go b/action/schema/list_nested_block.go new file mode 100644 index 000000000..d61b8d888 --- /dev/null +++ b/action/schema/list_nested_block.go @@ -0,0 +1,184 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Block = ListNestedBlock{} + _ fwschema.BlockWithValidateImplementation = ListNestedBlock{} +) + +// ListNestedBlock represents a block that is a list of objects where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.List +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. +// +// Prefer ListNestedAttribute over ListNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block repeatedly using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # list of blocks with two elements +// example_block { +// nested_attribute = #... +// } +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept a list of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_block[0] +// # first known object nested_attribute value +// .example_block[0].nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type ListNestedBlock struct { + // NestedObject is the underlying object that contains nested attributes or + // blocks. This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this block definition with + // a DynamicAttribute. + NestedObject NestedBlockObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.ListType of types.ObjectType. When retrieving data, the + // basetypes.ListValuable associated with this custom type must be used in + // place of types.List. + CustomType basetypes.ListTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the NestedObject field value if step +// is ElementKeyInt, otherwise returns an error. +func (b ListNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyInt) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to ListNestedBlock", step) + } + + return b.NestedObject, nil +} + +// Equal returns true if the given Block is ListNestedBlock +// and all fields are equal. +func (b ListNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(ListNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b ListNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b ListNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b ListNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (b ListNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return b.NestedObject +} + +// GetNestingMode always returns BlockNestingModeList. +func (b ListNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeList +} + +// Type returns ListType of ObjectType or CustomType. +func (b ListNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + return types.ListType{ + ElemType: b.NestedObject.Type(), + } +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the block to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (b ListNestedBlock) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if b.CustomType == nil && fwtype.ContainsCollectionWithDynamic(b.Type()) { + resp.Diagnostics.Append(fwtype.BlockCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/action/schema/list_nested_block_test.go b/action/schema/list_nested_block_test.go new file mode 100644 index 000000000..f888c19e7 --- /dev/null +++ b/action/schema/list_nested_block_test.go @@ -0,0 +1,513 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListNestedBlock"), + }, + "ElementKeyInt": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyString": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListNestedBlock"), + }, + "ElementKeyValue": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListNestedBlock"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.ListNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.BlockWithListValidators{}, + expected: false, + }, + "different-attributes-definitions": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "different-blocks-definitions": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-description": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + block: schema.ListNestedBlock{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.ListNestedBlock{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected attr.Type + }{ + "base": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.ListNestedBlock{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + block: schema.ListNestedBlock{ + CustomType: testtypes.ListType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + Required: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a block that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" block definition with a DynamicAttribute.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.block.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/set_nested_block.go b/action/schema/set_nested_block.go new file mode 100644 index 000000000..158e43be1 --- /dev/null +++ b/action/schema/set_nested_block.go @@ -0,0 +1,184 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Block = SetNestedBlock{} + _ fwschema.BlockWithValidateImplementation = SetNestedBlock{} +) + +// SetNestedBlock represents a block that is a set of objects where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.Set +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. +// +// Prefer SetNestedAttribute over SetNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block repeatedly using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # set of blocks with two elements +// example_block { +// nested_attribute = #... +// } +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept a set of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_block[0] +// # first known object nested_attribute value +// .example_block[0].nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type SetNestedBlock struct { + // NestedObject is the underlying object that contains nested attributes or + // blocks. This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this block definition with + // a DynamicAttribute. + NestedObject NestedBlockObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.SetType of types.ObjectType. When retrieving data, the + // basetypes.SetValuable associated with this custom type must be used in + // place of types.Set. + CustomType basetypes.SetTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string +} + +// ApplyTerraform5AttributePathStep returns the NestedObject field value if step +// is ElementKeyValue, otherwise returns an error. +func (b SetNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyValue) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SetNestedBlock", step) + } + + return b.NestedObject, nil +} + +// Equal returns true if the given Block is SetNestedBlock +// and all fields are equal. +func (b SetNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(SetNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b SetNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b SetNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b SetNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (b SetNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return b.NestedObject +} + +// GetNestingMode always returns BlockNestingModeSet. +func (b SetNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSet +} + +// Type returns SetType of ObjectType or CustomType. +func (b SetNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + return types.SetType{ + ElemType: b.NestedObject.Type(), + } +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the block to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (b SetNestedBlock) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if b.CustomType == nil && fwtype.ContainsCollectionWithDynamic(b.Type()) { + resp.Diagnostics.Append(fwtype.BlockCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/action/schema/set_nested_block_test.go b/action/schema/set_nested_block_test.go new file mode 100644 index 000000000..e9c96e094 --- /dev/null +++ b/action/schema/set_nested_block_test.go @@ -0,0 +1,513 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetNestedBlock"), + }, + "ElementKeyInt": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetNestedBlock"), + }, + "ElementKeyString": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetNestedBlock"), + }, + "ElementKeyValue": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.SetNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.BlockWithSetValidators{}, + expected: false, + }, + "different-attributes-definitions": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "different-blocks-definitions": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-description": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + block: schema.SetNestedBlock{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.SetNestedBlock{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected attr.Type + }{ + "base": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.SetNestedBlock{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + block: schema.SetNestedBlock{ + CustomType: testtypes.SetType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + Required: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a block that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" block definition with a DynamicAttribute.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.block.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 1503be57c52e11f92efe5af767d3a06a629246c0 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 16 Jul 2025 10:07:09 -0400 Subject: [PATCH 21/21] update all commented out custom type tests --- action/schema/list_nested_attribute_test.go | 12 ++++++------ action/schema/list_nested_block_test.go | 12 ++++++------ action/schema/map_attribute_test.go | 12 ++++++------ action/schema/map_nested_attribute_test.go | 12 ++++++------ action/schema/set_attribute_test.go | 12 ++++++------ action/schema/set_nested_attribute_test.go | 12 ++++++------ action/schema/set_nested_block_test.go | 12 ++++++------ ephemeral/schema/list_attribute_test.go | 12 ++++++------ ephemeral/schema/list_nested_attribute_test.go | 12 ++++++------ ephemeral/schema/list_nested_block_test.go | 12 ++++++------ ephemeral/schema/map_attribute_test.go | 12 ++++++------ ephemeral/schema/map_nested_attribute_test.go | 12 ++++++------ ephemeral/schema/set_attribute_test.go | 12 ++++++------ ephemeral/schema/set_nested_attribute_test.go | 12 ++++++------ ephemeral/schema/set_nested_block_test.go | 12 ++++++------ ephemeral/schema/single_nested_block_test.go | 13 +++++++------ list/schema/list_attribute_test.go | 12 ++++++------ list/schema/map_attribute_test.go | 12 ++++++------ 18 files changed, 109 insertions(+), 108 deletions(-) diff --git a/action/schema/list_nested_attribute_test.go b/action/schema/list_nested_attribute_test.go index 9f8a7cbd6..0a53508ba 100644 --- a/action/schema/list_nested_attribute_test.go +++ b/action/schema/list_nested_attribute_test.go @@ -373,12 +373,12 @@ func TestListNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.ListNestedAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: schema.ListNestedAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/action/schema/list_nested_block_test.go b/action/schema/list_nested_block_test.go index f888c19e7..d23f2614e 100644 --- a/action/schema/list_nested_block_test.go +++ b/action/schema/list_nested_block_test.go @@ -414,12 +414,12 @@ func TestListNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.ListNestedBlock{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + block: schema.ListNestedBlock{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/action/schema/map_attribute_test.go b/action/schema/map_attribute_test.go index 5ca883ebf..5e0d42947 100644 --- a/action/schema/map_attribute_test.go +++ b/action/schema/map_attribute_test.go @@ -228,12 +228,12 @@ func TestMapAttributeGetType(t *testing.T) { attribute: schema.MapAttribute{ElementType: types.StringType}, expected: types.MapType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.MapAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/action/schema/map_nested_attribute_test.go b/action/schema/map_nested_attribute_test.go index 5561db14c..0db21ede3 100644 --- a/action/schema/map_nested_attribute_test.go +++ b/action/schema/map_nested_attribute_test.go @@ -373,12 +373,12 @@ func TestMapNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.MapNestedAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapNestedAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/action/schema/set_attribute_test.go b/action/schema/set_attribute_test.go index 81602b855..063c2b26a 100644 --- a/action/schema/set_attribute_test.go +++ b/action/schema/set_attribute_test.go @@ -228,12 +228,12 @@ func TestSetAttributeGetType(t *testing.T) { attribute: schema.SetAttribute{ElementType: types.StringType}, expected: types.SetType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.SetAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: schema.SetAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/action/schema/set_nested_attribute_test.go b/action/schema/set_nested_attribute_test.go index 007f8f5eb..2fd2f7b44 100644 --- a/action/schema/set_nested_attribute_test.go +++ b/action/schema/set_nested_attribute_test.go @@ -374,12 +374,12 @@ func TestSetNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.SetNestedAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: schema.SetNestedAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/action/schema/set_nested_block_test.go b/action/schema/set_nested_block_test.go index e9c96e094..5e95c6d1a 100644 --- a/action/schema/set_nested_block_test.go +++ b/action/schema/set_nested_block_test.go @@ -414,12 +414,12 @@ func TestSetNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.SetNestedBlock{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + block: schema.SetNestedBlock{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/list_attribute_test.go b/ephemeral/schema/list_attribute_test.go index 9ee0e5c85..45d6bb420 100644 --- a/ephemeral/schema/list_attribute_test.go +++ b/ephemeral/schema/list_attribute_test.go @@ -229,12 +229,12 @@ func TestListAttributeGetType(t *testing.T) { attribute: schema.ListAttribute{ElementType: types.StringType}, expected: types.ListType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.ListAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: schema.ListAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/list_nested_attribute_test.go b/ephemeral/schema/list_nested_attribute_test.go index daba7e113..3f64ab5c5 100644 --- a/ephemeral/schema/list_nested_attribute_test.go +++ b/ephemeral/schema/list_nested_attribute_test.go @@ -374,12 +374,12 @@ func TestListNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.ListNestedAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: schema.ListNestedAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/list_nested_block_test.go b/ephemeral/schema/list_nested_block_test.go index 763afd28c..67c530fa5 100644 --- a/ephemeral/schema/list_nested_block_test.go +++ b/ephemeral/schema/list_nested_block_test.go @@ -453,12 +453,12 @@ func TestListNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.ListNestedBlock{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + block: schema.ListNestedBlock{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/map_attribute_test.go b/ephemeral/schema/map_attribute_test.go index ed2348551..f4d8bfefe 100644 --- a/ephemeral/schema/map_attribute_test.go +++ b/ephemeral/schema/map_attribute_test.go @@ -229,12 +229,12 @@ func TestMapAttributeGetType(t *testing.T) { attribute: schema.MapAttribute{ElementType: types.StringType}, expected: types.MapType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.MapAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/map_nested_attribute_test.go b/ephemeral/schema/map_nested_attribute_test.go index 2bbaa15d5..a629cf874 100644 --- a/ephemeral/schema/map_nested_attribute_test.go +++ b/ephemeral/schema/map_nested_attribute_test.go @@ -374,12 +374,12 @@ func TestMapNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.MapNestedAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapNestedAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/set_attribute_test.go b/ephemeral/schema/set_attribute_test.go index af56ab903..22607393b 100644 --- a/ephemeral/schema/set_attribute_test.go +++ b/ephemeral/schema/set_attribute_test.go @@ -229,12 +229,12 @@ func TestSetAttributeGetType(t *testing.T) { attribute: schema.SetAttribute{ElementType: types.StringType}, expected: types.SetType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.SetAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: schema.SetAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/set_nested_attribute_test.go b/ephemeral/schema/set_nested_attribute_test.go index a049ade74..2fa517ee2 100644 --- a/ephemeral/schema/set_nested_attribute_test.go +++ b/ephemeral/schema/set_nested_attribute_test.go @@ -375,12 +375,12 @@ func TestSetNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.SetNestedAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: schema.SetNestedAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/set_nested_block_test.go b/ephemeral/schema/set_nested_block_test.go index d403d4486..721fbb933 100644 --- a/ephemeral/schema/set_nested_block_test.go +++ b/ephemeral/schema/set_nested_block_test.go @@ -453,12 +453,12 @@ func TestSetNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.SetNestedBlock{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + block: schema.SetNestedBlock{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/ephemeral/schema/single_nested_block_test.go b/ephemeral/schema/single_nested_block_test.go index 6c6a74639..aba710d23 100644 --- a/ephemeral/schema/single_nested_block_test.go +++ b/ephemeral/schema/single_nested_block_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -447,12 +448,12 @@ func TestSingleNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.SingleNestedBlock{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + block: schema.SingleNestedBlock{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } for name, testCase := range testCases { diff --git a/list/schema/list_attribute_test.go b/list/schema/list_attribute_test.go index 6346f7b33..c133fbff7 100644 --- a/list/schema/list_attribute_test.go +++ b/list/schema/list_attribute_test.go @@ -229,12 +229,12 @@ func TestListAttributeGetType(t *testing.T) { attribute: schema.ListAttribute{ElementType: types.StringType}, expected: types.ListType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.ListAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: schema.ListAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { diff --git a/list/schema/map_attribute_test.go b/list/schema/map_attribute_test.go index d74324d8c..39a45eb68 100644 --- a/list/schema/map_attribute_test.go +++ b/list/schema/map_attribute_test.go @@ -229,12 +229,12 @@ func TestMapAttributeGetType(t *testing.T) { attribute: schema.MapAttribute{ElementType: types.StringType}, expected: types.MapType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.MapAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases {