diff --git a/internal/provider/network_data_source.go b/internal/provider/network_data_source.go index 857f41c..65a1bc7 100644 --- a/internal/provider/network_data_source.go +++ b/internal/provider/network_data_source.go @@ -105,13 +105,13 @@ func (d *NetworkDataSource) Read(ctx context.Context, req datasource.ReadRequest return } - state.Id = types.StringValue(res.Network.Id) - state.DataCenterId = types.StringValue(res.Network.DataCenterId) - state.IPRange = types.StringValue(res.Network.IpRange) - state.Gateway = types.StringValue(res.Network.Gateway) - state.ExternalIPAddress = types.StringValue(res.Network.ExternalIpAddress) - state.InternalIPAddress = types.StringValue(res.Network.InternalIpAddress) - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, NetworkDataSourceModel{ + Id: types.StringValue(res.Network.Id), + DataCenterId: types.StringValue(res.Network.DataCenterId), + IPRange: types.StringValue(res.Network.IpRange), + Gateway: types.StringValue(res.Network.Gateway), + ExternalIPAddress: types.StringValue(res.Network.ExternalIpAddress), + InternalIPAddress: types.StringValue(res.Network.InternalIpAddress), + })...) } diff --git a/internal/provider/network_resource.go b/internal/provider/network_resource.go index b722eb2..eb70fbc 100644 --- a/internal/provider/network_resource.go +++ b/internal/provider/network_resource.go @@ -112,128 +112,8 @@ func (r *NetworkResource) Configure(ctx context.Context, req resource.ConfigureR r.client = client } -func waitForNetworkAvailable(ctx context.Context, projectID string, networkID string, c network.NetworkServiceClient) (*network.Network, error) { - refreshFunc := func() (interface{}, string, error) { - res, err := c.GetNetwork(ctx, &network.GetNetworkRequest{ - Id: networkID, - ProjectId: projectID, - }) - if err != nil { - if ok := helper.IsErrCode(err, codes.NotFound); ok { - tflog.Debug(ctx, fmt.Sprintf("Network %s in project %s not found: ", networkID, projectID)) - return res, "done", nil - } - return nil, "", err - } - - tflog.Trace(ctx, fmt.Sprintf("pending network %s in project %s state: %s", networkID, projectID, res.Network.ShortState)) - return res, res.Network.ShortState, nil - } - - tflog.Debug(ctx, fmt.Sprintf("waiting for network %s in project %s ", networkID, projectID)) - - stateConf := &helper.StateChangeConf{ - Pending: []string{"clea", "clon", "dsrz", "epil", "hold", "hotp", "init", "migr", "pend", "prol", "save", "shut", "snap", "unkn"}, - Target: []string{"boot", "done", "fail", "poff", "runn", "stop", "susp", "unde"}, - Refresh: refreshFunc, - Timeout: 2 * time.Hour, - Delay: 1 * time.Second, - MinTimeout: 3 * time.Second, - } - - if res, err := stateConf.WaitForState(ctx); err != nil { - return nil, fmt.Errorf("error waiting for network %s in project %s to become available: %w", networkID, projectID, err) - } else if res, ok := res.(*network.GetNetworkResponse); ok { - tflog.Trace(ctx, fmt.Sprintf("completed waiting for network %s in project %s (%s)", networkID, projectID, res.Network.ShortState)) - return res.Network, nil - } - - return nil, nil -} - -func waitForNetworkStop(ctx context.Context, projectID string, networkID string, c network.NetworkServiceClient) (*network.Network, error) { - refreshFunc := func() (interface{}, string, error) { - res, err := c.GetNetwork(ctx, &network.GetNetworkRequest{ - Id: networkID, - ProjectId: projectID, - }) - if err != nil { - if ok := helper.IsErrCode(err, codes.NotFound); ok { - tflog.Debug(ctx, fmt.Sprintf("Network %s in project %s is done: ", networkID, projectID)) - return res, "done", nil - } - tflog.Error(ctx, fmt.Sprintf("error getting network %s in project %s: %v", networkID, projectID, err)) - return nil, "", err - } - if res.Network.ShortState == "" { - tflog.Debug(ctx, fmt.Sprintf("Network %s in project %s is stopped: ", networkID, projectID)) - return res, "done", nil - } - - tflog.Trace(ctx, fmt.Sprintf("pending network %s in project %s state: %s", networkID, projectID, res.Network.ShortState)) - return res, res.Network.ShortState, nil - } - - tflog.Debug(ctx, fmt.Sprintf("waiting for network %s in project %s ", networkID, projectID)) - - stateConf := &helper.StateChangeConf{ - Pending: []string{"fail", "poff", "runn", "stop", "susp", "unde", "boot", "clea", "clon", "dsrz", "epil", "hold", "hotp", "init", "migr", "pend", "prol", "save", "shut", "snap", "unkn"}, - Target: []string{"done"}, - Refresh: refreshFunc, - Timeout: 20 * time.Minute, - MinTimeout: 3 * time.Second, - } - - if _, err := stateConf.WaitForState(ctx); err != nil { - return nil, fmt.Errorf("error waiting for network %s in project %s to be stopped: %w", networkID, projectID, err) - } - - return nil, nil -} - -func waitForNetworkDelete(ctx context.Context, projectID string, networkID string, c network.NetworkServiceClient) (*network.Network, error) { - refreshFunc := func() (interface{}, string, error) { - res, err := c.GetNetwork(ctx, &network.GetNetworkRequest{ - Id: networkID, - ProjectId: projectID, - }) - if err != nil { - if ok := helper.IsErrCode(err, codes.NotFound); ok { - tflog.Debug(ctx, fmt.Sprintf("Network %s in project %s is done: ", networkID, projectID)) - return res, "done", nil - } - - tflog.Error(ctx, fmt.Sprintf("error getting network %s in project %s: %v", networkID, projectID, err)) - return nil, "", err - } - if res.Network.ShortState == "" { - tflog.Debug(ctx, fmt.Sprintf("Network %s in project %s is stopped: ", networkID, projectID)) - return res, "stop", nil - } - - tflog.Trace(ctx, fmt.Sprintf("pending network %s in project %s state: %s", networkID, projectID, res.Network.ShortState)) - return res, res.Network.ShortState, nil - } - - tflog.Debug(ctx, fmt.Sprintf("waiting for network %s in project %s ", networkID, projectID)) - - stateConf := &helper.StateChangeConf{ - Pending: []string{"fail", "poff", "runn", "stop", "susp", "unde", "boot", "clea", "clon", "dsrz", "epil", "hold", "hotp", "init", "migr", "pend", "prol", "save", "shut", "snap", "unkn"}, - Target: []string{"done"}, - Refresh: refreshFunc, - Timeout: 2 * time.Hour, - MinTimeout: 3 * time.Second, - } - - if _, err := stateConf.WaitForState(ctx); err != nil { - return nil, fmt.Errorf("error waiting for network %s in project %s to be deleted: %w", networkID, projectID, err) - } - - return nil, nil -} - func (r *NetworkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var state *NetworkResourceModel + var state NetworkResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...) @@ -265,15 +145,18 @@ func (r *NetworkResource) Create(ctx context.Context, req resource.CreateRequest return } - state.Gateway = types.StringValue(network.Gateway) - state.ExternalIPAddress = types.StringValue(network.ExternalIpAddress) - state.InternalIPAddress = types.StringValue(network.InternalIpAddress) - - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, NetworkResourceModel{ + ID: types.StringValue(network.Id), + DataCenterId: types.StringValue(network.DataCenterId), + IPRange: types.StringValue(network.IpRange), + Gateway: types.StringValue(network.Gateway), + ExternalIPAddress: types.StringValue(network.ExternalIpAddress), + InternalIPAddress: types.StringValue(network.InternalIpAddress), + })...) } func (r *NetworkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var state *NetworkResourceModel + var state NetworkResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) @@ -297,15 +180,15 @@ func (r *NetworkResource) Read(ctx context.Context, req resource.ReadRequest, re return } - state.ID = types.StringValue(res.Network.Id) - state.DataCenterId = types.StringValue(res.Network.DataCenterId) - state.ExternalIPAddress = types.StringValue(res.Network.ExternalIpAddress) - state.InternalIPAddress = types.StringValue(res.Network.InternalIpAddress) - state.IPRange = types.StringValue(res.Network.IpRange) - state.Gateway = types.StringValue(res.Network.Gateway) - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, NetworkResourceModel{ + ID: types.StringValue(res.Network.Id), + DataCenterId: types.StringValue(res.Network.DataCenterId), + ExternalIPAddress: types.StringValue(res.Network.ExternalIpAddress), + InternalIPAddress: types.StringValue(res.Network.InternalIpAddress), + IPRange: types.StringValue(res.Network.IpRange), + Gateway: types.StringValue(res.Network.Gateway), + })...) } func (r *NetworkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -381,3 +264,123 @@ func (r *NetworkResource) Delete(ctx context.Context, req resource.DeleteRequest func (r *NetworkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } + +func waitForNetworkAvailable(ctx context.Context, projectID string, networkID string, c network.NetworkServiceClient) (*network.Network, error) { + refreshFunc := func() (interface{}, string, error) { + res, err := c.GetNetwork(ctx, &network.GetNetworkRequest{ + Id: networkID, + ProjectId: projectID, + }) + if err != nil { + if ok := helper.IsErrCode(err, codes.NotFound); ok { + tflog.Debug(ctx, fmt.Sprintf("Network %s in project %s not found: ", networkID, projectID)) + return res, "done", nil + } + return nil, "", err + } + + tflog.Trace(ctx, fmt.Sprintf("pending network %s in project %s state: %s", networkID, projectID, res.Network.ShortState)) + return res, res.Network.ShortState, nil + } + + tflog.Debug(ctx, fmt.Sprintf("waiting for network %s in project %s ", networkID, projectID)) + + stateConf := &helper.StateChangeConf{ + Pending: []string{"clea", "clon", "dsrz", "epil", "hold", "hotp", "init", "migr", "pend", "prol", "save", "shut", "snap", "unkn"}, + Target: []string{"boot", "done", "fail", "poff", "runn", "stop", "susp", "unde"}, + Refresh: refreshFunc, + Timeout: 2 * time.Hour, + Delay: 1 * time.Second, + MinTimeout: 3 * time.Second, + } + + if res, err := stateConf.WaitForState(ctx); err != nil { + return nil, fmt.Errorf("error waiting for network %s in project %s to become available: %w", networkID, projectID, err) + } else if res, ok := res.(*network.GetNetworkResponse); ok { + tflog.Trace(ctx, fmt.Sprintf("completed waiting for network %s in project %s (%s)", networkID, projectID, res.Network.ShortState)) + return res.Network, nil + } + + return nil, nil +} + +func waitForNetworkStop(ctx context.Context, projectID string, networkID string, c network.NetworkServiceClient) (*network.Network, error) { + refreshFunc := func() (interface{}, string, error) { + res, err := c.GetNetwork(ctx, &network.GetNetworkRequest{ + Id: networkID, + ProjectId: projectID, + }) + if err != nil { + if ok := helper.IsErrCode(err, codes.NotFound); ok { + tflog.Debug(ctx, fmt.Sprintf("Network %s in project %s is done: ", networkID, projectID)) + return res, "done", nil + } + tflog.Error(ctx, fmt.Sprintf("error getting network %s in project %s: %v", networkID, projectID, err)) + return nil, "", err + } + if res.Network.ShortState == "" { + tflog.Debug(ctx, fmt.Sprintf("Network %s in project %s is stopped: ", networkID, projectID)) + return res, "done", nil + } + + tflog.Trace(ctx, fmt.Sprintf("pending network %s in project %s state: %s", networkID, projectID, res.Network.ShortState)) + return res, res.Network.ShortState, nil + } + + tflog.Debug(ctx, fmt.Sprintf("waiting for network %s in project %s ", networkID, projectID)) + + stateConf := &helper.StateChangeConf{ + Pending: []string{"fail", "poff", "runn", "stop", "susp", "unde", "boot", "clea", "clon", "dsrz", "epil", "hold", "hotp", "init", "migr", "pend", "prol", "save", "shut", "snap", "unkn"}, + Target: []string{"done"}, + Refresh: refreshFunc, + Timeout: 20 * time.Minute, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForState(ctx); err != nil { + return nil, fmt.Errorf("error waiting for network %s in project %s to be stopped: %w", networkID, projectID, err) + } + + return nil, nil +} + +func waitForNetworkDelete(ctx context.Context, projectID string, networkID string, c network.NetworkServiceClient) (*network.Network, error) { + refreshFunc := func() (interface{}, string, error) { + res, err := c.GetNetwork(ctx, &network.GetNetworkRequest{ + Id: networkID, + ProjectId: projectID, + }) + if err != nil { + if ok := helper.IsErrCode(err, codes.NotFound); ok { + tflog.Debug(ctx, fmt.Sprintf("Network %s in project %s is done: ", networkID, projectID)) + return res, "done", nil + } + + tflog.Error(ctx, fmt.Sprintf("error getting network %s in project %s: %v", networkID, projectID, err)) + return nil, "", err + } + if res.Network.ShortState == "" { + tflog.Debug(ctx, fmt.Sprintf("Network %s in project %s is stopped: ", networkID, projectID)) + return res, "stop", nil + } + + tflog.Trace(ctx, fmt.Sprintf("pending network %s in project %s state: %s", networkID, projectID, res.Network.ShortState)) + return res, res.Network.ShortState, nil + } + + tflog.Debug(ctx, fmt.Sprintf("waiting for network %s in project %s ", networkID, projectID)) + + stateConf := &helper.StateChangeConf{ + Pending: []string{"fail", "poff", "runn", "stop", "susp", "unde", "boot", "clea", "clon", "dsrz", "epil", "hold", "hotp", "init", "migr", "pend", "prol", "save", "shut", "snap", "unkn"}, + Target: []string{"done"}, + Refresh: refreshFunc, + Timeout: 2 * time.Hour, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForState(ctx); err != nil { + return nil, fmt.Errorf("error waiting for network %s in project %s to be deleted: %w", networkID, projectID, err) + } + + return nil, nil +} diff --git a/internal/provider/security_group_data_source.go b/internal/provider/security_group_data_source.go index 577db17..1c363db 100644 --- a/internal/provider/security_group_data_source.go +++ b/internal/provider/security_group_data_source.go @@ -137,12 +137,11 @@ func (d *SecurityGroupDataSource) Read(ctx context.Context, req datasource.ReadR return } - sg := res.SecurityGroup - state.ID = types.StringValue(sg.Id) - state.DataCenterID = types.StringValue(sg.DataCenterId) - state.Description = types.StringValue(sg.Description) - state.Rules = getRuleModels(sg.Rules) - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, SecurityGroupDataSourceModel{ + ID: types.StringValue(res.SecurityGroup.Id), + DataCenterID: types.StringValue(res.SecurityGroup.DataCenterId), + Description: types.StringValue(res.SecurityGroup.Description), + Rules: getRuleModels(res.SecurityGroup.Rules), + })...) } diff --git a/internal/provider/security_group_resource.go b/internal/provider/security_group_resource.go index 2c2766a..13143a6 100644 --- a/internal/provider/security_group_resource.go +++ b/internal/provider/security_group_resource.go @@ -278,7 +278,7 @@ func (r *SecurityGroupResource) Create(ctx context.Context, req resource.CreateR } func (r *SecurityGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var state *SecurityGroupResourceModel + var state SecurityGroupResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) @@ -304,14 +304,13 @@ func (r *SecurityGroupResource) Read(ctx context.Context, req resource.ReadReque return } - state.Id = types.StringValue(res.SecurityGroup.Id) - state.Description = types.StringValue(res.SecurityGroup.Description) - state.DataCenterID = types.StringValue(res.SecurityGroup.DataCenterId) - state.Id = types.StringValue(res.SecurityGroup.Id) - state.Rules = getRuleModels(res.SecurityGroup.Rules) - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, SecurityGroupResourceModel{ + Id: types.StringValue(res.SecurityGroup.Id), + Description: types.StringValue(res.SecurityGroup.Description), + DataCenterID: types.StringValue(res.SecurityGroup.DataCenterId), + Rules: getRuleModels(res.SecurityGroup.Rules), + })...) } func (r *SecurityGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -342,7 +341,6 @@ func (r *SecurityGroupResource) Update(ctx context.Context, req resource.UpdateR } state.Rules = getRuleModels(res.SecurityGroup.Rules) - // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } diff --git a/internal/provider/vm_data_source.go b/internal/provider/vm_data_source.go index 1533ceb..91015c4 100644 --- a/internal/provider/vm_data_source.go +++ b/internal/provider/vm_data_source.go @@ -151,22 +151,24 @@ func (d *VMDataSource) Read(ctx context.Context, req datasource.ReadRequest, res imageID = res.VM.PublicImageId } - state.BootDiskSizeGib = types.Int64Value(int64(res.VM.BootDiskSizeGib)) - state.CPUModel = types.StringValue(res.VM.CpuModel) - state.DatacenterID = types.StringValue(res.VM.DatacenterId) - state.GpuModel = types.StringValue(res.VM.GpuModel) - state.Gpus = types.Int64Value(int64(res.VM.GpuQuantity)) - state.ImageID = types.StringValue(imageID) - state.InternalIPAddress = types.StringValue(res.VM.InternalIpAddress) - state.ExternalIPAddress = types.StringValue(res.VM.ExternalIpAddress) - state.Memory = types.Int64Value(int64(res.VM.Memory)) - state.PriceHr = types.Float64Value(float64(res.VM.PriceHr)) - state.Vcpus = types.Int64Value(int64(res.VM.Vcpus)) - // Write logs using the tflog package // Documentation: https://terraform.io/plugin/log tflog.Trace(ctx, "read a data source") // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, VMDataSourceModel{ + ProjectID: state.ProjectID, + Id: state.Id, + BootDiskSizeGib: types.Int64Value(int64(res.VM.BootDiskSizeGib)), + CPUModel: types.StringValue(res.VM.CpuModel), + DatacenterID: types.StringValue(res.VM.DatacenterId), + GpuModel: types.StringValue(res.VM.GpuModel), + Gpus: types.Int64Value(int64(res.VM.GpuQuantity)), + ImageID: types.StringValue(imageID), + InternalIPAddress: types.StringValue(res.VM.InternalIpAddress), + ExternalIPAddress: types.StringValue(res.VM.ExternalIpAddress), + Memory: types.Int64Value(int64(res.VM.Memory)), + PriceHr: types.Float64Value(float64(res.VM.PriceHr)), + Vcpus: types.Int64Value(int64(res.VM.Vcpus)), + })...) } diff --git a/internal/provider/vm_image_resource.go b/internal/provider/vm_image_resource.go index a8670bf..53861b9 100644 --- a/internal/provider/vm_image_resource.go +++ b/internal/provider/vm_image_resource.go @@ -107,7 +107,7 @@ func (r *VMImageResource) Configure(ctx context.Context, req resource.ConfigureR } func (r *VMImageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var state *VMImageResourceModel + var state VMImageResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...) @@ -137,10 +137,10 @@ func (r *VMImageResource) Create(ctx context.Context, req resource.CreateRequest return } - state.DataCenterId = types.StringValue(res.Image.DataCenterId) - state.SizeGib = types.Int64Value(int64(res.Image.SizeGib)) - - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, VMImageResourceModel{ + DataCenterId: types.StringValue(res.Image.DataCenterId), + SizeGib: types.Int64Value(int64(res.Image.SizeGib)), + })...) } func (r *VMImageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { diff --git a/internal/provider/vm_resource.go b/internal/provider/vm_resource.go index e9e11ff..f021f82 100644 --- a/internal/provider/vm_resource.go +++ b/internal/provider/vm_resource.go @@ -3,14 +3,26 @@ package provider import ( "context" "fmt" + "regexp" "time" "github.com/CudoVentures/terraform-provider-cudo/internal/compute/vm" "github.com/CudoVentures/terraform-provider-cudo/internal/helper" grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "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/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "google.golang.org/genproto/googleapis/type/decimal" @@ -29,6 +41,208 @@ func (r *VMResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp.TypeName = "cudo_vm" } +func (r *VMResource) 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: "VM resource", + Attributes: map[string]schema.Attribute{ + "boot_disk": schema.SingleNestedAttribute{ + MarkdownDescription: "Specification for boot disk", + Attributes: map[string]schema.Attribute{ + "size_gib": schema.Int64Attribute{ + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Computed: true, + Optional: true, + MarkdownDescription: "Size of boot disk in Gib", + }, + "image_id": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "ID of OS image on boot disk", + Required: true, + Validators: []validator.String{stringvalidator.RegexMatches(regexp.MustCompile("^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$"), "must be a valid resource id")}, + }, + }, + Required: true, + }, + "cpu_model": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "The model of the CPU.", + Optional: true, + Computed: true, + }, + "data_center_id": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "The id of the datacenter where the VM instance is located.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.RegexMatches(regexp.MustCompile("^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$"), "must be a valid resource id")}, + }, + "external_ip_address": schema.StringAttribute{ + MarkdownDescription: "The external IP address of the VM instance.", + Computed: true, + }, + "gpu_model": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "The model of the GPU.", + Optional: true, + Computed: true, + }, + "gpus": schema.Int64Attribute{ + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + MarkdownDescription: "Number of GPUs", + Optional: true, + Computed: true, + Default: int64default.StaticInt64(0), + }, + "id": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "ID for VM within project", + Required: true, + Validators: []validator.String{stringvalidator.RegexMatches(regexp.MustCompile("^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$"), "must be a valid resource id e.g. my-vm")}, + }, + "internal_ip_address": schema.StringAttribute{ + MarkdownDescription: "The internal IP address of the VM instance.", + Computed: true, + }, + "machine_type": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "VM machine type, from machine type data source", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.RegexMatches(regexp.MustCompile("^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$"), "must be a valid resource id")}, + }, + "max_price_hr": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "The maximum price per hour for the VM instance.", + Optional: true, + }, + "memory_gib": schema.Int64Attribute{ + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + MarkdownDescription: "Amount of VM memory in GiB", + Optional: true, + }, + "networks": schema.ListNestedAttribute{ + Optional: true, + MarkdownDescription: "Network adapters for private networks", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "network_id": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "ID of private network to attach the NIC to", + Required: true, + }, + "assign_public_ip": schema.BoolAttribute{ + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "Assign a public IP to the NIC", + Optional: true, + }, + "external_ip_address": schema.StringAttribute{ + MarkdownDescription: "The external IP address of the NIC.", + Computed: true, + }, + "internal_ip_address": schema.StringAttribute{ + MarkdownDescription: "The internal IP address of the NIC.", + Computed: true, + }, + "security_group_ids": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Security groups to assign to the NIC", + }, + }, + }, + }, + "password": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "Root password for linux, or Admin password for windows", + Optional: true, + Sensitive: true, + Validators: []validator.String{stringvalidator.LengthBetween(6, 64)}, + }, + "price_hr": schema.StringAttribute{ + MarkdownDescription: "The current price per hour for the VM instance.", + Computed: true, + }, + "project_id": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "The project the VM instance is in.", + Optional: true, + }, + "renewable_energy": schema.BoolAttribute{ + MarkdownDescription: "Whether the VM instance is powered by renewable energy", + Computed: true, + }, + "security_group_ids": schema.SetAttribute{ + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Security groups to assign to the VM when using public networking", + }, + "ssh_key_source": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "Which SSH keys to add to the VM: project (default), user or custom", + Optional: true, + Validators: []validator.String{stringvalidator.OneOf("project", "user", "custom")}, + }, + "ssh_keys": schema.ListAttribute{ + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + ElementType: types.StringType, + MarkdownDescription: "List of SSH keys to add to the VM, ssh_key_source must be set to custom", + Optional: true, + }, + "start_script": schema.StringAttribute{ + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "A script to run when VM boots", + Optional: true, + }, + "vcpus": schema.Int64Attribute{ + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + MarkdownDescription: "Number of VCPUs", + Optional: true, + Validators: []validator.Int64{int64validator.AtMost(100)}, + }, + }, + } +} + // VMResource defines the resource implementation. type VMResource struct { client *CudoClientData @@ -196,8 +410,7 @@ func (r *VMResource) Create(ctx context.Context, req resource.CreateRequest, res return } - fillVmState(state, vm.VM) - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, fillVmState(vm.VM))...) } func (r *VMResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { @@ -228,8 +441,7 @@ func (r *VMResource) Read(ctx context.Context, req resource.ReadRequest, resp *r return } - fillVmState(state, vm.VM) - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, fillVmState(vm.VM))...) } func (r *VMResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -257,11 +469,10 @@ func (r *VMResource) Delete(ctx context.Context, req resource.DeleteRequest, res var state *VMResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - params := &vm.TerminateVMRequest{} - params.ProjectId = r.client.DefaultProjectID - params.Id = state.ID.ValueString() + projectId := r.client.DefaultProjectID + vmId := state.ID.ValueString() - if _, err := waitForVmAvailable(ctx, params.ProjectId, params.Id, r.client.VMClient); err != nil { + if _, err := waitForVmAvailable(ctx, projectId, vmId, r.client.VMClient); err != nil { resp.Diagnostics.AddError( "Unable to wait for VM resource to be available", err.Error(), @@ -269,7 +480,10 @@ func (r *VMResource) Delete(ctx context.Context, req resource.DeleteRequest, res return } - _, err := r.client.VMClient.TerminateVM(ctx, params) + _, err := r.client.VMClient.TerminateVM(ctx, &vm.TerminateVMRequest{ + ProjectId: projectId, + Id: vmId, + }) if err != nil { resp.Diagnostics.AddError( "Unable to delete VM resource", @@ -278,7 +492,7 @@ func (r *VMResource) Delete(ctx context.Context, req resource.DeleteRequest, res return } - _, err = waitForVmDelete(ctx, params.ProjectId, params.Id, r.client.VMClient) + _, err = waitForVmDelete(ctx, projectId, vmId, r.client.VMClient) if err != nil { resp.Diagnostics.AddError( "Unable to wait for VM resource to be deleted", @@ -379,7 +593,8 @@ func waitForVmDelete(ctx context.Context, projectId string, vmID string, c vm.VM } } -func fillVmState(state *VMResourceModel, vm *vm.VM) { +func fillVmState(vm *vm.VM) VMResourceModel { + var state VMResourceModel state.DataCenterID = types.StringValue(vm.DatacenterId) state.CPUModel = types.StringValue(vm.CpuModel) state.GPUs = types.Int64Value(int64(vm.GpuQuantity)) @@ -401,4 +616,5 @@ func fillVmState(state *VMResourceModel, vm *vm.VM) { state.ExternalIPAddress = types.StringValue(vm.ExternalIpAddress) state.PriceHr = types.StringValue(fmt.Sprintf("%0.2f", vm.PriceHr)) state.RenewableEnergy = types.BoolValue(vm.RenewableEnergy) + return state } diff --git a/internal/provider/vm_schema.go b/internal/provider/vm_schema.go deleted file mode 100644 index 19153cc..0000000 --- a/internal/provider/vm_schema.go +++ /dev/null @@ -1,222 +0,0 @@ -package provider - -import ( - "context" - "regexp" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -func (r *VMResource) 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: "VM resource", - Attributes: map[string]schema.Attribute{ - "boot_disk": schema.SingleNestedAttribute{ - MarkdownDescription: "Specification for boot disk", - Attributes: map[string]schema.Attribute{ - "size_gib": schema.Int64Attribute{ - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Computed: true, - Optional: true, - MarkdownDescription: "Size of boot disk in Gib", - }, - "image_id": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "ID of OS image on boot disk", - Required: true, - Validators: []validator.String{stringvalidator.RegexMatches(regexp.MustCompile("^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$"), "must be a valid resource id")}, - }, - }, - Required: true, - }, - "cpu_model": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "The model of the CPU.", - Optional: true, - Computed: true, - }, - "data_center_id": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "The id of the datacenter where the VM instance is located.", - Optional: true, - Computed: true, - Validators: []validator.String{stringvalidator.RegexMatches(regexp.MustCompile("^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$"), "must be a valid resource id")}, - }, - "external_ip_address": schema.StringAttribute{ - MarkdownDescription: "The external IP address of the VM instance.", - Computed: true, - }, - "gpu_model": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "The model of the GPU.", - Optional: true, - Computed: true, - }, - "gpus": schema.Int64Attribute{ - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - MarkdownDescription: "Number of GPUs", - Optional: true, - Computed: true, - Default: int64default.StaticInt64(0), - }, - "id": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "ID for VM within project", - Required: true, - Validators: []validator.String{stringvalidator.RegexMatches(regexp.MustCompile("^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$"), "must be a valid resource id e.g. my-vm")}, - }, - "internal_ip_address": schema.StringAttribute{ - MarkdownDescription: "The internal IP address of the VM instance.", - Computed: true, - }, - "machine_type": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "VM machine type, from machine type data source", - Optional: true, - Computed: true, - Validators: []validator.String{stringvalidator.RegexMatches(regexp.MustCompile("^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$"), "must be a valid resource id")}, - }, - "max_price_hr": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "The maximum price per hour for the VM instance.", - Optional: true, - }, - "memory_gib": schema.Int64Attribute{ - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - MarkdownDescription: "Amount of VM memory in GiB", - Optional: true, - }, - "networks": schema.ListNestedAttribute{ - Optional: true, - MarkdownDescription: "Network adapters for private networks", - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "network_id": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "ID of private network to attach the NIC to", - Required: true, - }, - "assign_public_ip": schema.BoolAttribute{ - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "Assign a public IP to the NIC", - Optional: true, - }, - "external_ip_address": schema.StringAttribute{ - MarkdownDescription: "The external IP address of the NIC.", - Computed: true, - }, - "internal_ip_address": schema.StringAttribute{ - MarkdownDescription: "The internal IP address of the NIC.", - Computed: true, - }, - "security_group_ids": schema.SetAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "Security groups to assign to the NIC", - }, - }, - }, - }, - "password": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "Root password for linux, or Admin password for windows", - Optional: true, - Sensitive: true, - Validators: []validator.String{stringvalidator.LengthBetween(6, 64)}, - }, - "price_hr": schema.StringAttribute{ - MarkdownDescription: "The current price per hour for the VM instance.", - Computed: true, - }, - "project_id": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "The project the VM instance is in.", - Optional: true, - }, - "renewable_energy": schema.BoolAttribute{ - MarkdownDescription: "Whether the VM instance is powered by renewable energy", - Computed: true, - }, - "security_group_ids": schema.SetAttribute{ - PlanModifiers: []planmodifier.Set{ - setplanmodifier.RequiresReplace(), - }, - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "Security groups to assign to the VM when using public networking", - }, - "ssh_key_source": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "Which SSH keys to add to the VM: project (default), user or custom", - Optional: true, - Validators: []validator.String{stringvalidator.OneOf("project", "user", "custom")}, - }, - "ssh_keys": schema.ListAttribute{ - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, - ElementType: types.StringType, - MarkdownDescription: "List of SSH keys to add to the VM, ssh_key_source must be set to custom", - Optional: true, - }, - "start_script": schema.StringAttribute{ - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - MarkdownDescription: "A script to run when VM boots", - Optional: true, - }, - "vcpus": schema.Int64Attribute{ - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - MarkdownDescription: "Number of VCPUs", - Optional: true, - Validators: []validator.Int64{int64validator.AtMost(100)}, - }, - }, - } -}