diff --git a/go.mod b/go.mod index 7b4c877..682a43b 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.7 require ( github.com/hashicorp/go-memdb v1.3.4 + github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-json v0.23.0 github.com/hashicorp/terraform-plugin-framework v1.13.0 github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 @@ -33,7 +34,6 @@ require ( github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hc-install v0.9.0 // indirect github.com/hashicorp/hcl/v2 v2.22.0 // indirect diff --git a/internal/echoprovider/data_router.go b/internal/echoprovider/data_router.go new file mode 100644 index 0000000..9c704e5 --- /dev/null +++ b/internal/echoprovider/data_router.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package echoprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +type errUnsupportedDataSource string + +func (e errUnsupportedDataSource) Error() string { + return "unsupported data source: " + string(e) +} + +type dataSourceRouter map[string]tfprotov6.DataSourceServer + +func (d dataSourceRouter) ValidateDataResourceConfig(ctx context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { + ds, ok := d[req.TypeName] + if !ok { + return nil, errUnsupportedDataSource(req.TypeName) + } + return ds.ValidateDataResourceConfig(ctx, req) +} + +func (d dataSourceRouter) ReadDataSource(ctx context.Context, req *tfprotov6.ReadDataSourceRequest) (*tfprotov6.ReadDataSourceResponse, error) { + ds, ok := d[req.TypeName] + if !ok { + return nil, errUnsupportedDataSource(req.TypeName) + } + return ds.ReadDataSource(ctx, req) +} diff --git a/internal/echoprovider/echo_resource.go b/internal/echoprovider/echo_resource.go new file mode 100644 index 0000000..3ce7785 --- /dev/null +++ b/internal/echoprovider/echo_resource.go @@ -0,0 +1,137 @@ +package echoprovider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ tfprotov6.ResourceServer = echoResource{} + +type echoResource struct { + providerConfig *tfprotov6.DynamicValue +} + +var echoSchemaType = tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "data": tftypes.DynamicPseudoType, + }, +} + +func (e echoResource) ApplyResourceChange(ctx context.Context, req *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) { + plannedState, err := req.PlannedState.Unmarshal(echoSchemaType) + if err != nil { + return &tfprotov6.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Detail: fmt.Sprintf("error unmarhsaling planned state data: %s", err.Error()), + }, + }, + }, nil + } + + // Destroy Op, return the null from planned state + if plannedState.IsNull() { + return &tfprotov6.ApplyResourceChangeResponse{ + NewState: req.PlannedState, + }, nil + } + + if !plannedState.IsFullyKnown() { + return &tfprotov6.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "echo_resource encountered an unexpected unknown value, this resource is only meant to echo configuration from the provider config.", + }, + }, + }, nil + } + // Take the provider config verbatim and put back into state. It shares the same schema + // as the echo resource, so the data types/value should match up and there shouldn't be any + // unknown values present + return &tfprotov6.ApplyResourceChangeResponse{ + NewState: e.providerConfig, + }, nil +} + +func (e echoResource) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { + return &tfprotov6.ImportResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "import not supported", + }, + }, + }, nil +} + +func (e echoResource) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) { + return &tfprotov6.MoveResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Detail: "move state not supported", + }, + }, + }, nil +} + +func (e echoResource) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { + return &tfprotov6.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + }, nil +} + +func (e echoResource) ReadResource(ctx context.Context, req *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) { + return &tfprotov6.ReadResourceResponse{ + NewState: req.CurrentState, + }, nil +} + +func (e echoResource) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { + + rawStateValue, err := req.RawState.UnmarshalWithOpts( + echoSchemaType, + tfprotov6.UnmarshalOpts{ + ValueFromJSONOpts: tftypes.ValueFromJSONOpts{ + IgnoreUndefinedAttributes: true, + }, + }, + ) + + if err != nil { + return &tfprotov6.UpgradeResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Detail: fmt.Sprintf("error unmarhsaling raw state data: %s", err.Error()), + }, + }, + }, nil + } + + rawDynamicValue, err := tfprotov6.NewDynamicValue(rawStateValue.Type(), rawStateValue) + if err != nil { + return &tfprotov6.UpgradeResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Detail: fmt.Sprintf("error creating dynamic value from raw state data: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov6.UpgradeResourceStateResponse{ + UpgradedState: &rawDynamicValue, + Diagnostics: []*tfprotov6.Diagnostic{}, + }, nil +} + +func (e echoResource) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { + return &tfprotov6.ValidateResourceConfigResponse{}, nil +} diff --git a/internal/echoprovider/function_router.go b/internal/echoprovider/function_router.go new file mode 100644 index 0000000..3dc278e --- /dev/null +++ b/internal/echoprovider/function_router.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package echoprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +type errUnsupportedFunction string + +func (e errUnsupportedFunction) Error() string { + return "unsupported function: " + string(e) +} + +type functionRouter map[string]tfprotov6.FunctionServer + +func (f functionRouter) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) { + fu, ok := f[req.Name] + + if !ok { + return nil, errUnsupportedFunction(req.Name) + } + + return fu.CallFunction(ctx, req) +} + +func (f functionRouter) GetFunctions(ctx context.Context, req *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) { + panic("not implemented") +} diff --git a/internal/echoprovider/resource_router.go b/internal/echoprovider/resource_router.go new file mode 100644 index 0000000..e603d48 --- /dev/null +++ b/internal/echoprovider/resource_router.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package echoprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +type errUnsupportedResource string + +func (e errUnsupportedResource) Error() string { + return "unsupported resource: " + string(e) +} + +type resourceRouter map[string]tfprotov6.ResourceServer + +func (r resourceRouter) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { + res, ok := r[req.TypeName] + if !ok { + return nil, errUnsupportedResource(req.TypeName) + } + return res.ValidateResourceConfig(ctx, req) +} + +func (r resourceRouter) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { + res, ok := r[req.TypeName] + if !ok { + return nil, errUnsupportedResource(req.TypeName) + } + return res.UpgradeResourceState(ctx, req) +} + +func (r resourceRouter) ReadResource(ctx context.Context, req *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) { + res, ok := r[req.TypeName] + if !ok { + return nil, errUnsupportedResource(req.TypeName) + } + return res.ReadResource(ctx, req) +} + +func (r resourceRouter) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { + res, ok := r[req.TypeName] + if !ok { + return nil, errUnsupportedResource(req.TypeName) + } + return res.PlanResourceChange(ctx, req) +} + +func (r resourceRouter) ApplyResourceChange(ctx context.Context, req *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) { + res, ok := r[req.TypeName] + if !ok { + return nil, errUnsupportedResource(req.TypeName) + } + return res.ApplyResourceChange(ctx, req) +} + +func (r resourceRouter) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { + res, ok := r[req.TypeName] + if !ok { + return nil, errUnsupportedResource(req.TypeName) + } + return res.ImportResourceState(ctx, req) +} + +func (r resourceRouter) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) { + _, ok := r[req.TargetTypeName] + if !ok { + return nil, errUnsupportedResource(req.TargetTypeName) + } + // If this support ever needs to be added, this can follow the existing + // pattern of calling res.MoveResourceState(ctx, req). + return &tfprotov6.MoveResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unsupported Resource Operation", + Detail: "MoveResourceState is not supported by this provider.", + }, + }, + }, nil +} diff --git a/internal/echoprovider/server.go b/internal/echoprovider/server.go new file mode 100644 index 0000000..ade1169 --- /dev/null +++ b/internal/echoprovider/server.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package echoprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type server struct { + providerSchema *tfprotov6.Schema + providerMetaSchema *tfprotov6.Schema + resourceSchemas map[string]*tfprotov6.Schema + dataSourceSchemas map[string]*tfprotov6.Schema + functions map[string]*tfprotov6.Function + + resourceRouter + dataSourceRouter + functionRouter +} + +func (s *server) serverCapabilities() *tfprotov6.ServerCapabilities { + return &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + } +} + +func (s *server) GetMetadata(ctx context.Context, req *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) { + resp := &tfprotov6.GetMetadataResponse{ + DataSources: make([]tfprotov6.DataSourceMetadata, 0, len(s.dataSourceSchemas)), + Resources: make([]tfprotov6.ResourceMetadata, 0, len(s.resourceSchemas)), + ServerCapabilities: s.serverCapabilities(), + } + + for typeName := range s.dataSourceSchemas { + resp.DataSources = append(resp.DataSources, tfprotov6.DataSourceMetadata{ + TypeName: typeName, + }) + } + + for typeName := range s.resourceSchemas { + resp.Resources = append(resp.Resources, tfprotov6.ResourceMetadata{ + TypeName: typeName, + }) + } + + return resp, nil +} + +func (s *server) GetProviderSchema(ctx context.Context, req *tfprotov6.GetProviderSchemaRequest) (*tfprotov6.GetProviderSchemaResponse, error) { + return &tfprotov6.GetProviderSchemaResponse{ + Provider: s.providerSchema, + ProviderMeta: s.providerMetaSchema, + ResourceSchemas: s.resourceSchemas, + DataSourceSchemas: s.dataSourceSchemas, + ServerCapabilities: s.serverCapabilities(), + Functions: s.functions, + }, nil +} + +func (s *server) ValidateProviderConfig(ctx context.Context, req *tfprotov6.ValidateProviderConfigRequest) (*tfprotov6.ValidateProviderConfigResponse, error) { + return &tfprotov6.ValidateProviderConfigResponse{ + PreparedConfig: req.Config, + }, nil +} + +func (s *server) ConfigureProvider(ctx context.Context, req *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) { + // Quick hack to save the ephemeral data from the config for later + s.resourceRouter["echo_resource"] = echoResource{ + providerConfig: req.Config, + } + + return &tfprotov6.ConfigureProviderResponse{}, nil +} + +func (s *server) StopProvider(ctx context.Context, req *tfprotov6.StopProviderRequest) (*tfprotov6.StopProviderResponse, error) { + return &tfprotov6.StopProviderResponse{}, nil +} + +func NewServer() func() (tfprotov6.ProviderServer, error) { + return func() (tfprotov6.ProviderServer, error) { + return &server{ + // Both provider config + echo_resource have the same schema + providerSchema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "data", + Type: tftypes.DynamicPseudoType, + DescriptionKind: tfprotov6.StringKindPlain, + Required: true, + }, + }, + }, + }, + resourceSchemas: map[string]*tfprotov6.Schema{ + // Both provider config + echo_resource have the same schema + "echo_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "data", + Type: tftypes.DynamicPseudoType, + DescriptionKind: tfprotov6.StringKindPlain, + Computed: true, + }, + }, + }, + }, + }, + resourceRouter: resourceRouter{ + "echo_resource": echoResource{}, + }, + }, nil + } +}