From 588f3ca9a3dbca3f87cae23741a7fbe536b04221 Mon Sep 17 00:00:00 2001 From: Pauline Espalieu <pauline.espalieu@intercloud.com> Date: Wed, 18 Sep 2024 16:20:15 +0200 Subject: [PATCH] Feat/iopr 4336 Add resource virtual_access_node (#37) * resource virtual access node: wip * resource virtual access node --------- Co-authored-by: Pauline ESPALIEU <paulineespalieu@MacBook-Pro-Pauline-Espalieu.local> --- docs/resources/access_node.md | 5 +- docs/resources/virtual_access_node.md | 64 ++++ .../autonomi_virtual_access_node/resource.tf | 7 + go.mod | 2 +- go.sum | 4 +- internal/provider/provider.go | 1 + internal/resources/access_node_resource.go | 2 +- .../resources/virtual_access_node_resource.go | 362 ++++++++++++++++++ 8 files changed, 440 insertions(+), 7 deletions(-) create mode 100644 docs/resources/virtual_access_node.md create mode 100644 examples/resources/autonomi_virtual_access_node/resource.tf create mode 100644 internal/resources/virtual_access_node_resource.go diff --git a/docs/resources/access_node.md b/docs/resources/access_node.md index 6fd919b..832746f 100644 --- a/docs/resources/access_node.md +++ b/docs/resources/access_node.md @@ -5,15 +5,14 @@ subcategory: "" description: |- Manages an access node resource. Access node resource allows you to create, modify and delete Autonomi access nodes. - Autonomi access node allows you to easily connect to your datacenters assets via a physical connection (physical access node) or a virtual connection through Megaport / Equinix connections (virtual access nodes). + Autonomi access node allows you to easily connect to your datacenters assets via a physical connection (physical access node). --- # autonomi_access_node (Resource) Manages an access node resource. Access node resource allows you to create, modify and delete Autonomi access nodes. -Autonomi access node allows you to easily connect to your datacenters assets via a physical connection -(physical access node) or a virtual connection through Megaport / Equinix connections (virtual access nodes). +Autonomi access node allows you to easily connect to your datacenters assets via a physical connection (physical access node). ## Example Usage diff --git a/docs/resources/virtual_access_node.md b/docs/resources/virtual_access_node.md new file mode 100644 index 0000000..923ce88 --- /dev/null +++ b/docs/resources/virtual_access_node.md @@ -0,0 +1,64 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "autonomi_virtual_access_node Resource - autonomi" +subcategory: "" +description: |- + Manages a virtual access node resource. + Virtual access node resource allows you to create, modify and delete Autonomi virtual access nodes. + Autonomi virtual access node allows you to easily connect to your datacenters assets via a virtual connection through Megaport / Equinix connections (virtual access nodes). +--- + +# autonomi_virtual_access_node (Resource) + +Manages a virtual access node resource. +Virtual access node resource allows you to create, modify and delete Autonomi virtual access nodes. +Autonomi virtual access node allows you to easily connect to your datacenters assets via a virtual connection through Megaport / Equinix connections (virtual access nodes). + +## Example Usage + +```terraform +resource "autonomi_virtual_access_node" "virtual_access_node" { + name = "Virtual Access Node created with Terraform" + workspace_id = autonomi_workspace.workspace.id + product = { + sku = "valid_sku" + } +} +``` + +<!-- schema generated by tfplugindocs --> +## Schema + +### Required + +- `name` (String) Name of the access node +- `product` (Attributes) (see [below for nested schema](#nestedatt--product)) +- `workspace_id` (String) ID of the workspace to which the access node belongs. + +### Read-Only + +- `administrative_state` (String) Administrative state of the access node [creation_pending, creation_proceed, creation_error, +deployed, delete_pending, delete_proceed, delete_error] +- `created_at` (String) Creation date of the access node +- `deployed_at` (String) Deployment date of the access node +- `id` (String) ID of the access node, set after creation +- `service_key` (Attributes) Access node's service key (see [below for nested schema](#nestedatt--service_key)) +- `type` (String) Type of the node [access] +- `updated_at` (String) Update date of the access node +- `vlan` (Number) Vlan of the access node + +<a id="nestedatt--product"></a> +### Nested Schema for `product` + +Required: + +- `sku` (String) ID of the product + +<a id="nestedatt--service_key"></a> +### Nested Schema for `service_key` + +Read-Only: + +- `expiration_date` (String) expiration date of the service key +- `id` (String) ID of the service key +- `name` (String) name of the service key diff --git a/examples/resources/autonomi_virtual_access_node/resource.tf b/examples/resources/autonomi_virtual_access_node/resource.tf new file mode 100644 index 0000000..0edc15c --- /dev/null +++ b/examples/resources/autonomi_virtual_access_node/resource.tf @@ -0,0 +1,7 @@ +resource "autonomi_virtual_access_node" "virtual_access_node" { + name = "Virtual Access Node created with Terraform" + workspace_id = autonomi_workspace.workspace.id + product = { + sku = "valid_sku" + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 27b21a4..e6e7d67 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/terraform-plugin-framework v1.10.0 github.com/hashicorp/terraform-plugin-go v0.23.0 github.com/hashicorp/terraform-plugin-testing v1.9.0 - github.com/intercloud/autonomi-sdk v0.0.14 + github.com/intercloud/autonomi-sdk v1.0.1 github.com/meilisearch/meilisearch-go v0.27.2 github.com/stretchr/testify v1.9.0 ) diff --git a/go.sum b/go.sum index 9dd4b95..43c41c9 100644 --- a/go.sum +++ b/go.sum @@ -125,8 +125,8 @@ github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/intercloud/autonomi-sdk v0.0.14 h1:xwCwN+4vHop7R5j4VVB0kLpezoo1uXoKeDJe5GdYrSo= -github.com/intercloud/autonomi-sdk v0.0.14/go.mod h1:O0r2AhNEPkT5bsDByewDbpc6rojBNJG5EpGJpfp+Kxw= +github.com/intercloud/autonomi-sdk v1.0.1 h1:Ryr/w+VEfLquO53TD5cy2o5PCkChdNQJ1uM4H2C4cjU= +github.com/intercloud/autonomi-sdk v1.0.1/go.mod h1:O0r2AhNEPkT5bsDByewDbpc6rojBNJG5EpGJpfp+Kxw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c969b8b..b8c411d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -218,6 +218,7 @@ func (p *autonomiProvider) Resources(_ context.Context) []func() resource.Resour autonomiresource.NewWorkspaceResource, autonomiresource.NewCloudNodeResource, autonomiresource.NewAccessNodeResource, + autonomiresource.NewVirtualAccessNodeResource, autonomiresource.NewTransportResource, autonomiresource.NewAttachmentResource, autonomiresource.NewPhysicalPortResource, diff --git a/internal/resources/access_node_resource.go b/internal/resources/access_node_resource.go index 1ba8f41..059d8cb 100644 --- a/internal/resources/access_node_resource.go +++ b/internal/resources/access_node_resource.go @@ -76,7 +76,7 @@ func (r *accessNodeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp.Schema = schema.Schema{ MarkdownDescription: `Manages an access node resource. Access node resource allows you to create, modify and delete Autonomi access nodes. -Autonomi access node allows you to easily connect to your datacenters assets via a physical connection (physical access node) or a virtual connection through Megaport / Equinix connections (virtual access nodes).`, +Autonomi access node allows you to easily connect to your datacenters assets via a physical connection (physical access node).`, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ MarkdownDescription: "ID of the access node, set after creation", diff --git a/internal/resources/virtual_access_node_resource.go b/internal/resources/virtual_access_node_resource.go new file mode 100644 index 0000000..cde5abb --- /dev/null +++ b/internal/resources/virtual_access_node_resource.go @@ -0,0 +1,362 @@ +package autonomiresource + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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" + autonomisdk "github.com/intercloud/autonomi-sdk" + "github.com/intercloud/autonomi-sdk/models" +) + +// virtualAccessNodeResource is the resource implementation. +type virtualAccessNodeResource struct { + client *autonomisdk.Client +} + +var serviceKey = map[string]attr.Type{ + "id": types.StringType, + "name": types.StringType, + "expiration_date": types.StringType, +} + +type virtualAccessNodeResourceModel struct { + ID types.String `tfsdk:"id"` + WorkspaceID types.String `tfsdk:"workspace_id"` + CreatedAt types.String `tfsdk:"created_at"` + UpdatedAt types.String `tfsdk:"updated_at"` + DeployedAt types.String `tfsdk:"deployed_at"` + Name types.String `tfsdk:"name"` + State types.String `tfsdk:"administrative_state"` + Type types.String `tfsdk:"type"` + Product product `tfsdk:"product"` + Vlan types.Int64 `tfsdk:"vlan"` + ServiceKey types.Object `tfsdk:"service_key"` +} + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &virtualAccessNodeResource{} + _ resource.ResourceWithConfigure = &virtualAccessNodeResource{} +) + +// NewAccessNodeResource is a helper function to simplify the provider implementation. +func NewVirtualAccessNodeResource() resource.Resource { + return &virtualAccessNodeResource{} +} + +// Configure adds the provider configured client to the resource. +func (r *virtualAccessNodeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*autonomisdk.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *autonomi.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +// Metadata returns the resource type name. +func (r *virtualAccessNodeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_virtual_access_node" +} + +// Schema defines the schema for the resource. +func (r *virtualAccessNodeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: `Manages a virtual access node resource. +Virtual access node resource allows you to create, modify and delete Autonomi virtual access nodes. +Autonomi virtual access node allows you to easily connect to your datacenters assets via a virtual connection through Megaport / Equinix connections (virtual access nodes).`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "ID of the access node, set after creation", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "created_at": schema.StringAttribute{ + MarkdownDescription: "Creation date of the access node", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + MarkdownDescription: "Update date of the access node", + Computed: true, + }, + "deployed_at": schema.StringAttribute{ + MarkdownDescription: "Deployment date of the access node", + Computed: true, + }, + "workspace_id": schema.StringAttribute{ + MarkdownDescription: "ID of the workspace to which the access node belongs.", + Required: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the access node", + Required: true, + }, + "administrative_state": schema.StringAttribute{ + MarkdownDescription: `Administrative state of the access node [creation_pending, creation_proceed, creation_error, +deployed, delete_pending, delete_proceed, delete_error]`, + Computed: true, + }, + "product": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "sku": schema.StringAttribute{ + MarkdownDescription: "ID of the product", + Required: true, + }, + }, + }, + "vlan": schema.Int64Attribute{ + MarkdownDescription: "Vlan of the access node", + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "type": schema.StringAttribute{ + MarkdownDescription: "Type of the node [access]", + Computed: true, + }, + "service_key": schema.SingleNestedAttribute{ + MarkdownDescription: "Access node's service key", + Computed: true, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "ID of the service key", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "name of the service key", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "expiration_date": schema.StringAttribute{ + MarkdownDescription: "expiration date of the service key", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *virtualAccessNodeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan virtualAccessNodeResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from plan + payload := models.CreateNode{ + Name: plan.Name.ValueString(), + Type: models.NodeTypeAccess, + Product: models.AddProduct{ + SKU: plan.Product.SKU.ValueString(), + }, + } + + // Create new node + node, err := r.client.CreateNode(ctx, payload, plan.WorkspaceID.ValueString(), autonomisdk.WithWaitUntilElementDeployed()) + if err != nil { + resp.Diagnostics.AddError( + "Error creating node", + "Could not create node, unexpected error: "+err.Error(), + ) + return + } + + // Map response body to schema and populate Computed attribute values + plan.ID = types.StringValue(node.ID.String()) + plan.State = types.StringValue(node.State.String()) + plan.Type = types.StringValue(node.Type.String()) + plan.CreatedAt = types.StringValue(node.CreatedAt.String()) + plan.UpdatedAt = types.StringValue(node.UpdatedAt.String()) + plan.DeployedAt = types.StringValue(node.DeployedAt.String()) + plan.Vlan = types.Int64Value(node.Vlan) + // set serviceKey object + serviceKeyObject, diag := types.ObjectValue( + serviceKey, + map[string]attr.Value{ + "id": types.StringValue(node.ServiceKey.ID), + "name": types.StringValue(node.ServiceKey.Name), + "expiration_date": types.StringValue(node.ServiceKey.ExpirationDate.String()), + }, + ) + // Check for errors + if diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + plan.ServiceKey = serviceKeyObject + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *virtualAccessNodeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state virtualAccessNodeResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get refreshed node value from Autonomi + node, err := r.client.GetNode(ctx, state.WorkspaceID.ValueString(), state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error Reading Autonomi virtual access node", + "Could not read Autonomi virtual access node ID "+state.ID.ValueString()+": "+err.Error(), + ) + return + } + + // Overwrite items with refreshed state + state.ID = types.StringValue(node.ID.String()) + state.CreatedAt = types.StringValue(node.CreatedAt.String()) + state.UpdatedAt = types.StringValue(node.UpdatedAt.String()) + state.DeployedAt = types.StringValue(node.DeployedAt.String()) + state.Name = types.StringValue(node.Name) + state.State = types.StringValue(node.State.String()) + state.Type = types.StringValue(node.Type.String()) + state.Product = product{ + SKU: types.StringValue(node.Product.SKU), + } + state.Vlan = types.Int64Value(node.Vlan) + serviceKeyObject, diag := types.ObjectValue( + serviceKey, + map[string]attr.Value{ + "id": types.StringValue(node.ServiceKey.ID), + "name": types.StringValue(node.ServiceKey.Name), + "expiration_date": types.StringValue(node.ServiceKey.ExpirationDate.String()), + }, + ) + // Check for errors + if diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + state.ServiceKey = serviceKeyObject + + // Set refreshed state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *virtualAccessNodeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan virtualAccessNodeResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from plan + payload := models.UpdateElement{ + Name: plan.Name.ValueString(), + } + + // Update existing access node + node, err := r.client.UpdateNode(ctx, payload, plan.WorkspaceID.ValueString(), plan.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error Updating Access Node", + fmt.Sprintf("Could not update Autonomi access node: "+plan.ID.ValueString())+": error: "+err.Error(), + ) + return + } + + // Update resource state with updated items and timestamp + plan.ID = types.StringValue(node.ID.String()) + plan.State = types.StringValue(node.State.String()) + plan.Type = types.StringValue(node.Type.String()) + plan.CreatedAt = types.StringValue(node.CreatedAt.String()) + plan.UpdatedAt = types.StringValue(node.UpdatedAt.String()) + plan.DeployedAt = types.StringValue(node.DeployedAt.String()) + plan.Vlan = types.Int64Value(node.Vlan) + serviceKeyObject, diag := types.ObjectValue( + serviceKey, + map[string]attr.Value{ + "id": types.StringValue(node.ServiceKey.ID), + "name": types.StringValue(node.ServiceKey.Name), + "expiration_date": types.StringValue(node.ServiceKey.ExpirationDate.String()), + }, + ) + // Check for errors + if diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + plan.ServiceKey = serviceKeyObject + plan.Product = product{ + SKU: types.StringValue(node.Product.SKU), + } + plan.Vlan = types.Int64Value(node.Vlan) + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *virtualAccessNodeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state virtualAccessNodeResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Delete existing node + _, err := r.client.DeleteNode(ctx, state.WorkspaceID.ValueString(), state.ID.ValueString(), autonomisdk.WithWaitUntilElementUndeployed()) + if err != nil { + resp.Diagnostics.AddError( + "Error Deleting node", + "Could not delete node, unexpected error: "+err.Error(), + ) + return + } +}