From 47c48a3b89a8b89af99cedb3257e4bc4bb97bb14 Mon Sep 17 00:00:00 2001 From: Tomas Karasek Date: Tue, 10 Oct 2023 15:03:59 +0200 Subject: [PATCH] Framework provider stub and mux functionality --- equinix/framework_provider.go | 92 +++++++++++++ equinix/helper/framework_datasource_base.go | 73 ++++++++++ equinix/helper/framework_provider_model.go | 23 ++++ equinix/helper/framework_resource_base.go | 132 +++++++++++++++++++ equinix/helper/resource_datasource_config.go | 48 +++++++ equinix/metal_ssh_key/datasource.go | 61 +++++++++ equinix/metal_ssh_key/resource.go | 118 +++++++++++++++++ go.mod | 4 +- go.sum | 4 + main.go | 42 ++++-- 10 files changed, 588 insertions(+), 9 deletions(-) create mode 100644 equinix/framework_provider.go create mode 100644 equinix/helper/framework_datasource_base.go create mode 100644 equinix/helper/framework_provider_model.go create mode 100644 equinix/helper/framework_resource_base.go create mode 100644 equinix/helper/resource_datasource_config.go create mode 100644 equinix/metal_ssh_key/datasource.go create mode 100644 equinix/metal_ssh_key/resource.go diff --git a/equinix/framework_provider.go b/equinix/framework_provider.go new file mode 100644 index 000000000..256d95266 --- /dev/null +++ b/equinix/framework_provider.go @@ -0,0 +1,92 @@ +package equinix + +import ( + "context" + "fmt" + + "github.com/equinix/terraform-provider-equinix/equinix/helper" + "github.com/equinix/terraform-provider-equinix/equinix/metal_ssh_key" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +type FrameworkProvider struct { + ProviderVersion string + Meta *helper.FrameworkProviderMeta +} + +func CreateFrameworkProvider(version string) provider.Provider { + return &FrameworkProvider{ + ProviderVersion: version, + } +} + +func (p *FrameworkProvider) Metadata( + ctx context.Context, + req provider.MetadataRequest, + resp *provider.MetadataResponse, +) { + resp.TypeName = "equinixcloud" +} + +func (p *FrameworkProvider) Schema( + ctx context.Context, + req provider.SchemaRequest, + resp *provider.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "endpoint": schema.StringAttribute{ + Optional: true, + Description: "The Equinix API base URL to point out desired environment. Defaults to " + DefaultBaseURL, + }, + "client_id": schema.StringAttribute{ + Optional: true, + Description: "API Consumer Key available under My Apps section in developer portal", + }, + "client_secret": schema.StringAttribute{ + Optional: true, + Description: "API Consumer secret available under My Apps section in developer portal", + }, + "token": schema.StringAttribute{ + Optional: true, + Description: "API token from the developer sandbox", + }, + "auth_token": schema.StringAttribute{ + Optional: true, + Description: "The Equinix Metal API auth key for API operations", + }, + "request_timeout": schema.Int64Attribute{ + Optional: true, + Description: "The duration of time, in seconds, that the Equinix Platform API Client should wait before canceling an API request. Defaults to " + fmt.Sprint(DefaultTimeout), + }, + "response_max_page_size": schema.Int64Attribute{ + Optional: true, + Description: "The maximum number of records in a single response for REST queries that produce paginated responses", + }, + "max_retries": schema.Int64Attribute{ + Optional: true, + Description: "Maximum number of retries.", + }, + "max_retry_wait_seconds": schema.Int64Attribute{ + Optional: true, + Description: "Maximum number of seconds to wait before retrying a request.", + }, + }, + } +} + +func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + metal_ssh_key.NewResource, + } +} + +func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource.DataSource { + // return nil + return []func() datasource.DataSource{ + metal_ssh_key.NewDataSource, + } +} diff --git a/equinix/helper/framework_datasource_base.go b/equinix/helper/framework_datasource_base.go new file mode 100644 index 000000000..8d38d66bf --- /dev/null +++ b/equinix/helper/framework_datasource_base.go @@ -0,0 +1,73 @@ +package helper + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" +) + +// NewBaseDataSource returns a new instance of the BaseDataSource +// struct for cleaner initialization. +func NewBaseDataSource(cfg BaseDataSourceConfig) BaseDataSource { + return BaseDataSource{ + Config: cfg, + } +} + +// BaseDataSourceConfig contains all configurable base resource fields. +type BaseDataSourceConfig struct { + Name string + + // Optional + Schema *schema.Schema + IsEarlyAccess bool +} + +// BaseDataSource contains various re-usable fields and methods +// intended for use in data source implementations by composition. +type BaseDataSource struct { + Config BaseDataSourceConfig + Meta *FrameworkProviderMeta +} + +func (r *BaseDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + r.Meta = GetDataSourceMeta(req, resp) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *BaseDataSource) Metadata( + ctx context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { + resp.TypeName = r.Config.Name +} + +func (r *BaseDataSource) Schema( + ctx context.Context, + req datasource.SchemaRequest, + resp *datasource.SchemaResponse, +) { + if r.Config.Schema == nil { + resp.Diagnostics.AddError( + "Missing Schema", + "Base data source was not provided a schema. "+ + "Please provide a Schema config attribute or implement, the Schema(...) function.", + ) + return + } + + resp.Schema = *r.Config.Schema +} diff --git a/equinix/helper/framework_provider_model.go b/equinix/helper/framework_provider_model.go new file mode 100644 index 000000000..fb1eaa73e --- /dev/null +++ b/equinix/helper/framework_provider_model.go @@ -0,0 +1,23 @@ +package helper + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/packethost/packngo" +) + +type FrameworkProviderModel struct { + Endpoint types.String `tfsdk:"endpoint,omitempty"` + ClientID types.String `tfsdk:"client_id,omitempty"` + ClientSecret types.String `tfsdk:"client_secret,omitempty"` + Token types.String `tfsdk:"token,omitempty"` + AuthToken types.String `tfsdk:"auth_token,omitempty"` + RequestTimeout types.Int64 `tfsdk:"request_timeout,omitempty"` + ResponseMaxPageSize types.Int64 `tfsdk:"response_max_page_size,omitempty"` + MaxRetries types.Int64 `tfsdk:"max_retries,omitempty"` + MaxRetryWaitSeconds types.Int64 `tfsdk:"max_retry_wait_seconds,omitempty"` +} + +type FrameworkProviderMeta struct { + Client *packngo.Client + Config *FrameworkProviderModel +} diff --git a/equinix/helper/framework_resource_base.go b/equinix/helper/framework_resource_base.go new file mode 100644 index 000000000..6dc95148a --- /dev/null +++ b/equinix/helper/framework_resource_base.go @@ -0,0 +1,132 @@ +package helper + +import ( + "context" + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// NewBaseResource returns a new instance of the BaseResource +// struct for cleaner initialization. +func NewBaseResource(cfg BaseResourceConfig) BaseResource { + return BaseResource{ + Config: cfg, + } +} + +// BaseResourceConfig contains all configurable base resource fields. +type BaseResourceConfig struct { + Name string + IDAttr string + IDType attr.Type + + // Optional + Schema *schema.Schema + IsEarlyAccess bool +} + +// BaseResource contains various re-usable fields and methods +// intended for use in resource implementations by composition. +type BaseResource struct { + Config BaseResourceConfig + Meta *FrameworkProviderMeta +} + +func (r *BaseResource) Configure( + ctx context.Context, + req resource.ConfigureRequest, + resp *resource.ConfigureResponse, +) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + r.Meta = GetResourceMeta(req, resp) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *BaseResource) Metadata( + ctx context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { + resp.TypeName = r.Config.Name +} + +func (r *BaseResource) Schema( + ctx context.Context, + req resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + if r.Config.Schema == nil { + resp.Diagnostics.AddError( + "Missing Schema", + "Base resource was not provided a schema. "+ + "Please provide a Schema config attribute or implement, the Schema(...) function.", + ) + return + } + + resp.Schema = *r.Config.Schema +} + +// ImportState should be overridden for resources with +// complex read logic (e.g. parent ID). +func (r *BaseResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + // Enforce defaults + idAttr := r.Config.IDAttr + if idAttr == "" { + idAttr = "id" + } + + idType := r.Config.IDType + if idType == nil { + idType = types.Int64Type + } + + attrPath := path.Root(idAttr) + + if attrPath.Equal(path.Empty()) { + resp.Diagnostics.AddError( + "Resource Import Passthrough Missing Attribute Path", + "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Resource ImportState path must be set to a valid attribute path.", + ) + return + } + + // Handle type conversion + var err error + var idValue any + + switch idType { + case types.Int64Type: + idValue, err = strconv.ParseInt(req.ID, 10, 64) + case types.StringType: + idValue = req.ID + default: + err = fmt.Errorf("unsupported id attribute type: %v", idType) + } + if err != nil { + resp.Diagnostics.AddError( + "Failed to convert ID attribute", + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, attrPath, idValue)...) +} diff --git a/equinix/helper/resource_datasource_config.go b/equinix/helper/resource_datasource_config.go new file mode 100644 index 000000000..bf97a0c2d --- /dev/null +++ b/equinix/helper/resource_datasource_config.go @@ -0,0 +1,48 @@ +package helper + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func GetResourceMeta( + req resource.ConfigureRequest, + resp *resource.ConfigureResponse, +) *FrameworkProviderMeta { + meta, ok := req.ProviderData.(*FrameworkProviderMeta) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf( + "Expected *http.Client, got: %T. Please report this issue to the provider developers.", + req.ProviderData, + ), + ) + return nil + } + + return meta +} + +func GetDataSourceMeta( + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) *FrameworkProviderMeta { + meta, ok := req.ProviderData.(*FrameworkProviderMeta) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected DataSource Configure Type", + fmt.Sprintf( + "Expected *http.Client, got: %T. Please report this issue to the provider developers.", + req.ProviderData, + ), + ) + return nil + } + + return meta +} diff --git a/equinix/metal_ssh_key/datasource.go b/equinix/metal_ssh_key/datasource.go new file mode 100644 index 000000000..8514efa5a --- /dev/null +++ b/equinix/metal_ssh_key/datasource.go @@ -0,0 +1,61 @@ +package metal_ssh_key + +import ( + "context" + + "github.com/equinix/terraform-provider-equinix/equinix/helper" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" +) + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "equinix_metal_ssh_key", + // do we have other than str id types? + Schema: &frameworkDataSourceSchema, + }, + ), + } +} + +type DataSource struct { + helper.BaseDataSource +} + +func (r *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { +} + +var frameworkDataSourceSchema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the SSH key for identification", + Required: true, + }, + "public_key": schema.StringAttribute{ + Description: "The public key", + Required: true, + }, + "fingerprint": schema.StringAttribute{ + Description: "The fingerprint of the SSH key", + Computed: true, + }, + "owner_id": schema.StringAttribute{ + Description: "The UUID of the Equinix Metal API User who owns this key", + Computed: true, + }, + "created": schema.StringAttribute{ + Description: "The timestamp for when the SSH key was created", + Computed: true, + }, + "updated": schema.StringAttribute{ + Description: "The timestamp for the last time the SSH key was updated", + Computed: true, + }, + }, +} diff --git a/equinix/metal_ssh_key/resource.go b/equinix/metal_ssh_key/resource.go new file mode 100644 index 000000000..0dbcf212d --- /dev/null +++ b/equinix/metal_ssh_key/resource.go @@ -0,0 +1,118 @@ +package metal_ssh_key + +import ( + "path" + + "context" + + "github.com/equinix/terraform-provider-equinix/equinix/helper" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/packethost/packngo" +) + +type ResourceModel struct { + ID types.String `tfsdk:"id,omitempty"` + Name types.String `tfsdk:"name,omitempty"` + PublicKey types.String `tfsdk:"public_key,omitempty"` + ProjectID types.String `tfsdk:"project_id,omitempty"` + Fingerprint types.String `tfsdk:"fingerprint,omitempty"` + Updated types.String `tfsdk:"updated,omitempty"` + OwnerID types.String `tfsdk:"owner_id,omitempty"` +} + +func (rm *ResourceModel) parse(key *packngo.SSHKey) { + rm.ID = types.StringValue(key.ID) + rm.Name = types.StringValue(key.Label) + rm.PublicKey = types.StringValue(key.Key) + rm.ProjectID = types.StringValue(path.Base(key.Owner.Href)) + rm.Fingerprint = types.StringValue(key.FingerPrint) + rm.Updated = types.StringValue(key.Updated) + rm.OwnerID = types.StringValue(path.Base(key.Owner.Href)) +} + +func NewResource() resource.Resource { + return &Resource{ + BaseResource: helper.NewBaseResource( + helper.BaseResourceConfig{ + Name: "equinix_metal_ssh_key", + // do we have other than str id types? + IDType: types.StringType, + Schema: &frameworkResourceSchema, + }, + ), + } +} + +type Resource struct { + helper.BaseResource +} + +func (r *Resource) Create( + ctx context.Context, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { +} + +func (r *Resource) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { +} + +func (r *Resource) Update( + ctx context.Context, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { +} + +func (r *Resource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { +} + +var frameworkResourceSchema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the SSH key for identification", + Required: true, + }, + "public_key": schema.StringAttribute{ + Description: "The public key", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "fingerprint": schema.StringAttribute{ + Description: "The fingerprint of the SSH key", + Computed: true, + }, + "owner_id": schema.StringAttribute{ + Description: "The UUID of the Equinix Metal API User who owns this key", + Computed: true, + }, + "created": schema.StringAttribute{ + Description: "The timestamp for when the SSH key was created", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "updated": schema.StringAttribute{ + Description: "The timestamp for the last time the SSH key was updated", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, +} diff --git a/go.mod b/go.mod index 705f911d8..9d5dd5230 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,9 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-retryablehttp v0.7.4 github.com/hashicorp/terraform-plugin-docs v0.14.1 + github.com/hashicorp/terraform-plugin-framework v1.4.1 + github.com/hashicorp/terraform-plugin-go v0.19.0 + github.com/hashicorp/terraform-plugin-mux v0.12.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 github.com/packethost/packngo v0.30.0 github.com/pkg/errors v0.9.1 @@ -64,7 +67,6 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.19.0 // indirect github.com/hashicorp/terraform-json v0.17.1 // indirect - github.com/hashicorp/terraform-plugin-go v0.19.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.2 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect diff --git a/go.sum b/go.sum index aa30a9a5a..97c150cdf 100644 --- a/go.sum +++ b/go.sum @@ -431,10 +431,14 @@ github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQH github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= github.com/hashicorp/terraform-plugin-docs v0.14.1 h1:MikFi59KxrP/ewrZoaowrB9he5Vu4FtvhamZFustiA4= github.com/hashicorp/terraform-plugin-docs v0.14.1/go.mod h1:k2NW8+t113jAus6bb5tQYQgEAX/KueE/u8X2Z45V1GM= +github.com/hashicorp/terraform-plugin-framework v1.4.1 h1:ZC29MoB3Nbov6axHdgPbMz7799pT5H8kIrM8YAsaVrs= +github.com/hashicorp/terraform-plugin-framework v1.4.1/go.mod h1:XC0hPcQbBvlbxwmjxuV/8sn8SbZRg4XwGMs22f+kqV0= github.com/hashicorp/terraform-plugin-go v0.19.0 h1:BuZx/6Cp+lkmiG0cOBk6Zps0Cb2tmqQpDM3iAtnhDQU= github.com/hashicorp/terraform-plugin-go v0.19.0/go.mod h1:EhRSkEPNoylLQntYsk5KrDHTZJh9HQoumZXbOGOXmec= 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-plugin-mux v0.12.0 h1:TJlmeslQ11WlQtIFAfth0vXx+gSNgvMEng2Rn9z3WZY= +github.com/hashicorp/terraform-plugin-mux v0.12.0/go.mod h1:8MR0AgmV+Q03DIjyrAKxXyYlq2EUnYBQP8gxAAA0zeM= github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 h1:wcOKYwPI9IorAJEBLzgclh3xVolO7ZorYd6U1vnok14= github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0/go.mod h1:qH/34G25Ugdj5FcM95cSoXzUgIbgfhVLXCcEcYaMwq8= github.com/hashicorp/terraform-registry-address v0.2.2 h1:lPQBg403El8PPicg/qONZJDC6YlgCVbWDtNmmZKtBno= diff --git a/main.go b/main.go index e80a0b96a..9d46c4983 100644 --- a/main.go +++ b/main.go @@ -6,24 +6,50 @@ import ( "log" "github.com/equinix/terraform-provider-equinix/equinix" - "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" + "github.com/equinix/terraform-provider-equinix/version" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" ) //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs func main() { + + ctx := context.Background() + var debugMode bool flag.BoolVar(&debugMode, "debug", false, "set to true to run the provider with support for debuggers like delve") flag.Parse() - opts := &plugin.ServeOpts{ProviderFunc: equinix.Provider} + + providers := []func() tfprotov5.ProviderServer{ + providerserver.NewProtocol5( + equinix.CreateFrameworkProvider(version.ProviderVersion)), + equinix.Provider().GRPCProvider, + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, providers...) + if err != nil { + log.Fatal(err) + } + + var serveOpts []tf5server.ServeOpt if debugMode { - err := plugin.Debug(context.Background(), "registry.terraform.io/equinix/equinix", opts) - if err != nil { - log.Fatal(err.Error()) - } - return + serveOpts = append(serveOpts, tf5server.WithManagedDebug()) + } + + err = tf5server.Serve( + "registry.terraform.io/equnix/equinix", + muxServer.ProviderServer, + serveOpts..., + ) + + if err != nil { + log.Fatal(err) } - plugin.Serve(opts) + //plugin.Serve(opts) }