From 456d32bca9e9c522161a5c92fa9834e8ebb9df5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Oct 2023 18:51:18 -0400 Subject: [PATCH] Created server build --- docs/resources/build.md | 59 ++++ docs/resources/server.md | 1 - .../resources/virtfusion_build/resource.tf | 26 ++ internal/provider/provider.go | 1 + .../virtfusion_server_build_resource.go | 285 ++++++++++++++++++ .../provider/virtfusion_server_resource.go | 54 ++-- 6 files changed, 388 insertions(+), 38 deletions(-) create mode 100644 docs/resources/build.md create mode 100644 examples/resources/virtfusion_build/resource.tf create mode 100644 internal/provider/virtfusion_server_build_resource.go diff --git a/docs/resources/build.md b/docs/resources/build.md new file mode 100644 index 0000000..d93c19a --- /dev/null +++ b/docs/resources/build.md @@ -0,0 +1,59 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "virtfusion_build Resource - terraform-provider-virtfusion" +subcategory: "" +description: |- + Virtfusion Server Build Resource +--- + +# virtfusion_build (Resource) + +Virtfusion Server Build Resource + +## Example Usage + +```terraform +resource "virtfusion_server" "node1" { + package_id = 1 + user_id = 1 + hypervisor_id = 1 + ipv4 = 1 + storage = 30 + memory = 1024 + cores = 1 + traffic = 1000 + inbound_network_speed = 100 + outbound_network_speed = 100 + storage_profile = 1 + network_profile = 1 +} + + +resource "virtfusion_build" "node1" { + server_id = virtfusion_server.node1.id + name = "node1-demo" + hostname = "node1.example.com" + osid = 1 + vnc = true + ipv6 = true + ssh_keys = [1, 2, 3] + email = true +} +``` + + +## Schema + +### Required + +- `name` (String) Server Name +- `osid` (Number) Server Operating System ID +- `server_id` (Number) Server ID + +### Optional + +- `email` (Boolean) Server Email +- `hostname` (String) Server Hostname +- `ipv6` (Boolean) Server IPv6 +- `ssh_keys` (List of Number) Server SSH Keys IDs +- `vnc` (Boolean) Server VNC diff --git a/docs/resources/server.md b/docs/resources/server.md index 4817cae..dfc94f9 100644 --- a/docs/resources/server.md +++ b/docs/resources/server.md @@ -45,7 +45,6 @@ resource "virtfusion_server" "node1" { - `inbound_network_speed` (Number) Inbound network speed in kB/s. Omit to use the default inbound network speed from the package. - `ipv4` (Number) IPv4 Addresses to assign. Omit to use the default of 1 IPv4. - `memory` (Number) How much memory to allocate in MB. Omit to use the default memory size from the package. -- `name` (String) Server name. If omitted, a random UUID will be generated. - `network_profile` (Number) Network profile ID. Omit to use the default network profile from the package. - `outbound_network_speed` (Number) Outbound network speed in kB/s. Omit to use the default outbound network speed from the package. - `storage` (Number) Primary storage size in GB. Omit to use the default storage size from the package. diff --git a/examples/resources/virtfusion_build/resource.tf b/examples/resources/virtfusion_build/resource.tf new file mode 100644 index 0000000..9c39ff1 --- /dev/null +++ b/examples/resources/virtfusion_build/resource.tf @@ -0,0 +1,26 @@ +resource "virtfusion_server" "node1" { + package_id = 1 + user_id = 1 + hypervisor_id = 1 + ipv4 = 1 + storage = 30 + memory = 1024 + cores = 1 + traffic = 1000 + inbound_network_speed = 100 + outbound_network_speed = 100 + storage_profile = 1 + network_profile = 1 +} + + +resource "virtfusion_build" "node1" { + server_id = virtfusion_server.node1.id + name = "node1-demo" + hostname = "node1.example.com" + osid = 1 + vnc = true + ipv6 = true + ssh_keys = [1, 2, 3] + email = true +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 30203df..1ab43ee 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -115,6 +115,7 @@ func (p *ScaffoldingProvider) Configure(ctx context.Context, req provider.Config func (p *ScaffoldingProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewVirtfusionServerResource, + NewVirtfusionServerBuildResource, } } diff --git a/internal/provider/virtfusion_server_build_resource.go b/internal/provider/virtfusion_server_build_resource.go new file mode 100644 index 0000000..ad528a6 --- /dev/null +++ b/internal/provider/virtfusion_server_build_resource.go @@ -0,0 +1,285 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/types" + "io" + "io/ioutil" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &VirtfusionServerBuildResource{} +var _ resource.ResourceWithImportState = &VirtfusionServerBuildResource{} + +func NewVirtfusionServerBuildResource() resource.Resource { + return &VirtfusionServerBuildResource{} +} + +// VirtfusionServerBuildResource defines the resource implementation. +type VirtfusionServerBuildResource struct { + client *http.Client +} + +type VirtfusionServerBuildResourceModel struct { + ServerId int64 `tfsdk:"server_id"` + Name string `tfsdk:"name" json:"name"` + Hostname string `tfsdk:"hostname" json:"hostname"` + Osid int64 `tfsdk:"osid" json:"operatingSystemId"` + Vnc bool `tfsdk:"vnc" json:"vnc"` + Ipv6 bool `tfsdk:"ipv6" json:"ipv6"` + SshKeys []int64 `tfsdk:"ssh_keys" json:"sshKeys"` + Email bool `tfsdk:"email" json:"email"` +} + +func (r *VirtfusionServerBuildResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_build" +} + +func (r *VirtfusionServerBuildResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Virtfusion Server Build Resource", + + Attributes: map[string]schema.Attribute{ + "server_id": schema.Int64Attribute{ + MarkdownDescription: "Server ID", + Required: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Server Name", + Required: true, + }, + "hostname": schema.StringAttribute{ + MarkdownDescription: "Server Hostname", + Optional: true, + }, + "osid": schema.Int64Attribute{ + MarkdownDescription: "Server Operating System ID", + Required: true, + }, + "vnc": schema.BoolAttribute{ + MarkdownDescription: "Server VNC", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "ipv6": schema.BoolAttribute{ + MarkdownDescription: "Server IPv6", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "ssh_keys": schema.ListAttribute{ + MarkdownDescription: "Server SSH Keys IDs", + ElementType: types.Int64Type, + Optional: true, + }, + "email": schema.BoolAttribute{ + MarkdownDescription: "Server Email", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + } +} + +func (r *VirtfusionServerBuildResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*http.Client) + + 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 + } + + r.client = client +} + +func (r *VirtfusionServerBuildResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VirtfusionServerBuildResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + createReq := VirtfusionServerBuildResourceModel{ + Name: data.Name, + Hostname: data.Hostname, + Osid: data.Osid, + Vnc: data.Vnc, + Ipv6: data.Ipv6, + SshKeys: data.SshKeys, + Email: data.Email, + } + + httpReqBody, err := json.Marshal(createReq) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Create Resource", + "An unexpected error occurred while creating the resource create request. "+ + "Please report this issue to the provider developers.\n\n"+ + "JSON Error: "+err.Error(), + ) + + return + } + + httpReq, err := http.NewRequest("POST", fmt.Sprintf("/servers/%d/build", data.ServerId), bytes.NewBuffer(httpReqBody)) + + if err != nil { + resp.Diagnostics.AddError( + "Failed to Create Request", + fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()), + ) + return + } + + // Add any additional headers (Content-Type, etc.) + httpReq.Header.Set("Content-Type", "application/json") + + httpResponse, err := r.client.Do(httpReq) + if err != nil { + resp.Diagnostics.AddError( + "Failed to Execute Request", + fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()), + ) + return + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + resp.Diagnostics.AddError( + "Failed to Close Request", + fmt.Sprintf("Failed to close HTTP request: %s", err.Error()), + ) + return + } + }(httpResponse.Body) + + if httpResponse.StatusCode == 422 { + responseBody, err := ioutil.ReadAll(httpResponse.Body) + if err != nil { + resp.Diagnostics.AddError( + "Failed to Read Response", + fmt.Sprintf("Failed to read HTTP response body: %s", err.Error()), + ) + return + } + + var errorResponse map[string]interface{} + err = json.Unmarshal(responseBody, &errorResponse) + if err != nil { + resp.Diagnostics.AddError( + "Failed to Parse Error Response", + fmt.Sprintf("Failed to parse HTTP response body: %s", err.Error()), + ) + return + } + + if errors, exists := errorResponse["errors"]; exists { + resp.Diagnostics.AddError( + "Server Returned Errors", + fmt.Sprintf("Errors from server: %v", errors), + ) + } + + return + } + + if httpResponse.StatusCode != 200 { + resp.Diagnostics.AddError( + "Failed to Create Resource", + fmt.Sprintf("Failed to create resource: %s", httpResponse.Status), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *VirtfusionServerBuildResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data VirtfusionServerBuildResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // If applicable, this is a great opportunity to initialize any necessary + // provider client data and make a call using it. + // httpResp, err := r.client.Do(httpReq) + // if err != nil { + // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err)) + // return + // } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *VirtfusionServerBuildResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data VirtfusionServerBuildResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // If applicable, this is a great opportunity to initialize any necessary + // provider client data and make a call using it. + // httpResp, err := r.client.Do(httpReq) + // if err != nil { + // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err)) + // return + // } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *VirtfusionServerBuildResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data VirtfusionServerBuildResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *VirtfusionServerBuildResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/provider/virtfusion_server_resource.go b/internal/provider/virtfusion_server_resource.go index 97cace1..0dbf8f6 100644 --- a/internal/provider/virtfusion_server_resource.go +++ b/internal/provider/virtfusion_server_resource.go @@ -8,7 +8,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "github.com/hashicorp/terraform-plugin-framework/types" "io" @@ -35,53 +34,31 @@ type VirtfusionServerResource struct { // VirtfusionServerResourceModel describes the resource data model. type VirtfusionServerResourceModel struct { - PackageId *int64 `tfsdk:"package_id" json:"packageId,omitempty"` - UserId *int64 `tfsdk:"user_id" json:"userId,omitempty"` - HypervisorId *int64 `tfsdk:"hypervisor_id" json:"hypervisorId,omitempty"` - Ipv4 *int64 `tfsdk:"ipv4" json:"ipv4,omitempty"` - Storage *int64 `tfsdk:"storage" json:"storage,omitempty"` - Memory *int64 `tfsdk:"memory" json:"memory,omitempty"` - Cores *int64 `tfsdk:"cores" json:"cpuCores,omitempty"` - Traffic *int64 `tfsdk:"traffic" json:"traffic,omitempty"` - InboundNetworkSpeed *int64 `tfsdk:"inbound_network_speed" json:"networkSpeedInbound,omitempty"` - OutboundNetworkSpeed *int64 `tfsdk:"outbound_network_speed" json:"networkSpeedOutbound,omitempty"` - StorageProfile *int64 `tfsdk:"storage_profile" json:"storageProfile,omitempty"` - NetworkProfile *int64 `tfsdk:"network_profile" json:"networkProfile,omitempty"` - Name types.String `tfsdk:"name" json:"name,omitempty"` - Id types.Int64 `tfsdk:"id" json:"id"` - Build struct { - Name types.String `tfsdk:"name" json:"name"` - Hostname types.String `tfsdk:"hostname" json:"hostname"` - Osid types.Int64 `tfsdk:"osid" json:"operatingSystemId"` - Vnc types.Bool `tfsdk:"vnc" json:"vnc"` - Ipv6 types.Bool `tfsdk:"ipv6" json:"ipv6"` - SshKeys []types.Int64 `tfsdk:"ssh_keys" json:"sshKeys"` - Email types.Bool `tfsdk:"email" json:"email"` - } + PackageId *int64 `tfsdk:"package_id" json:"packageId,omitempty"` + UserId *int64 `tfsdk:"user_id" json:"userId,omitempty"` + HypervisorId *int64 `tfsdk:"hypervisor_id" json:"hypervisorId,omitempty"` + Ipv4 *int64 `tfsdk:"ipv4" json:"ipv4,omitempty"` + Storage *int64 `tfsdk:"storage" json:"storage,omitempty"` + Memory *int64 `tfsdk:"memory" json:"memory,omitempty"` + Cores *int64 `tfsdk:"cores" json:"cpuCores,omitempty"` + Traffic *int64 `tfsdk:"traffic" json:"traffic,omitempty"` + InboundNetworkSpeed *int64 `tfsdk:"inbound_network_speed" json:"networkSpeedInbound,omitempty"` + OutboundNetworkSpeed *int64 `tfsdk:"outbound_network_speed" json:"networkSpeedOutbound,omitempty"` + StorageProfile *int64 `tfsdk:"storage_profile" json:"storageProfile,omitempty"` + NetworkProfile *int64 `tfsdk:"network_profile" json:"networkProfile,omitempty"` + Id types.Int64 `tfsdk:"id" json:"id"` } func (r *VirtfusionServerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_server" } -func GenerateRandomName() string { - // Generate a random name for the server if one is not provided - // It'll be in a format like "tf-" - return "tf-" + uuid.New().String()[:8] -} - func (r *VirtfusionServerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ // This description is used by the documentation generator and the language server. MarkdownDescription: "Virtfusion Server Resource", Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - MarkdownDescription: "Server name. If omitted, a random UUID will be generated.", - Optional: true, - Computed: true, - //Default: stringdefault.StaticString(GenerateRandomName()), - }, "package_id": schema.Int64Attribute{ MarkdownDescription: "Package ID", Required: true, @@ -199,6 +176,7 @@ func (r *VirtfusionServerResource) Create(ctx context.Context, req resource.Crea } httpReq, err := http.NewRequest("POST", "/servers", bytes.NewBuffer(httpReqBody)) + if err != nil { resp.Diagnostics.AddError( "Failed to Create Request", @@ -269,6 +247,7 @@ func (r *VirtfusionServerResource) Create(ctx context.Context, req resource.Crea } responseBody, err := ioutil.ReadAll(httpResponse.Body) + if err != nil { resp.Diagnostics.AddError( "Failed to Read Response", @@ -289,6 +268,7 @@ func (r *VirtfusionServerResource) Create(ctx context.Context, req resource.Crea // Unmarshal the JSON response err = json.Unmarshal(responseBody, &responseData) + if err != nil { resp.Diagnostics.AddError( "Failed to Parse Response", @@ -299,7 +279,7 @@ func (r *VirtfusionServerResource) Create(ctx context.Context, req resource.Crea // Update the Terraform state with the server ID data.Id = types.Int64Value(responseData.Data.Id) - data.Name = types.StringValue(responseData.Data.Name) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) }