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
+	}
+}