From 3f4f27d90ce0c90243650093789ae206054f00a6 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Mon, 20 Nov 2023 18:33:13 -0800 Subject: [PATCH 01/22] v1alpha5 --- crusoe/provider.go | 3 + examples/infiniband/main.tf | 2 + internal/common/util.go | 51 +---- internal/disk/disk_resource.go | 48 ++--- internal/disk/disks_data_source.go | 31 ++- internal/disk/util.go | 25 --- .../firewall_rule/firewall_rule_resource.go | 37 +--- internal/firewall_rule/util.go | 2 +- internal/firewall_rule/util_test.go | 2 +- internal/ib_network/ib_network_data_source.go | 31 ++- .../ib_partition/ib_partition_resource.go | 22 +- internal/project/project_resource.go | 202 ++++++++++++++++++ internal/project/projects_data_source.go | 99 +++++++++ internal/project/util.go | 27 +++ internal/vm/util.go | 16 +- internal/vm/vm_data_source.go | 9 +- internal/vm/vm_resource.go | 64 +++--- 17 files changed, 476 insertions(+), 195 deletions(-) delete mode 100644 internal/disk/util.go create mode 100644 internal/project/project_resource.go create mode 100644 internal/project/projects_data_source.go create mode 100644 internal/project/util.go diff --git a/crusoe/provider.go b/crusoe/provider.go index fb8d568..08f95fc 100644 --- a/crusoe/provider.go +++ b/crusoe/provider.go @@ -16,6 +16,7 @@ import ( "github.com/crusoecloud/terraform-provider-crusoe/internal/firewall_rule" "github.com/crusoecloud/terraform-provider-crusoe/internal/ib_network" "github.com/crusoecloud/terraform-provider-crusoe/internal/ib_partition" + "github.com/crusoecloud/terraform-provider-crusoe/internal/project" "github.com/crusoecloud/terraform-provider-crusoe/internal/vm" ) @@ -51,6 +52,7 @@ func (p *crusoeProvider) DataSources(_ context.Context) []func() datasource.Data vm.NewVMDataSource, disk.NewDisksDataSource, ib_network.NewIBNetworkDataSource, + project.NewProjectsDataSource, } } @@ -61,6 +63,7 @@ func (p *crusoeProvider) Resources(_ context.Context) []func() resource.Resource disk.NewDiskResource, firewall_rule.NewFirewallRuleResource, ib_partition.NewIBPartitionResource, + project.NewProjectResource, } } diff --git a/examples/infiniband/main.tf b/examples/infiniband/main.tf index 409aa33..4ebfb75 100644 --- a/examples/infiniband/main.tf +++ b/examples/infiniband/main.tf @@ -6,6 +6,8 @@ terraform { } } +default project = my_new_project + locals { my_ssh_key = file("~/.ssh/id_ed25519.pub") } diff --git a/internal/common/util.go b/internal/common/util.go index 3d1cae6..2a1a584 100644 --- a/internal/common/util.go +++ b/internal/common/util.go @@ -9,9 +9,7 @@ import ( "strings" "time" - "github.com/antihax/optional" - - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" ) const ( @@ -36,12 +34,8 @@ var ( OpInProgress opStatus = "IN_PROGRESS" OpFailed opStatus = "FAILED" - errNoOperations = errors.New("no operation with id found") errUnableToGetOpRes = errors.New("failed to get result of operation") - errAmbiguousRole = errors.New("user is associated with multiple roles - please contact support@crusoecloud.com") - errNoRoleAssociation = errors.New("user is not associated with any role") - // fallback error presented to the user in unexpected situations errUnexpected = errors.New("An unexpected error occurred. Please try again, and if the problem persists, contact support@crusoecloud.com.") ) @@ -60,46 +54,19 @@ func NewAPIClient(host, key, secret string) *swagger.APIClient { return swagger.NewAPIClient(cfg) } -// GetRole creates a get Role request and calls the API. -// This function returns a role id if the user's role can be determined -// (i.e. user only has one role, which is the case for v0). -func GetRole(ctx context.Context, api *swagger.APIClient) (string, error) { - opts := swagger.RolesApiGetRolesOpts{ - OrgId: optional.EmptyString(), - } - - resp, httpResp, err := api.RolesApi.GetRoles(ctx, &opts) - if err != nil { - return "", fmt.Errorf("could not get roles: %w", err) - } - defer httpResp.Body.Close() - - switch len(resp.Roles) { - case 0: - return "", errNoRoleAssociation - case 1: - return resp.Roles[0].Id, nil - default: - // user has multiple roles: unable to disambiguate - return "", errAmbiguousRole - } -} - // AwaitOperation polls an async API operation until it resolves into a success or failure state. -func AwaitOperation(ctx context.Context, op *swagger.Operation, - getFunc func(context.Context, string) (swagger.ListOperationsResponseV1Alpha4, *http.Response, error)) ( +func AwaitOperation(ctx context.Context, op *swagger.Operation, projectID string, + getFunc func(context.Context, string, string) (swagger.Operation, *http.Response, error)) ( *swagger.Operation, error, ) { for op.State == string(OpInProgress) { - updatedOps, httpResp, err := getFunc(ctx, op.OperationId) + updatedOps, httpResp, err := getFunc(ctx, projectID, op.OperationId) if err != nil { return nil, fmt.Errorf("error getting operation with id %s: %w", op.OperationId, err) } httpResp.Body.Close() - if len(updatedOps.Operations) == 0 { - return nil, errNoOperations - } - op = &updatedOps.Operations[0] + + op = &updatedOps time.Sleep(pollInterval) } @@ -122,10 +89,10 @@ func AwaitOperation(ctx context.Context, op *swagger.Operation, // AwaitOperationAndResolve awaits an async API operation and attempts to parse the response as an instance of T, // if the operation was successful. -func AwaitOperationAndResolve[T any](ctx context.Context, op *swagger.Operation, - getFunc func(context.Context, string) (swagger.ListOperationsResponseV1Alpha4, *http.Response, error), +func AwaitOperationAndResolve[T any](ctx context.Context, op *swagger.Operation, projectID string, + getFunc func(context.Context, string, string) (swagger.Operation, *http.Response, error), ) (*T, *swagger.Operation, error) { - op, err := AwaitOperation(ctx, op, getFunc) + op, err := AwaitOperation(ctx, op, projectID, getFunc) if err != nil { return nil, op, err } diff --git a/internal/disk/disk_resource.go b/internal/disk/disk_resource.go index 536ea90..e88e81c 100644 --- a/internal/disk/disk_resource.go +++ b/internal/disk/disk_resource.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" "github.com/crusoecloud/terraform-provider-crusoe/internal/common" validators "github.com/crusoecloud/terraform-provider-crusoe/internal/validators" ) @@ -27,6 +27,7 @@ type diskResource struct { type diskResourceModel struct { ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` Location types.String `tfsdk:"location"` Name types.String `tfsdk:"name"` Type types.String `tfsdk:"type"` @@ -67,6 +68,10 @@ func (r *diskResource) Schema(ctx context.Context, req resource.SchemaRequest, r Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates }, + "project_id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates + }, "location": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place @@ -113,20 +118,13 @@ func (r *diskResource) Create(ctx context.Context, req resource.CreateRequest, r diskType = defaultDiskType } - roleID, err := common.GetRole(ctx, r.client) - if err != nil { - resp.Diagnostics.AddError("Failed to get Role ID", err.Error()) - - return - } - dataResp, httpResp, err := r.client.DisksApi.CreateDisk(ctx, swagger.DisksPostRequest{ - RoleId: roleID, + RoleId: plan.ProjectID.ValueString(), Name: plan.Name.ValueString(), Location: plan.Location.ValueString(), Type_: diskType, Size: plan.Size.ValueString(), - }) + }, plan.ProjectID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to create disk", fmt.Sprintf("There was an error starting a create disk operation: %s", common.UnpackAPIError(err))) @@ -135,7 +133,7 @@ func (r *diskResource) Create(ctx context.Context, req resource.CreateRequest, r } defer httpResp.Body.Close() - disk, _, err := common.AwaitOperationAndResolve[swagger.Disk](ctx, dataResp.Operation, r.client.DiskOperationsApi.GetStorageDisksOperation) + disk, _, err := common.AwaitOperationAndResolve[swagger.Disk](ctx, dataResp.Operation, plan.ProjectID.ValueString(), r.client.DiskOperationsApi.GetStorageDisksOperation) if err != nil { resp.Diagnostics.AddError("Failed to create disk", fmt.Sprintf("There was an error creating a disk: %s", common.UnpackAPIError(err))) @@ -146,18 +144,7 @@ func (r *diskResource) Create(ctx context.Context, req resource.CreateRequest, r plan.ID = types.StringValue(disk.Id) plan.Type = types.StringValue(disk.Type_) plan.Location = types.StringValue(disk.Location) - - // The Serial Number is not populated in the creation response, but we can reliably fetch it immediately after - // disk creation. TODO: this request can be dropped with if the creation response is updated to include serial number - disk2, err := getDisk(ctx, r.client, disk.Id) - if err != nil { - // log a warning and not an error, because creation still worked but the serial number won't be populated - // until the next time the resource is read. - resp.Diagnostics.AddWarning("Unable to get Serial Number", - "The serial number of one of your created disks was not populated; it should be populated during the next Terraform run.") - } else { - plan.SerialNumber = types.StringValue(disk2.SerialNumber) - } + plan.SerialNumber = types.StringValue(disk.SerialNumber) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) @@ -172,7 +159,7 @@ func (r *diskResource) Read(ctx context.Context, req resource.ReadRequest, resp return } - dataResp, httpResp, err := r.client.DisksApi.GetDisks(ctx) + dataResp, httpResp, err := r.client.DisksApi.ListDisks(ctx, state.ProjectID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to get disks", fmt.Sprintf("Fetching Crusoe disks failed: %s\n\nIf the problem persists, contact support@crusoecloud.com", err.Error())) @@ -182,9 +169,9 @@ func (r *diskResource) Read(ctx context.Context, req resource.ReadRequest, resp defer httpResp.Body.Close() var disk *swagger.Disk - for i := range dataResp.Disks { - if dataResp.Disks[i].Id == state.ID.ValueString() { - disk = &dataResp.Disks[i] + for i := range dataResp.Items { + if dataResp.Items[i].Id == state.ID.ValueString() { + disk = &dataResp.Items[i] } } @@ -222,6 +209,7 @@ func (r *diskResource) Update(ctx context.Context, req resource.UpdateRequest, r dataResp, httpResp, err := r.client.DisksApi.ResizeDisk(ctx, swagger.DisksPatchRequest{Size: plan.Size.ValueString()}, + plan.ProjectID.ValueString(), plan.ID.ValueString(), ) if err != nil { @@ -234,7 +222,7 @@ func (r *diskResource) Update(ctx context.Context, req resource.UpdateRequest, r } defer httpResp.Body.Close() - _, _, err = common.AwaitOperationAndResolve[swagger.Disk](ctx, dataResp.Operation, r.client.DiskOperationsApi.GetStorageDisksOperation) + _, _, err = common.AwaitOperationAndResolve[swagger.Disk](ctx, dataResp.Operation, plan.ProjectID.ValueString(), r.client.DiskOperationsApi.GetStorageDisksOperation) if err != nil { resp.Diagnostics.AddError("Failed to resize disk", fmt.Sprintf("There was an error resizing a disk: %s.\n\n"+ @@ -257,7 +245,7 @@ func (r *diskResource) Delete(ctx context.Context, req resource.DeleteRequest, r return } - dataResp, httpResp, err := r.client.DisksApi.DeleteDisk(ctx, state.ID.ValueString()) + dataResp, httpResp, err := r.client.DisksApi.DeleteDisk(ctx, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to delete disk", fmt.Sprintf("There was an error starting a delete disk operation: %s", common.UnpackAPIError(err))) @@ -266,7 +254,7 @@ func (r *diskResource) Delete(ctx context.Context, req resource.DeleteRequest, r } defer httpResp.Body.Close() - _, err = common.AwaitOperation(ctx, dataResp.Operation, r.client.DiskOperationsApi.GetStorageDisksOperation) + _, err = common.AwaitOperation(ctx, dataResp.Operation, state.ProjectID.ValueString(), r.client.DiskOperationsApi.GetStorageDisksOperation) if err != nil { resp.Diagnostics.AddError("Failed to delete disk", fmt.Sprintf("There was an error deleting a disk: %s", common.UnpackAPIError(err))) diff --git a/internal/disk/disks_data_source.go b/internal/disk/disks_data_source.go index 218cc20..5972c43 100644 --- a/internal/disk/disks_data_source.go +++ b/internal/disk/disks_data_source.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" "github.com/crusoecloud/terraform-provider-crusoe/internal/common" ) @@ -18,6 +18,10 @@ type disksDataSourceModel struct { Disks []diskModel `tfsdk:"disks"` } +type disksDataSourceFilter struct { + ProjectID *string `tfsdk:"project_id"` +} + type diskModel struct { ID string `tfsdk:"id"` Name string `tfsdk:"name"` @@ -86,7 +90,14 @@ func (ds *disksDataSource) Schema(ctx context.Context, request datasource.Schema //nolint:gocritic // Implements Terraform defined interface func (ds *disksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - dataResp, httpResp, err := ds.client.DisksApi.GetDisks(ctx) + var config disksDataSourceFilter + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + dataResp, httpResp, err := ds.client.DisksApi.ListDisks(ctx, *config.ProjectID) if err != nil { resp.Diagnostics.AddError("Failed to Fetch Disks", "Could not fetch Disk data at this time.") @@ -95,17 +106,17 @@ func (ds *disksDataSource) Read(ctx context.Context, req datasource.ReadRequest, defer httpResp.Body.Close() var state disksDataSourceModel - for i := range dataResp.Disks { + for i := range dataResp.Items { state.Disks = append(state.Disks, diskModel{ - ID: dataResp.Disks[i].Id, - Name: dataResp.Disks[i].Name, - Location: dataResp.Disks[i].Location, - Type: dataResp.Disks[i].Type_, - Size: dataResp.Disks[i].Size, - SerialNumber: dataResp.Disks[i].SerialNumber, + ID: dataResp.Items[i].Id, + Name: dataResp.Items[i].Name, + Location: dataResp.Items[i].Location, + Type: dataResp.Items[i].Type_, + Size: dataResp.Items[i].Size, + SerialNumber: dataResp.Items[i].SerialNumber, }) } - diags := resp.State.Set(ctx, &state) + diags = resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) } diff --git a/internal/disk/util.go b/internal/disk/util.go deleted file mode 100644 index 8bad96e..0000000 --- a/internal/disk/util.go +++ /dev/null @@ -1,25 +0,0 @@ -package disk - -import ( - "context" - "errors" - - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" -) - -// getDisk fetches a storage disk by ID. -func getDisk(ctx context.Context, apiClient *swagger.APIClient, diskID string) (*swagger.Disk, error) { - dataResp, httpResp, err := apiClient.DisksApi.GetDisk(ctx, diskID) - if err != nil { - return nil, err - } - defer httpResp.Body.Close() - - for i := range dataResp.Disks { - if dataResp.Disks[i].Id == diskID { - return &dataResp.Disks[i], nil - } - } - - return nil, errors.New("failed to fetch disk with matching ID") -} diff --git a/internal/firewall_rule/firewall_rule_resource.go b/internal/firewall_rule/firewall_rule_resource.go index db59805..b4640fa 100644 --- a/internal/firewall_rule/firewall_rule_resource.go +++ b/internal/firewall_rule/firewall_rule_resource.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" "github.com/crusoecloud/terraform-provider-crusoe/internal/common" validators "github.com/crusoecloud/terraform-provider-crusoe/internal/validators" ) @@ -23,6 +23,7 @@ type firewallRuleResource struct { type firewallRuleResourceModel struct { ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` Name types.String `tfsdk:"name"` Network types.String `tfsdk:"network"` Action types.String `tfsdk:"action"` @@ -117,19 +118,10 @@ func (r *firewallRuleResource) Create(ctx context.Context, req resource.CreateRe if resp.Diagnostics.HasError() { return } - - roleID, err := common.GetRole(ctx, r.client) - if err != nil { - resp.Diagnostics.AddError("Failed to get Role ID", err.Error()) - - return - } - sourcePortsStr := strings.ReplaceAll(plan.SourcePorts.ValueString(), "*", "1-65535") destPortsStr := strings.ReplaceAll(plan.DestinationPorts.ValueString(), "*", "1-65535") - dataResp, httpResp, err := r.client.VPCFirewallRulesApi.CreateVPCFirewallRule(ctx, swagger.VpcFirewallRulesPostRequestV1Alpha4{ - RoleId: roleID, + dataResp, httpResp, err := r.client.VPCFirewallRulesApi.CreateVPCFirewallRule(ctx, swagger.VpcFirewallRulesPostRequestV1Alpha5{ VpcNetworkId: plan.Network.ValueString(), Name: plan.Name.ValueString(), Action: plan.Action.ValueString(), @@ -139,7 +131,7 @@ func (r *firewallRuleResource) Create(ctx context.Context, req resource.CreateRe SourcePorts: stringToSlice(sourcePortsStr, ","), Destinations: []swagger.FirewallRuleObject{toFirewallRuleObject(plan.Destination.ValueString())}, DestinationPorts: stringToSlice(destPortsStr, ","), - }) + }, plan.ProjectID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to create firewall rule", fmt.Sprintf("There was an error starting a create firewall rule operation: %s", common.UnpackAPIError(err))) @@ -149,7 +141,7 @@ func (r *firewallRuleResource) Create(ctx context.Context, req resource.CreateRe defer httpResp.Body.Close() firewallRule, _, err := common.AwaitOperationAndResolve[swagger.VpcFirewallRule]( - ctx, dataResp.Operation, + ctx, dataResp.Operation, plan.ProjectID.ValueString(), r.client.VPCFirewallRuleOperationsApi.GetNetworkingVPCFirewallRulesOperation) if err != nil { resp.Diagnostics.AddError("Failed to create firewall rule", @@ -173,8 +165,8 @@ func (r *firewallRuleResource) Read(ctx context.Context, req resource.ReadReques return } - dataResp, httpResp, err := r.client.VPCFirewallRulesApi.GetVPCFirewallRule(ctx, state.ID.ValueString()) - if err != nil || len(dataResp.FirewallRules) == 0 { + rule, httpResp, err := r.client.VPCFirewallRulesApi.GetVPCFirewallRule(ctx, state.ProjectID.ValueString(), state.ID.ValueString()) + if err != nil { // fw rule has most likely been deleted out of band, so we update Terraform state to match resp.State.RemoveResource(ctx) @@ -185,14 +177,6 @@ func (r *firewallRuleResource) Read(ctx context.Context, req resource.ReadReques return } - if len(dataResp.FirewallRules) > 0 { - // should never happen - resp.Diagnostics.AddWarning("Found multiple matching firewall rules", - "An unexpected number of matching firewall rules was found. If you're seeing this error message, "+ - "please report an issue to support@crusoecloud.com") - } - - rule := dataResp.FirewallRules[0] state.ID = types.StringValue(rule.Id) state.Name = types.StringValue(rule.Name) state.Network = types.StringValue(rule.VpcNetworkId) @@ -245,6 +229,7 @@ func (r *firewallRuleResource) Update(ctx context.Context, req resource.UpdateRe dataResp, httpResp, err := r.client.VPCFirewallRulesApi.PatchVPCFirewallRule(ctx, patchReq, + plan.ProjectID.ValueString(), plan.ID.ValueString(), ) if err != nil { @@ -256,7 +241,7 @@ func (r *firewallRuleResource) Update(ctx context.Context, req resource.UpdateRe defer httpResp.Body.Close() - _, _, err = common.AwaitOperationAndResolve[swagger.VpcFirewallRule](ctx, dataResp.Operation, r.client.VPCFirewallRuleOperationsApi.GetNetworkingVPCFirewallRulesOperation) + _, _, err = common.AwaitOperationAndResolve[swagger.VpcFirewallRule](ctx, dataResp.Operation, plan.ProjectID.ValueString(), r.client.VPCFirewallRuleOperationsApi.GetNetworkingVPCFirewallRulesOperation) if err != nil { resp.Diagnostics.AddError("Failed to patch firewall rule", fmt.Sprintf("There was an error updating the firewall rule: %s.", common.UnpackAPIError(err))) @@ -277,7 +262,7 @@ func (r *firewallRuleResource) Delete(ctx context.Context, req resource.DeleteRe return } - dataResp, httpResp, err := r.client.VPCFirewallRulesApi.DeleteVPCFirewallRule(ctx, state.ID.ValueString()) + dataResp, httpResp, err := r.client.VPCFirewallRulesApi.DeleteVPCFirewallRule(ctx, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to delete firewall rule", fmt.Sprintf("There was an error starting a delete firewall rule operation: %s", common.UnpackAPIError(err))) @@ -286,7 +271,7 @@ func (r *firewallRuleResource) Delete(ctx context.Context, req resource.DeleteRe } defer httpResp.Body.Close() - _, err = common.AwaitOperation(ctx, dataResp.Operation, r.client.VPCFirewallRuleOperationsApi.GetNetworkingVPCFirewallRulesOperation) + _, err = common.AwaitOperation(ctx, dataResp.Operation, state.ProjectID.ValueString(), r.client.VPCFirewallRuleOperationsApi.GetNetworkingVPCFirewallRulesOperation) if err != nil { resp.Diagnostics.AddError("Failed to delete firewall rule", fmt.Sprintf("There was an error deleting a firewall rule: %s", common.UnpackAPIError(err))) diff --git a/internal/firewall_rule/util.go b/internal/firewall_rule/util.go index 723dc8c..48becfc 100644 --- a/internal/firewall_rule/util.go +++ b/internal/firewall_rule/util.go @@ -4,7 +4,7 @@ import ( "regexp" "strings" - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" ) var whitespaceRegex = regexp.MustCompile(`\s*`) diff --git a/internal/firewall_rule/util_test.go b/internal/firewall_rule/util_test.go index 23b1892..11e250a 100644 --- a/internal/firewall_rule/util_test.go +++ b/internal/firewall_rule/util_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" ) func Test_cidrListToString(t *testing.T) { diff --git a/internal/ib_network/ib_network_data_source.go b/internal/ib_network/ib_network_data_source.go index 7355c2f..c71ccf7 100644 --- a/internal/ib_network/ib_network_data_source.go +++ b/internal/ib_network/ib_network_data_source.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" "github.com/crusoecloud/terraform-provider-crusoe/internal/common" ) @@ -20,10 +20,15 @@ type ibNetworksDataSourceModel struct { IBNetworks []ibNetworkModel `tfsdk:"ib_networks"` } +type ibNetworksDataSourceFilter struct { + ProjectID *string `tfsdk:"project_id"` +} + type ibNetworkModel struct { - ID string `tfsdk:"id"` - Name string `tfsdk:"name"` - Location string `tfsdk:"location"` + ID string `tfsdk:"id"` + ProjectID string `tfsdk:"project_id"` + Name string `tfsdk:"name"` + Location string `tfsdk:"location"` } func NewIBNetworkDataSource() datasource.DataSource { @@ -72,7 +77,13 @@ func (ds *ibNetworksDataSource) Schema(ctx context.Context, request datasource.S } func (ds *ibNetworksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - dataResp, httpResp, err := ds.client.IBNetworksApi.GetIBNetworks(ctx) + var config ibNetworksDataSourceFilter + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + dataResp, httpResp, err := ds.client.IBNetworksApi.ListIBNetworks(ctx, *config.ProjectID) if err != nil { resp.Diagnostics.AddError("Failed to Fetch IB Networks", fmt.Sprintf("Could not fetch Infiniband network data at this time: %s", common.UnpackAPIError(err))) @@ -82,14 +93,14 @@ func (ds *ibNetworksDataSource) Read(ctx context.Context, req datasource.ReadReq defer httpResp.Body.Close() var state ibNetworksDataSourceModel - for i := range dataResp.IbNetworks { + for i := range dataResp.Items { state.IBNetworks = append(state.IBNetworks, ibNetworkModel{ - ID: dataResp.IbNetworks[i].Id, - Name: dataResp.IbNetworks[i].Name, - Location: dataResp.IbNetworks[i].Location, + ID: dataResp.Items[i].Id, + Name: dataResp.Items[i].Name, + Location: dataResp.Items[i].Location, }) } - diags := resp.State.Set(ctx, &state) + diags = resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) } diff --git a/internal/ib_partition/ib_partition_resource.go b/internal/ib_partition/ib_partition_resource.go index 3b1f487..dd95816 100644 --- a/internal/ib_partition/ib_partition_resource.go +++ b/internal/ib_partition/ib_partition_resource.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" "github.com/crusoecloud/terraform-provider-crusoe/internal/common" ) @@ -24,6 +24,7 @@ type ibPartitionResource struct { type ibPartitionResourceModel struct { ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` Name types.String `tfsdk:"name"` IBNetworkID types.String `tfsdk:"ib_network_id"` } @@ -66,6 +67,10 @@ func (r *ibPartitionResource) Schema(ctx context.Context, req resource.SchemaReq Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place }, + "project_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place + }, }, } } @@ -82,18 +87,11 @@ func (r *ibPartitionResource) Create(ctx context.Context, req resource.CreateReq return } - roleID, err := common.GetRole(ctx, r.client) - if err != nil { - resp.Diagnostics.AddError("Failed to get Role ID", err.Error()) - - return - } - dataResp, httpResp, err := r.client.IBPartitionsApi.CreateIBPartition(ctx, swagger.IbPartitionsPostRequestV1Alpha4{ - RoleId: roleID, + RoleId: plan.ProjectID.ValueString(), Name: plan.Name.ValueString(), IbNetworkId: plan.IBNetworkID.ValueString(), - }) + }, plan.ProjectID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to create partition", fmt.Sprintf("There was an error creating an Infiniband partition: %s", common.UnpackAPIError(err))) @@ -116,7 +114,7 @@ func (r *ibPartitionResource) Read(ctx context.Context, req resource.ReadRequest return } - partition, httpResp, err := r.client.IBPartitionsApi.GetIBPartition(ctx, state.ID.ValueString()) + partition, httpResp, err := r.client.IBPartitionsApi.GetIBPartition(ctx, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { if err.Error() == notFoundMessage { // partition has most likely been deleted out of band, so we update Terraform state to match @@ -156,7 +154,7 @@ func (r *ibPartitionResource) Delete(ctx context.Context, req resource.DeleteReq return } - httpResp, err := r.client.IBPartitionsApi.DeleteIBPartition(ctx, state.ID.ValueString()) + httpResp, err := r.client.IBPartitionsApi.DeleteIBPartition(ctx, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to delete partition", fmt.Sprintf("There was an error deleting an Infiniband partition: %s", common.UnpackAPIError(err))) diff --git a/internal/project/project_resource.go b/internal/project/project_resource.go new file mode 100644 index 0000000..c3b1bc5 --- /dev/null +++ b/internal/project/project_resource.go @@ -0,0 +1,202 @@ +package project + +import ( + "context" + "fmt" + + "github.com/antihax/optional" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" + "github.com/crusoecloud/terraform-provider-crusoe/internal/common" +) + +type projectResource struct { + client *swagger.APIClient +} + +type projectResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` +} + +func NewProjectResource() resource.Resource { + return &projectResource{} +} + +//nolint:gocritic // Implements Terraform defined interface +func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*swagger.APIClient) + if !ok { + resp.Diagnostics.AddError("Failed to initialize provider", common.ErrorMsgProviderInitFailed) + + return + } + + r.client = client +} + +//nolint:gocritic // Implements Terraform defined interface +func (r *projectResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project" +} + +//nolint:gocritic // Implements Terraform defined interface +func (r *projectResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates + }, + "name": schema.StringAttribute{ + Required: true, + }, + }, + } +} + +func (r *projectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +//nolint:gocritic // Implements Terraform defined interface +func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan projectResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + orgID, err := getUserOrg(ctx, r.client) + if err != nil { + resp.Diagnostics.AddError("Failed to create project", + fmt.Sprintf("There was an error starting a create project operation: %s", err)) + + return + } + dataResp, httpResp, err := r.client.ProjectsApi.CreateProject(ctx, swagger.ProjectsPostRequest{ + Name: plan.Name.ValueString(), + OrganizationId: orgID, + }) + if err != nil { + resp.Diagnostics.AddError("Failed to create project", + fmt.Sprintf("There was an error starting a create project operation: %s", common.UnpackAPIError(err))) + + return + } + defer httpResp.Body.Close() + + project := dataResp.Project + + plan.ID = types.StringValue(project.Id) + plan.Name = types.StringValue(project.Name) + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} + +//nolint:gocritic // Implements Terraform defined interface +func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state projectResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + opts := &swagger.ProjectsApiGetProjectsOpts{ + OrgId: optional.EmptyString(), + } + + dataResp, httpResp, err := r.client.ProjectsApi.GetProjects(ctx, opts) + if err != nil { + resp.Diagnostics.AddError("Failed to get projects", + fmt.Sprintf("Fetching Crusoe projects failed: %s\n\nIf the problem persists, contact support@crusoecloud.com", err.Error())) + + return + } + defer httpResp.Body.Close() + + var project *swagger.Project + + for i := range dataResp.Items { + if dataResp.Items[i].Id == state.ID.ValueString() { + project = &dataResp.Items[i] + } + } + + if project == nil { + // disk has most likely been deleted out of band, so we update Terraform state to match + resp.State.RemoveResource(ctx) + + return + } + + state.Name = types.StringValue(project.Name) + state.ID = types.StringValue(project.Id) + + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) +} + +//nolint:gocritic // Implements Terraform defined interface +func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var state projectResource + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var plan projectResourceModel + diags = req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + _, httpResp, err := r.client.ProjectsApi.UpdateProject(ctx, + swagger.ProjectsPutRequest{Name: plan.Name.ValueString()}, + plan.ID.ValueString(), + ) + if err != nil { + resp.Diagnostics.AddError("Failed to update project", + fmt.Sprintf("There was an error starting an update project operation: %s.", err.Error())) + + return + } + defer httpResp.Body.Close() + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} + +//nolint:gocritic // Implements Terraform defined interface +func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state projectResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + httpResp, err := r.client.ProjectsApi.DeleteProject(ctx, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Failed to delete project", + fmt.Sprintf("There was an error starting a delete project operation: %s", common.UnpackAPIError(err))) + + return + } + defer httpResp.Body.Close() +} diff --git a/internal/project/projects_data_source.go b/internal/project/projects_data_source.go new file mode 100644 index 0000000..e4cdc6a --- /dev/null +++ b/internal/project/projects_data_source.go @@ -0,0 +1,99 @@ +package project + +import ( + "context" + + "github.com/antihax/optional" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" + "github.com/crusoecloud/terraform-provider-crusoe/internal/common" +) + +type projectsDataSource struct { + client *swagger.APIClient +} + +type projectsDataSourceModel struct { + Projects []projectModel `tfsdk:"projects"` +} + +type projectModel struct { + ID string `tfsdk:"id"` + Name string `tfsdk:"name"` + Location string `tfsdk:"location"` + Type string `tfsdk:"type"` + Size string `tfsdk:"size"` + SerialNumber string `tfsdk:"serial_number"` +} + +func NewProjectsDataSource() datasource.DataSource { + return &projectsDataSource{} +} + +// Configure adds the provider configured client to the data source. +func (ds *projectsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*swagger.APIClient) + if !ok { + resp.Diagnostics.AddError("Failed to initialize provider", common.ErrorMsgProviderInitFailed) + + return + } + + ds.client = client +} + +//nolint:gocritic // Implements Terraform defined interface +func (ds *projectsDataSource) Metadata(ctx context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_projects" +} + +//nolint:gocritic // Implements Terraform defined interface +func (ds *projectsDataSource) Schema(ctx context.Context, request datasource.SchemaRequest, response *datasource.SchemaResponse) { + response.Schema = schema.Schema{Attributes: map[string]schema.Attribute{ + "projects": schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }} +} + +//nolint:gocritic // Implements Terraform defined interface +func (ds *projectsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + opts := &swagger.ProjectsApiGetProjectsOpts{ + OrgId: optional.EmptyString(), + } + + dataResp, httpResp, err := ds.client.ProjectsApi.GetProjects(ctx, opts) + if err != nil { + resp.Diagnostics.AddError("Failed to Fetch Projects", "Could not fetch Project data at this time.") + + return + } + defer httpResp.Body.Close() + + var state projectsDataSourceModel + for i := range dataResp.Items { + state.Projects = append(state.Projects, projectModel{ + ID: dataResp.Items[i].Id, + Name: dataResp.Items[i].Name, + }) + } + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/project/util.go b/internal/project/util.go new file mode 100644 index 0000000..61b9c6c --- /dev/null +++ b/internal/project/util.go @@ -0,0 +1,27 @@ +package project + +import ( + "context" + "errors" + + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" +) + +// getUserOrg returns the organization id for the authenticated user. +func getUserOrg(ctx context.Context, apiClient *swagger.APIClient) (string, error) { + dataResp, httpResp, err := apiClient.EntitiesApi.GetOrganizations(ctx) + if err != nil { + return "", err + } + defer httpResp.Body.Close() + + entities := dataResp.Entities + switch len(entities) { + case 0: + return "", errors.New("user does not belong to any organizations") + case 1: + return entities[0].Id, nil + default: + return "", errors.New("user belongs to multiple organizations") + } +} diff --git a/internal/vm/util.go b/internal/vm/util.go index ed5a7f4..15b8664 100644 --- a/internal/vm/util.go +++ b/internal/vm/util.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" "github.com/crusoecloud/terraform-provider-crusoe/internal/common" ) @@ -37,7 +37,7 @@ var vmNetworkInterfaceSchema = types.ObjectType{ // getDisksDiff compares the disks attached to two VM resource models and returns // a diff of disks defined by disk ID. -func getDisksDiff(origDisks, newDisks []vmDiskResourceModel) (disksAdded, disksRemoved []string) { +func getDisksDiff(origDisks, newDisks []vmDiskResourceModel) (disksAdded []swagger.DiskAttachment, disksRemoved []string) { for _, newDisk := range newDisks { matched := false for _, origDisk := range origDisks { @@ -48,7 +48,7 @@ func getDisksDiff(origDisks, newDisks []vmDiskResourceModel) (disksAdded, disksR } } if !matched { - disksAdded = append(disksAdded, newDisk.ID) + disksAdded = append(disksAdded, swagger.DiskAttachment{DiskId: newDisk.ID, AttachmentType: newDisk.AttachmentType}) } } @@ -69,18 +69,14 @@ func getDisksDiff(origDisks, newDisks []vmDiskResourceModel) (disksAdded, disksR return disksAdded, disksRemoved } -func getVM(ctx context.Context, apiClient *swagger.APIClient, vmID string) (*swagger.InstanceV1Alpha4, error) { - dataResp, httpResp, err := apiClient.VMsApi.GetInstance(ctx, vmID) +func getVM(ctx context.Context, apiClient *swagger.APIClient, projectID, vmID string) (*swagger.InstanceV1Alpha5, error) { + dataResp, httpResp, err := apiClient.VMsApi.GetInstance(ctx, projectID, vmID) if err != nil { return nil, fmt.Errorf("failed to find VM: %w", common.UnpackAPIError(err)) } defer httpResp.Body.Close() - if dataResp.Instance != nil { - return dataResp.Instance, nil - } - - return nil, fmt.Errorf("failed to find VM with matching ID: %w", err) + return &dataResp, nil } // vmNetworkInterfacesToTerraformDataModel creates a slice of Terraform-compatible network diff --git a/internal/vm/vm_data_source.go b/internal/vm/vm_data_source.go index 43f8352..d303758 100644 --- a/internal/vm/vm_data_source.go +++ b/internal/vm/vm_data_source.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" "github.com/crusoecloud/terraform-provider-crusoe/internal/common" ) @@ -20,6 +20,7 @@ type vmDataSource struct { type vmDataSourceFilter struct { ID *string `tfsdk:"id"` + ProjectID *string `tfsdk:"project_id"` Name *string `tfsdk:"name"` Type *string `tfsdk:"type"` Disks []vmDiskResourceModel `tfsdk:"disks"` @@ -73,6 +74,9 @@ func (ds *vmDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, re "name": schema.StringAttribute{ Optional: true, }, + "project_id": schema.StringAttribute{ + Optional: true, + }, "type": schema.StringAttribute{ Computed: true, }, @@ -141,7 +145,7 @@ func (ds *vmDataSource) Read(ctx context.Context, req datasource.ReadRequest, re } if config.ID != nil { - vm, err := getVM(ctx, ds.client, *config.ID) + vm, err := getVM(ctx, ds.client, *config.ProjectID, *config.ID) if err != nil { resp.Diagnostics.AddError("Failed to get Instance", err.Error()) @@ -149,6 +153,7 @@ func (ds *vmDataSource) Read(ctx context.Context, req datasource.ReadRequest, re } state.ID = &vm.Id + state.ProjectID = &vm.ProjectId state.Name = &vm.Name state.Type = &vm.ProductName diff --git a/internal/vm/vm_resource.go b/internal/vm/vm_resource.go index a5fadf9..10e5c95 100644 --- a/internal/vm/vm_resource.go +++ b/internal/vm/vm_resource.go @@ -14,7 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - swagger "github.com/crusoecloud/client-go/swagger/v1alpha4" + swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" "github.com/crusoecloud/terraform-provider-crusoe/internal/common" validators "github.com/crusoecloud/terraform-provider-crusoe/internal/validators" ) @@ -25,6 +25,7 @@ type vmResource struct { type vmResourceModel struct { ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` Name types.String `tfsdk:"name"` Type types.String `tfsdk:"type"` SSHKey types.String `tfsdk:"ssh_key"` @@ -54,7 +55,8 @@ type vmPublicIPv4ResourceModel struct { } type vmDiskResourceModel struct { - ID string `tfsdk:"id"` + ID string `tfsdk:"id"` + AttachmentType string `tfsdk:"attachment_type"` } func NewVMResource() resource.Resource { @@ -91,6 +93,19 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place }, + "project_id": schema.ListNestedAttribute{ + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Optional: true, + }, + "name": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, "type": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place @@ -128,6 +143,9 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res "id": schema.StringAttribute{ Optional: true, }, + "attachment_type": schema.StringAttribute{ + Optional: true, + }, }, }, }, @@ -211,13 +229,6 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res return } - roleID, err := common.GetRole(ctx, r.client) - if err != nil { - resp.Diagnostics.AddError("Failed to get Role ID", err.Error()) - - return - } - diskIds := make([]string, 0, len(plan.Disks)) for _, d := range plan.Disks { diskIds = append(diskIds, d.ID) @@ -242,7 +253,7 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res } dataResp, httpResp, err := r.client.VMsApi.CreateInstance(ctx, swagger.InstancesPostRequestV1Alpha4{ - RoleId: roleID, + RoleId: plan.ProjectID.ValueString(), Name: plan.Name.ValueString(), ProductName: plan.Type.ValueString(), Location: plan.Location.ValueString(), @@ -253,7 +264,7 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res IbPartitionId: plan.IBPartitionID.ValueString(), NetworkInterfaces: newNetworkInterfaces, Disks: diskIds, - }) + }, plan.ProjectID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to create instance", fmt.Sprintf("There was an error starting a create instance operation: %s", common.UnpackAPIError(err))) @@ -262,8 +273,8 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res } defer httpResp.Body.Close() - instance, _, err := common.AwaitOperationAndResolve[swagger.InstanceV1Alpha4]( - ctx, dataResp.Operation, r.client.VMOperationsApi.GetComputeVMsInstancesOperation) + instance, _, err := common.AwaitOperationAndResolve[swagger.InstanceV1Alpha5]( + ctx, dataResp.Operation, plan.ProjectID.ValueString(), r.client.VMOperationsApi.GetComputeVMsInstancesOperation) if err != nil { resp.Diagnostics.AddError("Failed to create instance", fmt.Sprintf("There was an error creating a instance: %s", common.UnpackAPIError(err))) @@ -289,7 +300,7 @@ func (r *vmResource) Read(ctx context.Context, req resource.ReadRequest, resp *r return } - instance, err := getVM(ctx, r.client, state.ID.ValueString()) + instance, err := getVM(ctx, r.client, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil || instance == nil { // instance has most likely been deleted out of band, so we update Terraform state to match resp.State.RemoveResource(ctx) @@ -341,9 +352,9 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res // attach/detach disks if requested addedDisks, removedDisks := getDisksDiff(state.Disks, plan.Disks) if len(addedDisks) > 0 { - attachResp, httpResp, err := r.client.VMsApi.UpdateInstanceAttachDisks(ctx, swagger.InstancesAttachDiskPostRequestV1Alpha4{ + attachResp, httpResp, err := r.client.VMsApi.UpdateInstanceAttachDisks(ctx, swagger.InstancesAttachDiskPostRequestV1Alpha5{ AttachDisks: addedDisks, - }, state.ID.ValueString()) + }, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to attach disk", fmt.Sprintf("There was an error starting an attach disk operation: %s", common.UnpackAPIError(err))) @@ -352,7 +363,7 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res } defer httpResp.Body.Close() - _, err = common.AwaitOperation(ctx, attachResp.Operation, r.client.VMOperationsApi.GetComputeVMsInstancesOperation) + _, err = common.AwaitOperation(ctx, attachResp.Operation, plan.ProjectID.ValueString(), r.client.VMOperationsApi.GetComputeVMsInstancesOperation) if err != nil { resp.Diagnostics.AddError("Failed to attach disk", fmt.Sprintf("There was an error attaching a disk: %s", common.UnpackAPIError(err))) @@ -362,14 +373,14 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res if len(removedDisks) > 0 { detachResp, httpResp, err := r.client.VMsApi.UpdateInstanceDetachDisks(ctx, swagger.InstancesDetachDiskPostRequest{ DetachDisks: removedDisks, - }, state.ID.ValueString()) + }, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to detach disk", fmt.Sprintf("There was an error starting a detach disk operation: %s", common.UnpackAPIError(err))) } defer httpResp.Body.Close() - _, err = common.AwaitOperation(ctx, detachResp.Operation, r.client.VMOperationsApi.GetComputeVMsInstancesOperation) + _, err = common.AwaitOperation(ctx, detachResp.Operation, plan.ProjectID.ValueString(), r.client.VMOperationsApi.GetComputeVMsInstancesOperation) if err != nil { resp.Diagnostics.AddError("Failed to detach disk", fmt.Sprintf("There was an error detaching a disk: %s", common.UnpackAPIError(err))) @@ -388,7 +399,7 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res // handle toggling static/dynamic public IPs if !plan.NetworkInterfaces.IsUnknown() && len(plan.NetworkInterfaces.Elements()) == 1 { // instances must be running to toggle static public IP - instance, httpResp, err := r.client.VMsApi.GetInstance(ctx, state.ID.ValueString()) + instance, httpResp, err := r.client.VMsApi.GetInstance(ctx, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to update instance network interface", fmt.Sprintf("There was an error fetching the instance's current state: %v", err)) @@ -396,7 +407,7 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res return } defer httpResp.Body.Close() - if instance.Instance.State != StateRunning { + if instance.State != StateRunning { resp.Diagnostics.AddError("Cannot update instance network interface", "The instance needs to be running before updating its public IP address.") @@ -416,7 +427,7 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res }, }}, }}, - }, state.ID.ValueString()) + }, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to update instance network interface", fmt.Sprintf("There was an error requesting to update the instance's network interface: %v", err)) @@ -425,7 +436,7 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res } defer httpResp.Body.Close() - _, err = common.AwaitOperation(ctx, patchResp.Operation, r.client.VMOperationsApi.GetComputeVMsInstancesOperation) + _, err = common.AwaitOperation(ctx, patchResp.Operation, state.ProjectID.ValueString(), r.client.VMOperationsApi.GetComputeVMsInstancesOperation) if err != nil { resp.Diagnostics.AddError("Failed to update instance network interface", fmt.Sprintf("There was an error updating the instance's network interfaces: %s", common.UnpackAPIError(err))) @@ -448,14 +459,14 @@ func (r *vmResource) Delete(ctx context.Context, req resource.DeleteRequest, res return } - _, err := getVM(ctx, r.client, state.ID.ValueString()) + _, err := getVM(ctx, r.client, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to find instance", "Could not find a matching VM instance.") return } - delDataResp, delHttpResp, err := r.client.VMsApi.DeleteInstance(ctx, state.ID.ValueString()) + delDataResp, delHttpResp, err := r.client.VMsApi.DeleteInstance(ctx, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to delete instance", fmt.Sprintf("There was an error starting a delete instance operation: %s", common.UnpackAPIError(err))) @@ -464,7 +475,8 @@ func (r *vmResource) Delete(ctx context.Context, req resource.DeleteRequest, res } defer delHttpResp.Body.Close() - _, _, err = common.AwaitOperationAndResolve[interface{}](ctx, delDataResp.Operation, r.client.VMOperationsApi.GetComputeVMsInstancesOperation) + _, _, err = common.AwaitOperationAndResolve[interface{}](ctx, delDataResp.Operation, state.ProjectID.ValueString(), + r.client.VMOperationsApi.GetComputeVMsInstancesOperation) if err != nil { resp.Diagnostics.AddError("Failed to delete instance", fmt.Sprintf("There was an error deleting an instance: %s", common.UnpackAPIError(err))) From d68db58ea182cdcce8021b01059edc8da5b7781c Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Mon, 20 Nov 2023 18:54:43 -0800 Subject: [PATCH 02/22] update example --- examples/vms/main.tf | 8 +++++++- internal/disk/disk_resource.go | 2 +- internal/vm/vm_resource.go | 15 +++------------ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/examples/vms/main.tf b/examples/vms/main.tf index 82b6213..452aab9 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -7,7 +7,11 @@ terraform { } locals { - my_ssh_key = file("~/.ssh/id_ed25519.pub") + my_ssh_key = file("~/.ssh/id_rsa.pub") +} + +resource "crusoe_project" "my_project"{ + name = "my-new-cool-project" } // new VM @@ -21,6 +25,7 @@ resource "crusoe_compute_instance" "my_vm" { ssh_key = local.my_ssh_key startup_script = file("startup.sh") + project_id = crusoe_project.my_project.id disks = [ // attached at startup @@ -33,6 +38,7 @@ resource "crusoe_storage_disk" "data_disk" { name = "data-disk" size = "200GiB" location = "us-northcentral1-a" + project_id = crusoe_project.my_project.id } // firewall rule diff --git a/internal/disk/disk_resource.go b/internal/disk/disk_resource.go index e88e81c..db0d005 100644 --- a/internal/disk/disk_resource.go +++ b/internal/disk/disk_resource.go @@ -69,7 +69,7 @@ func (r *diskResource) Schema(ctx context.Context, req resource.SchemaRequest, r PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates }, "project_id": schema.StringAttribute{ - Computed: true, + Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates }, "location": schema.StringAttribute{ diff --git a/internal/vm/vm_resource.go b/internal/vm/vm_resource.go index 10e5c95..c1b3d01 100644 --- a/internal/vm/vm_resource.go +++ b/internal/vm/vm_resource.go @@ -93,18 +93,9 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place }, - "project_id": schema.ListNestedAttribute{ - Required: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Optional: true, - }, - "name": schema.StringAttribute{ - Optional: true, - }, - }, - }, + "project_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place }, "type": schema.StringAttribute{ Required: true, From 71c19b4cb3f8f0f6815df77f2e949e38ef7370d2 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Mon, 20 Nov 2023 19:12:03 -0800 Subject: [PATCH 03/22] update examples --- examples/infiniband/main.tf | 7 ++++-- examples/vms/main.tf | 22 +++++-------------- .../firewall_rule/firewall_rule_resource.go | 4 ++++ internal/ib_network/ib_network_data_source.go | 2 -- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/examples/infiniband/main.tf b/examples/infiniband/main.tf index 4ebfb75..325a3e3 100644 --- a/examples/infiniband/main.tf +++ b/examples/infiniband/main.tf @@ -6,10 +6,10 @@ terraform { } } -default project = my_new_project locals { - my_ssh_key = file("~/.ssh/id_ed25519.pub") + my_project_id = "d2ee27ac-52db-487a-ba7f-99c43bf159b2" + my_ssh_key = file("~/.ssh/id_rsa.pub") } # list IB networks @@ -26,6 +26,7 @@ resource "crusoe_ib_partition" "my_partition" { # above. alternatively, they can be obtain with the CLI by # crusoe networking ib-network list ib_network_id = "" + project_id = local.my_project_id } # create two VMs, both in the same Infiniband partition @@ -38,6 +39,7 @@ resource "crusoe_compute_instance" "my_vm1" { image = "ubuntu20.04-nvidia-sxm-docker:latest" # IB image, see full list at https://docs.crusoecloud.com/compute/images/overview/index.html#list-of-curated-images ib_partition_id = crusoe_ib_partition.my_partition.id ssh_key = local.my_ssh_key + project_id = local.my_project_id disks = [ // disk attached at startup @@ -50,4 +52,5 @@ resource "crusoe_storage_disk" "data_disk" { name = "data-disk" size = "1TiB" location = "us-east1-a" + project_id = local.my_project_id } diff --git a/examples/vms/main.tf b/examples/vms/main.tf index 452aab9..cc3921d 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -8,37 +8,26 @@ terraform { locals { my_ssh_key = file("~/.ssh/id_rsa.pub") + my_project_id = "d2ee27ac-52db-487a-ba7f-99c43bf159b2" } -resource "crusoe_project" "my_project"{ - name = "my-new-cool-project" +resource "crusoe_project" "my_cool_project2" { + name = "my_cool_project2" } // new VM resource "crusoe_compute_instance" "my_vm" { name = "my-new-vm" type = "a40.1x" - location = "us-northcentral1-a" + location = "us-northcentralstaging1-a" # optionally specify a different base image #image = "nvidia-docker" ssh_key = local.my_ssh_key startup_script = file("startup.sh") - project_id = crusoe_project.my_project.id + project_id = crusoe_project.my_cool_project2.id - disks = [ - // attached at startup - crusoe_storage_disk.data_disk - ] -} - -// attached disk -resource "crusoe_storage_disk" "data_disk" { - name = "data-disk" - size = "200GiB" - location = "us-northcentral1-a" - project_id = crusoe_project.my_project.id } // firewall rule @@ -53,4 +42,5 @@ resource "crusoe_vpc_firewall_rule" "open_fw_rule" { source_ports = "1-65535" destination = crusoe_compute_instance.my_vm.network_interfaces[0].public_ipv4.address destination_ports = "1-65535" + project_id = crusoe_project.my_cool_project2.id } diff --git a/internal/firewall_rule/firewall_rule_resource.go b/internal/firewall_rule/firewall_rule_resource.go index b4640fa..b090e02 100644 --- a/internal/firewall_rule/firewall_rule_resource.go +++ b/internal/firewall_rule/firewall_rule_resource.go @@ -68,6 +68,10 @@ func (r *firewallRuleResource) Schema(ctx context.Context, req resource.SchemaRe Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates }, + "project_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, "network": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, diff --git a/internal/ib_network/ib_network_data_source.go b/internal/ib_network/ib_network_data_source.go index c71ccf7..1d45d02 100644 --- a/internal/ib_network/ib_network_data_source.go +++ b/internal/ib_network/ib_network_data_source.go @@ -4,7 +4,6 @@ package ib_network import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -26,7 +25,6 @@ type ibNetworksDataSourceFilter struct { type ibNetworkModel struct { ID string `tfsdk:"id"` - ProjectID string `tfsdk:"project_id"` Name string `tfsdk:"name"` Location string `tfsdk:"location"` } From a1bb03199825bd27a5b64db28d9b62d135d9e0d4 Mon Sep 17 00:00:00 2001 From: andres gutierrez Date: Mon, 20 Nov 2023 21:39:43 -0800 Subject: [PATCH 04/22] update deps --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a5a1463..7ac6b8f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/BurntSushi/toml v0.3.1 github.com/antihax/optional v1.0.0 - github.com/crusoecloud/client-go v0.1.22 + github.com/crusoecloud/client-go v0.1.29 github.com/hashicorp/terraform-plugin-framework v1.3.5 ) diff --git a/go.sum b/go.sum index 72aa9a4..6765079 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/crusoecloud/client-go v0.1.22 h1:SE077syXicRyq9/1B9V1+ocYJdGZZRr6pdkimKhUPQM= -github.com/crusoecloud/client-go v0.1.22/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= +github.com/crusoecloud/client-go v0.1.29 h1:RxjVBz2nw+qERmo7ZmbDd74hW1vkn+3jxzaaaAJ/GF8= +github.com/crusoecloud/client-go v0.1.29/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 2d4681c7cdfcb77fc7f2e57a33dfeb5b113d5487 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Mon, 20 Nov 2023 22:49:31 -0800 Subject: [PATCH 05/22] fix --- internal/disk/disk_resource.go | 3 +-- .../ib_partition/ib_partition_resource.go | 3 +-- internal/project/project_resource.go | 22 +------------------ internal/project/projects_data_source.go | 4 ++-- internal/project/util.go | 2 +- internal/vm/vm_resource.go | 12 +++++----- 6 files changed, 13 insertions(+), 33 deletions(-) diff --git a/internal/disk/disk_resource.go b/internal/disk/disk_resource.go index db0d005..26d883c 100644 --- a/internal/disk/disk_resource.go +++ b/internal/disk/disk_resource.go @@ -118,8 +118,7 @@ func (r *diskResource) Create(ctx context.Context, req resource.CreateRequest, r diskType = defaultDiskType } - dataResp, httpResp, err := r.client.DisksApi.CreateDisk(ctx, swagger.DisksPostRequest{ - RoleId: plan.ProjectID.ValueString(), + dataResp, httpResp, err := r.client.DisksApi.CreateDisk(ctx, swagger.DisksPostRequestV1Alpha5{ Name: plan.Name.ValueString(), Location: plan.Location.ValueString(), Type_: diskType, diff --git a/internal/ib_partition/ib_partition_resource.go b/internal/ib_partition/ib_partition_resource.go index dd95816..baab6f0 100644 --- a/internal/ib_partition/ib_partition_resource.go +++ b/internal/ib_partition/ib_partition_resource.go @@ -87,8 +87,7 @@ func (r *ibPartitionResource) Create(ctx context.Context, req resource.CreateReq return } - dataResp, httpResp, err := r.client.IBPartitionsApi.CreateIBPartition(ctx, swagger.IbPartitionsPostRequestV1Alpha4{ - RoleId: plan.ProjectID.ValueString(), + dataResp, httpResp, err := r.client.IBPartitionsApi.CreateIBPartition(ctx, swagger.IbPartitionsPostRequestV1Alpha5{ Name: plan.Name.ValueString(), IbNetworkId: plan.IBNetworkID.ValueString(), }, plan.ProjectID.ValueString()) diff --git a/internal/project/project_resource.go b/internal/project/project_resource.go index c3b1bc5..de5bb90 100644 --- a/internal/project/project_resource.go +++ b/internal/project/project_resource.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/antihax/optional" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -115,11 +114,7 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re return } - opts := &swagger.ProjectsApiGetProjectsOpts{ - OrgId: optional.EmptyString(), - } - - dataResp, httpResp, err := r.client.ProjectsApi.GetProjects(ctx, opts) + project, httpResp, err := r.client.ProjectsApi.GetProject(ctx, state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to get projects", fmt.Sprintf("Fetching Crusoe projects failed: %s\n\nIf the problem persists, contact support@crusoecloud.com", err.Error())) @@ -128,21 +123,6 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re } defer httpResp.Body.Close() - var project *swagger.Project - - for i := range dataResp.Items { - if dataResp.Items[i].Id == state.ID.ValueString() { - project = &dataResp.Items[i] - } - } - - if project == nil { - // disk has most likely been deleted out of band, so we update Terraform state to match - resp.State.RemoveResource(ctx) - - return - } - state.Name = types.StringValue(project.Name) state.ID = types.StringValue(project.Id) diff --git a/internal/project/projects_data_source.go b/internal/project/projects_data_source.go index e4cdc6a..1cf6577 100644 --- a/internal/project/projects_data_source.go +++ b/internal/project/projects_data_source.go @@ -74,11 +74,11 @@ func (ds *projectsDataSource) Schema(ctx context.Context, request datasource.Sch //nolint:gocritic // Implements Terraform defined interface func (ds *projectsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - opts := &swagger.ProjectsApiGetProjectsOpts{ + opts := &swagger.ProjectsApiListProjectsOpts{ OrgId: optional.EmptyString(), } - dataResp, httpResp, err := ds.client.ProjectsApi.GetProjects(ctx, opts) + dataResp, httpResp, err := ds.client.ProjectsApi.ListProjects(ctx, opts) if err != nil { resp.Diagnostics.AddError("Failed to Fetch Projects", "Could not fetch Project data at this time.") diff --git a/internal/project/util.go b/internal/project/util.go index 61b9c6c..7fe1ea4 100644 --- a/internal/project/util.go +++ b/internal/project/util.go @@ -15,7 +15,7 @@ func getUserOrg(ctx context.Context, apiClient *swagger.APIClient) (string, erro } defer httpResp.Body.Close() - entities := dataResp.Entities + entities := dataResp.Items switch len(entities) { case 0: return "", errors.New("user does not belong to any organizations") diff --git a/internal/vm/vm_resource.go b/internal/vm/vm_resource.go index c1b3d01..afc164f 100644 --- a/internal/vm/vm_resource.go +++ b/internal/vm/vm_resource.go @@ -220,9 +220,12 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res return } - diskIds := make([]string, 0, len(plan.Disks)) + diskIds := make([]swagger.DiskAttachment, 0, len(plan.Disks)) for _, d := range plan.Disks { - diskIds = append(diskIds, d.ID) + diskIds = append(diskIds,swagger.DiskAttachment{ + AttachmentType: d.AttachmentType, + DiskId: d.ID, + }) } // public static IPs @@ -243,8 +246,7 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res } } - dataResp, httpResp, err := r.client.VMsApi.CreateInstance(ctx, swagger.InstancesPostRequestV1Alpha4{ - RoleId: plan.ProjectID.ValueString(), + dataResp, httpResp, err := r.client.VMsApi.CreateInstance(ctx, swagger.InstancesPostRequestV1Alpha5{ Name: plan.Name.ValueString(), ProductName: plan.Type.ValueString(), Location: plan.Location.ValueString(), @@ -408,7 +410,7 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res var tNetworkInterfaces []vmNetworkInterfaceResourceModel diags = plan.NetworkInterfaces.ElementsAs(ctx, &tNetworkInterfaces, true) resp.Diagnostics.Append(diags...) - patchResp, httpResp, err := r.client.VMsApi.UpdateInstance(ctx, swagger.InstancesPatchRequestV1Alpha4{ + patchResp, httpResp, err := r.client.VMsApi.UpdateInstance(ctx, swagger.InstancesPatchRequest{ Action: "UPDATE", NetworkInterfaces: []swagger.NetworkInterface{{ Ips: []swagger.IpAddresses{{ From acb77634ea9f85ec8993f5cfcc7f53f9f4defd42 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 21 Nov 2023 14:11:48 -0800 Subject: [PATCH 06/22] terraform working --- examples/infiniband/main.tf | 16 +++++++++++----- examples/vms/main.tf | 22 ++++++++++++++++++---- internal/vm/vm_data_source.go | 17 +++++++++++++++++ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/examples/infiniband/main.tf b/examples/infiniband/main.tf index 325a3e3..429f4af 100644 --- a/examples/infiniband/main.tf +++ b/examples/infiniband/main.tf @@ -8,10 +8,13 @@ terraform { locals { - my_project_id = "d2ee27ac-52db-487a-ba7f-99c43bf159b2" my_ssh_key = file("~/.ssh/id_rsa.pub") } +resource "crusoe_project" "my_cool_project2" { + name = "my_cool_project3" +} + # list IB networks data "crusoe_ib_networks" "ib_networks" {} output "crusoe_ib" { @@ -26,7 +29,7 @@ resource "crusoe_ib_partition" "my_partition" { # above. alternatively, they can be obtain with the CLI by # crusoe networking ib-network list ib_network_id = "" - project_id = local.my_project_id + project_id = crusoe_project.my_cool_project2.id } # create two VMs, both in the same Infiniband partition @@ -39,11 +42,14 @@ resource "crusoe_compute_instance" "my_vm1" { image = "ubuntu20.04-nvidia-sxm-docker:latest" # IB image, see full list at https://docs.crusoecloud.com/compute/images/overview/index.html#list-of-curated-images ib_partition_id = crusoe_ib_partition.my_partition.id ssh_key = local.my_ssh_key - project_id = local.my_project_id + project_id = crusoe_project.my_cool_project2.id disks = [ // disk attached at startup - crusoe_storage_disk.data_disk + { + id = crusoe_storage_disk.data_disk.id + attachment_type = "disk-readwrite" + } ] } @@ -52,5 +58,5 @@ resource "crusoe_storage_disk" "data_disk" { name = "data-disk" size = "1TiB" location = "us-east1-a" - project_id = local.my_project_id + project_id = crusoe_project.my_cool_project2.id } diff --git a/examples/vms/main.tf b/examples/vms/main.tf index cc3921d..7a16729 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -8,18 +8,17 @@ terraform { locals { my_ssh_key = file("~/.ssh/id_rsa.pub") - my_project_id = "d2ee27ac-52db-487a-ba7f-99c43bf159b2" } resource "crusoe_project" "my_cool_project2" { - name = "my_cool_project2" + name = "my_cool_project3" } // new VM resource "crusoe_compute_instance" "my_vm" { name = "my-new-vm" - type = "a40.1x" - location = "us-northcentralstaging1-a" + type = "a100-80gb.1x" + location = "us-northcentral1-a" # optionally specify a different base image #image = "nvidia-docker" @@ -28,6 +27,21 @@ resource "crusoe_compute_instance" "my_vm" { startup_script = file("startup.sh") project_id = crusoe_project.my_cool_project2.id + disks = [ + // disk attached at startup + { + id = crusoe_storage_disk.data_disk.id + attachment_type = "disk-readwrite" + } + ] + +} + +resource "crusoe_storage_disk" "data_disk" { + name = "data-disk" + size = "200GiB" + project_id = crusoe_project.my_cool_project2.id + location = "us-northcentral1-a" } // firewall rule diff --git a/internal/vm/vm_data_source.go b/internal/vm/vm_data_source.go index d303758..cb0f4cc 100644 --- a/internal/vm/vm_data_source.go +++ b/internal/vm/vm_data_source.go @@ -87,6 +87,9 @@ func (ds *vmDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, re "id": schema.StringAttribute{ Required: true, }, + "attachment_type": schema.StringAttribute{ + Required: true, + }, }, }, }, @@ -156,6 +159,20 @@ func (ds *vmDataSource) Read(ctx context.Context, req datasource.ReadRequest, re state.ProjectID = &vm.ProjectId state.Name = &vm.Name state.Type = &vm.ProductName + attachedDisks := make([]vmDiskResourceModel, 0, len(vm.Disks)) + for _, disk := range vm.Disks{ + attachmentType := "" + for _,attachment:=range disk.AttachedTo { + if attachment.VmId ==vm.Id { + attachmentType = attachment.AttachmentType + break + } + } + attachedDisks = append(attachedDisks, vmDiskResourceModel{ + ID: disk.Id, + AttachmentType: attachmentType, + }) + } networkInterfaces, _ := vmNetworkInterfacesToTerraformDataModel(vm.NetworkInterfaces) state.NetworkInterfaces = networkInterfaces From 740d64876da4715cb024250f34f05fe6feedc7f7 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 21 Nov 2023 17:59:54 -0800 Subject: [PATCH 07/22] storage validator --- internal/project/project_resource.go | 19 +--- .../storage_attachment_type_validator.go | 43 ++++++++ internal/vm/util.go | 27 ++++- internal/vm/vm_resource.go | 104 ++++++++++++------ 4 files changed, 140 insertions(+), 53 deletions(-) create mode 100644 internal/validators/storage_attachment_type_validator.go diff --git a/internal/project/project_resource.go b/internal/project/project_resource.go index de5bb90..15abc3f 100644 --- a/internal/project/project_resource.go +++ b/internal/project/project_resource.go @@ -132,7 +132,7 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re //nolint:gocritic // Implements Terraform defined interface func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var state projectResource + var state projectResourceModel diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -164,19 +164,8 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest //nolint:gocritic // Implements Terraform defined interface func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var state projectResourceModel - diags := req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - httpResp, err := r.client.ProjectsApi.DeleteProject(ctx, state.ID.ValueString()) - if err != nil { - resp.Diagnostics.AddError("Failed to delete project", - fmt.Sprintf("There was an error starting a delete project operation: %s", common.UnpackAPIError(err))) - return - } - defer httpResp.Body.Close() + resp.Diagnostics.AddWarning("Delete not supported", + "Deleting projects is not currently supported. If you're seeing this message, please reach"+ + " out to support@crusoecloud.com and let us know.") } diff --git a/internal/validators/storage_attachment_type_validator.go b/internal/validators/storage_attachment_type_validator.go new file mode 100644 index 0000000..34c8e8c --- /dev/null +++ b/internal/validators/storage_attachment_type_validator.go @@ -0,0 +1,43 @@ +package internal + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +type DiskAttachmentType string + +const ( + DiskReadOnly DiskAttachmentType = "disk-readonly" + DiskReadWrite DiskAttachmentType = "disk-readwrite" +) + +// StorageAttachmentTypeValidator validates that a given data storage size is accepted by the storage API. +type StorageAttachmentTypeValidator struct{} + +func (v StorageAttachmentTypeValidator) Description(ctx context.Context) string { + return "Disk attachment type must be either 'disk-readonly' or 'disk-readwrite'" +} + +func (v StorageAttachmentTypeValidator) MarkdownDescription(ctx context.Context) string { + return "Disk attachment type must be either 'disk-readonly' or 'disk-readwrite'" +} + +//nolint:gocritic // Implements Terraform defined interface +func (v StorageAttachmentTypeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + input := req.ConfigValue.ValueString() + input = strings.ToLower(input) + + if !(input == string(DiskReadOnly)) && !(input == string(DiskReadWrite)){ + resp.Diagnostics.AddAttributeError(req.Path, "Unsupported Disk Attachment Type", + "Disk attachment type must be either 'disk-readonly' or 'disk-readwrite'") + } + + return +} diff --git a/internal/vm/util.go b/internal/vm/util.go index 15b8664..ed2711d 100644 --- a/internal/vm/util.go +++ b/internal/vm/util.go @@ -3,8 +3,8 @@ package vm import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" swagger "github.com/crusoecloud/client-go/swagger/v1alpha5" @@ -35,13 +35,20 @@ var vmNetworkInterfaceSchema = types.ObjectType{ }, } +var vmDiskAttachmentSchema = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "attachment_type": types.StringType, + }, +} + // getDisksDiff compares the disks attached to two VM resource models and returns // a diff of disks defined by disk ID. func getDisksDiff(origDisks, newDisks []vmDiskResourceModel) (disksAdded []swagger.DiskAttachment, disksRemoved []string) { for _, newDisk := range newDisks { matched := false for _, origDisk := range origDisks { - if newDisk.ID == origDisk.ID { + if newDisk.ID == origDisk.ID && newDisk.AttachmentType == origDisk.AttachmentType { matched = true break @@ -55,7 +62,7 @@ func getDisksDiff(origDisks, newDisks []vmDiskResourceModel) (disksAdded []swagg for _, origDisk := range origDisks { matched := false for _, newDisk := range newDisks { - if newDisk.ID == origDisk.ID { + if newDisk.ID == origDisk.ID && newDisk.AttachmentType == origDisk.AttachmentType { matched = true break @@ -65,7 +72,6 @@ func getDisksDiff(origDisks, newDisks []vmDiskResourceModel) (disksAdded []swagg disksRemoved = append(disksRemoved, origDisk.ID) } } - return disksAdded, disksRemoved } @@ -147,3 +153,16 @@ func vmNetworkInterfacesToTerraformResourceModel(networkInterfaces []swagger.Net return values, warning } + +func vmDiskAttachmentToTerraformResourceModel(diskAttachments []swagger.DiskAttachment) (diskAttachmentsList types.List, diags diag.Diagnostics) { + attachments := make([]vmDiskResourceModel, 0, len(diskAttachments)) + for _, diskAttachment := range diskAttachments { + attachments = append(attachments, vmDiskResourceModel{ + ID: diskAttachment.DiskId, + AttachmentType: diskAttachment.AttachmentType, + }) + } + + diskAttachmentsList, diags = types.ListValueFrom(context.Background(), vmDiskAttachmentSchema, attachments) + return diskAttachmentsList, diags +} diff --git a/internal/vm/vm_resource.go b/internal/vm/vm_resource.go index afc164f..e001629 100644 --- a/internal/vm/vm_resource.go +++ b/internal/vm/vm_resource.go @@ -24,18 +24,18 @@ type vmResource struct { } type vmResourceModel struct { - ID types.String `tfsdk:"id"` - ProjectID types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - Type types.String `tfsdk:"type"` - SSHKey types.String `tfsdk:"ssh_key"` - Location types.String `tfsdk:"location"` - Image types.String `tfsdk:"image"` - StartupScript types.String `tfsdk:"startup_script"` - ShutdownScript types.String `tfsdk:"shutdown_script"` - IBPartitionID types.String `tfsdk:"ib_partition_id"` - Disks []vmDiskResourceModel `tfsdk:"disks"` - NetworkInterfaces types.List `tfsdk:"network_interfaces"` + ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` + SSHKey types.String `tfsdk:"ssh_key"` + Location types.String `tfsdk:"location"` + Image types.String `tfsdk:"image"` + StartupScript types.String `tfsdk:"startup_script"` + ShutdownScript types.String `tfsdk:"shutdown_script"` + IBPartitionID types.String `tfsdk:"ib_partition_id"` + Disks types.List `tfsdk:"disks"` + NetworkInterfaces types.List `tfsdk:"network_interfaces"` } type vmNetworkInterfaceResourceModel struct { @@ -136,6 +136,7 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res }, "attachment_type": schema.StringAttribute{ Optional: true, + Validators: []validator.String{validators.StorageAttachmentTypeValidator{}}, }, }, }, @@ -220,9 +221,16 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res return } - diskIds := make([]swagger.DiskAttachment, 0, len(plan.Disks)) - for _, d := range plan.Disks { - diskIds = append(diskIds,swagger.DiskAttachment{ + tDisks := make([]vmDiskResourceModel, 0, len(plan.Disks.Elements())) + diags = plan.Disks.ElementsAs(ctx, &tDisks, true) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diskIds := make([]swagger.DiskAttachment, 0, len(tDisks)) + for _, d := range tDisks { + diskIds = append(diskIds, swagger.DiskAttachment{ AttachmentType: d.AttachmentType, DiskId: d.ID, }) @@ -279,6 +287,12 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res networkInterfaces, _ := vmNetworkInterfacesToTerraformResourceModel(instance.NetworkInterfaces) plan.NetworkInterfaces = networkInterfaces + disks, diags := vmDiskAttachmentToTerraformResourceModel(diskIds) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + plan.Disks = disks diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) @@ -311,12 +325,23 @@ func (r *vmResource) Read(ctx context.Context, req resource.ReadRequest, resp *r disks := make([]vmDiskResourceModel, 0, len(instance.Disks)) for _, disk := range instance.Disks { if !disk.IsBootDisk { - disks = append(disks, vmDiskResourceModel{ID: disk.Id}) + attachmentType := "" + for _, attachment := range disk.AttachedTo { + if attachment.VmId == instance.Id { + attachmentType = attachment.AttachmentType + break + } + } + disks = append(disks, vmDiskResourceModel{ + ID: disk.Id, + AttachmentType: attachmentType, + }) } } if len(disks) > 0 { // only assign if disks is not empty. otherwise, intentionally keep this nil, for future comparisons - state.Disks = disks + tDisks, _ := types.ListValueFrom(context.Background(), vmDiskAttachmentSchema, disks) + state.Disks = tDisks } diags = resp.State.Set(ctx, &state) @@ -343,27 +368,16 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res } // attach/detach disks if requested - addedDisks, removedDisks := getDisksDiff(state.Disks, plan.Disks) - if len(addedDisks) > 0 { - attachResp, httpResp, err := r.client.VMsApi.UpdateInstanceAttachDisks(ctx, swagger.InstancesAttachDiskPostRequestV1Alpha5{ - AttachDisks: addedDisks, - }, state.ProjectID.ValueString(), state.ID.ValueString()) - if err != nil { - resp.Diagnostics.AddError("Failed to attach disk", - fmt.Sprintf("There was an error starting an attach disk operation: %s", common.UnpackAPIError(err))) + tPlanDisks := make([]vmDiskResourceModel, 0, len(plan.Disks.Elements())) + diags = plan.Disks.ElementsAs(ctx, &tPlanDisks, true) - return - } - defer httpResp.Body.Close() + tStateDisks := make([]vmDiskResourceModel, 0, len(state.Disks.Elements())) + diags = state.Disks.ElementsAs(ctx, &tStateDisks, true) - _, err = common.AwaitOperation(ctx, attachResp.Operation, plan.ProjectID.ValueString(), r.client.VMOperationsApi.GetComputeVMsInstancesOperation) - if err != nil { - resp.Diagnostics.AddError("Failed to attach disk", - fmt.Sprintf("There was an error attaching a disk: %s", common.UnpackAPIError(err))) - } - } + addedDisks, removedDisks := getDisksDiff(tStateDisks, tPlanDisks) if len(removedDisks) > 0 { + detachResp, httpResp, err := r.client.VMsApi.UpdateInstanceDetachDisks(ctx, swagger.InstancesDetachDiskPostRequest{ DetachDisks: removedDisks, }, state.ProjectID.ValueString(), state.ID.ValueString()) @@ -382,11 +396,33 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res } } + if len(addedDisks) > 0 { + attachResp, httpResp, err := r.client.VMsApi.UpdateInstanceAttachDisks(ctx, swagger.InstancesAttachDiskPostRequestV1Alpha5{ + AttachDisks: addedDisks, + }, state.ProjectID.ValueString(), state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Failed to attach disk", + fmt.Sprintf("There was an error starting an attach disk operation: %s", common.UnpackAPIError(err))) + + return + } + defer httpResp.Body.Close() + + _, err = common.AwaitOperation(ctx, attachResp.Operation, plan.ProjectID.ValueString(), r.client.VMOperationsApi.GetComputeVMsInstancesOperation) + if err != nil { + resp.Diagnostics.AddError("Failed to attach disk", + fmt.Sprintf("There was an error attaching a disk: %s", common.UnpackAPIError(err))) + } + } + // save intermediate results if len(addedDisks) > 0 || len(removedDisks) > 0 { state.Disks = plan.Disks diags = resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } } // handle toggling static/dynamic public IPs From aaa483755942d133b3649b96f5119aa261f73017 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 21 Nov 2023 18:00:53 -0800 Subject: [PATCH 08/22] update example --- examples/vms/main.tf | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/vms/main.tf b/examples/vms/main.tf index 7a16729..eb0dfe0 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -10,8 +10,8 @@ locals { my_ssh_key = file("~/.ssh/id_rsa.pub") } -resource "crusoe_project" "my_cool_project2" { - name = "my_cool_project3" +resource "crusoe_project" "my_project" { + name = "my_new_project" } // new VM @@ -25,13 +25,13 @@ resource "crusoe_compute_instance" "my_vm" { ssh_key = local.my_ssh_key startup_script = file("startup.sh") - project_id = crusoe_project.my_cool_project2.id + project_id = crusoe_project.my_project.id disks = [ // disk attached at startup { id = crusoe_storage_disk.data_disk.id - attachment_type = "disk-readwrite" + attachment_type = "disk-readonly" } ] @@ -40,8 +40,8 @@ resource "crusoe_compute_instance" "my_vm" { resource "crusoe_storage_disk" "data_disk" { name = "data-disk" size = "200GiB" - project_id = crusoe_project.my_cool_project2.id - location = "us-northcentral1-a" + project_id = crusoe_project.my_project.id + location = "us-northcentraldevelopment1-a" } // firewall rule @@ -56,5 +56,5 @@ resource "crusoe_vpc_firewall_rule" "open_fw_rule" { source_ports = "1-65535" destination = crusoe_compute_instance.my_vm.network_interfaces[0].public_ipv4.address destination_ports = "1-65535" - project_id = crusoe_project.my_cool_project2.id + project_id = crusoe_project.my_project.id } From 6d514f4293f5d93a06a3f446b6082c2ce07bd6d4 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 21 Nov 2023 18:01:50 -0800 Subject: [PATCH 09/22] change util --- internal/validators/storage_attachment_type_validator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/validators/storage_attachment_type_validator.go b/internal/validators/storage_attachment_type_validator.go index 34c8e8c..716e611 100644 --- a/internal/validators/storage_attachment_type_validator.go +++ b/internal/validators/storage_attachment_type_validator.go @@ -34,7 +34,7 @@ func (v StorageAttachmentTypeValidator) ValidateString(ctx context.Context, req input := req.ConfigValue.ValueString() input = strings.ToLower(input) - if !(input == string(DiskReadOnly)) && !(input == string(DiskReadWrite)){ + if input != string(DiskReadOnly) && input != string(DiskReadWrite){ resp.Diagnostics.AddAttributeError(req.Path, "Unsupported Disk Attachment Type", "Disk attachment type must be either 'disk-readonly' or 'disk-readwrite'") } From f299752bd1e3ecd0de3bb3d77f257126014f3d8b Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 21 Nov 2023 21:32:35 -0800 Subject: [PATCH 10/22] mr feedback --- examples/infiniband/main.tf | 12 ++-- examples/project-variable/main.tf | 60 +++++++++++++++++++ examples/vms/main.tf | 8 +-- internal/disk/disk_resource.go | 3 +- .../ib_partition/ib_partition_resource.go | 3 +- internal/project/project_resource.go | 2 +- internal/project/projects_data_source.go | 6 +- 7 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 examples/project-variable/main.tf diff --git a/examples/infiniband/main.tf b/examples/infiniband/main.tf index 429f4af..8d76cde 100644 --- a/examples/infiniband/main.tf +++ b/examples/infiniband/main.tf @@ -8,11 +8,11 @@ terraform { locals { - my_ssh_key = file("~/.ssh/id_rsa.pub") + my_ssh_key = file("~/.ssh/id_ed25519.pub") } -resource "crusoe_project" "my_cool_project2" { - name = "my_cool_project3" +resource "crusoe_project" "my_project" { + name = "my-new-project" } # list IB networks @@ -29,7 +29,7 @@ resource "crusoe_ib_partition" "my_partition" { # above. alternatively, they can be obtain with the CLI by # crusoe networking ib-network list ib_network_id = "" - project_id = crusoe_project.my_cool_project2.id + project_id = crusoe_project.my_project.id } # create two VMs, both in the same Infiniband partition @@ -42,7 +42,7 @@ resource "crusoe_compute_instance" "my_vm1" { image = "ubuntu20.04-nvidia-sxm-docker:latest" # IB image, see full list at https://docs.crusoecloud.com/compute/images/overview/index.html#list-of-curated-images ib_partition_id = crusoe_ib_partition.my_partition.id ssh_key = local.my_ssh_key - project_id = crusoe_project.my_cool_project2.id + project_id = crusoe_project.my_project.id disks = [ // disk attached at startup @@ -58,5 +58,5 @@ resource "crusoe_storage_disk" "data_disk" { name = "data-disk" size = "1TiB" location = "us-east1-a" - project_id = crusoe_project.my_cool_project2.id + project_id = crusoe_project.my_project.id } diff --git a/examples/project-variable/main.tf b/examples/project-variable/main.tf new file mode 100644 index 0000000..e963708 --- /dev/null +++ b/examples/project-variable/main.tf @@ -0,0 +1,60 @@ +terraform { + required_providers { + crusoe = { + source = "registry.terraform.io/crusoecloud/crusoe" + } + } +} + +locals { + my_ssh_key = file("~/.ssh/id_ed25519.pub") +} + +variable "my_project_id" { + type = string + default = "" +} + +// new VM +resource "crusoe_compute_instance" "my_vm" { + name = "my-new-vm" + type = "a100-80gb.1x" + location = "us-northcentral1-a" + + # optionally specify a different base image + #image = "nvidia-docker" + + ssh_key = local.my_ssh_key + project_id = var.my_project_id + + disks = [ + // disk attached at startup + { + id = crusoe_storage_disk.data_disk.id + attachment_type = "disk-readonly" + } + ] + +} + +resource "crusoe_storage_disk" "data_disk" { + name = "data-disk" + size = "200GiB" + project_id = var.my_project_id + location = "us-northcentral1-a" +} + +// firewall rule +// note: this allows all ingress over TCP to our VM +resource "crusoe_vpc_firewall_rule" "open_fw_rule" { + network = crusoe_compute_instance.my_vm.network_interfaces[0].network + name = "example-terraform-rule" + action = "allow" + direction = "ingress" + protocols = "tcp" + source = "0.0.0.0/0" + source_ports = "1-65535" + destination = crusoe_compute_instance.my_vm.network_interfaces[0].public_ipv4.address + destination_ports = "1-65535" + project_id = var.my_project_id +} diff --git a/examples/vms/main.tf b/examples/vms/main.tf index eb0dfe0..a45f8fe 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -7,11 +7,11 @@ terraform { } locals { - my_ssh_key = file("~/.ssh/id_rsa.pub") + my_ssh_key = file("~/.ssh/id_ed25519.pub") } resource "crusoe_project" "my_project" { - name = "my_new_project" + name = "my-new-project" } // new VM @@ -23,7 +23,7 @@ resource "crusoe_compute_instance" "my_vm" { # optionally specify a different base image #image = "nvidia-docker" - ssh_key = local.my_ssh_key + ssh_key = local.my_ssh_key startup_script = file("startup.sh") project_id = crusoe_project.my_project.id @@ -41,7 +41,7 @@ resource "crusoe_storage_disk" "data_disk" { name = "data-disk" size = "200GiB" project_id = crusoe_project.my_project.id - location = "us-northcentraldevelopment1-a" + location = "us-northcentral1-a" } // firewall rule diff --git a/internal/disk/disk_resource.go b/internal/disk/disk_resource.go index 26d883c..d711be8 100644 --- a/internal/disk/disk_resource.go +++ b/internal/disk/disk_resource.go @@ -70,7 +70,8 @@ func (r *diskResource) Schema(ctx context.Context, req resource.SchemaRequest, r }, "project_id": schema.StringAttribute{ Required: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace()}, }, "location": schema.StringAttribute{ Required: true, diff --git a/internal/ib_partition/ib_partition_resource.go b/internal/ib_partition/ib_partition_resource.go index baab6f0..93bcbc6 100644 --- a/internal/ib_partition/ib_partition_resource.go +++ b/internal/ib_partition/ib_partition_resource.go @@ -69,7 +69,8 @@ func (r *ibPartitionResource) Schema(ctx context.Context, req resource.SchemaReq }, "project_id": schema.StringAttribute{ Required: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace()}, // cannot be updated in place }, }, } diff --git a/internal/project/project_resource.go b/internal/project/project_resource.go index 15abc3f..488d4a4 100644 --- a/internal/project/project_resource.go +++ b/internal/project/project_resource.go @@ -80,7 +80,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest orgID, err := getUserOrg(ctx, r.client) if err != nil { resp.Diagnostics.AddError("Failed to create project", - fmt.Sprintf("There was an error starting a create project operation: %s", err)) + fmt.Sprintf("There was an error fetching the user's organization: %s", err)) return } diff --git a/internal/project/projects_data_source.go b/internal/project/projects_data_source.go index 1cf6577..f30c684 100644 --- a/internal/project/projects_data_source.go +++ b/internal/project/projects_data_source.go @@ -22,10 +22,6 @@ type projectsDataSourceModel struct { type projectModel struct { ID string `tfsdk:"id"` Name string `tfsdk:"name"` - Location string `tfsdk:"location"` - Type string `tfsdk:"type"` - Size string `tfsdk:"size"` - SerialNumber string `tfsdk:"serial_number"` } func NewProjectsDataSource() datasource.DataSource { @@ -64,7 +60,7 @@ func (ds *projectsDataSource) Schema(ctx context.Context, request datasource.Sch Computed: true, }, "name": schema.StringAttribute{ - Required: true, + Computed: true, }, }, }, From f4f806778388b7a1633474d546a2b893719e6ab8 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Wed, 22 Nov 2023 17:00:53 -0800 Subject: [PATCH 11/22] fallback project --- crusoe/provider.go | 10 ++++ examples/project-variable/main.tf | 18 ++----- internal/common/config.go | 2 + internal/common/util.go | 52 ++++++++++++++++++- internal/disk/disk_resource.go | 24 +++++++-- .../firewall_rule/firewall_rule_resource.go | 22 ++++++-- .../ib_partition/ib_partition_resource.go | 20 ++++++- internal/vm/vm_resource.go | 24 +++++++-- 8 files changed, 145 insertions(+), 27 deletions(-) diff --git a/crusoe/provider.go b/crusoe/provider.go index 08f95fc..ee0260c 100644 --- a/crusoe/provider.go +++ b/crusoe/provider.go @@ -105,6 +105,16 @@ func (p *crusoeProvider) Configure(ctx context.Context, req provider.ConfigureRe ) } + if clientConfig.DefaultProject == "" { + resp.Diagnostics.AddAttributeWarning( + path.Root("default_project"), + "Missing Crusoe Default Project", + "The provider did not find a default project specified in the configuration file and will attempt to infer the project to use. "+ + "Set the value in ~/.crusoe/config. "+ + "If either is already set, ensure the value is not empty.", + ) + } + if resp.Diagnostics.HasError() { return } diff --git a/examples/project-variable/main.tf b/examples/project-variable/main.tf index e963708..718e456 100644 --- a/examples/project-variable/main.tf +++ b/examples/project-variable/main.tf @@ -7,25 +7,19 @@ terraform { } locals { - my_ssh_key = file("~/.ssh/id_ed25519.pub") -} - -variable "my_project_id" { - type = string - default = "" + my_ssh_key = file("~/.ssh/id_rsa.pub") } // new VM resource "crusoe_compute_instance" "my_vm" { - name = "my-new-vm" + name = "my-new-vm4" type = "a100-80gb.1x" - location = "us-northcentral1-a" + location = "us-northcentraldevelopment1-a" # optionally specify a different base image #image = "nvidia-docker" ssh_key = local.my_ssh_key - project_id = var.my_project_id disks = [ // disk attached at startup @@ -38,10 +32,9 @@ resource "crusoe_compute_instance" "my_vm" { } resource "crusoe_storage_disk" "data_disk" { - name = "data-disk" + name = "data-disk5" size = "200GiB" - project_id = var.my_project_id - location = "us-northcentral1-a" + location = "us-northcentraldevelopment1-a" } // firewall rule @@ -56,5 +49,4 @@ resource "crusoe_vpc_firewall_rule" "open_fw_rule" { source_ports = "1-65535" destination = crusoe_compute_instance.my_vm.network_interfaces[0].public_ipv4.address destination_ports = "1-65535" - project_id = var.my_project_id } diff --git a/internal/common/config.go b/internal/common/config.go index 4f9445a..a4820b3 100644 --- a/internal/common/config.go +++ b/internal/common/config.go @@ -19,6 +19,7 @@ type Config struct { SecretKey string `toml:"secret_key"` SSHPublicKeyFile string `toml:"ssh_public_key_file"` ApiEndpoint string `toml:"api_endpoint"` + DefaultProject string `toml:"default_project"` } // ConfigFile reflects the structure of a valid Crusoe config, which should have a default profile at the root level. @@ -48,6 +49,7 @@ func GetConfig() (*Config, error) { config.AccessKeyID = configFile.Default.AccessKeyID config.SecretKey = configFile.Default.SecretKey config.SSHPublicKeyFile = configFile.Default.SSHPublicKeyFile + config.DefaultProject = configFile.Default.DefaultProject if configFile.Default.ApiEndpoint != "" { config.ApiEndpoint = configFile.Default.ApiEndpoint diff --git a/internal/common/util.go b/internal/common/util.go index 2a1a584..5e6848d 100644 --- a/internal/common/util.go +++ b/internal/common/util.go @@ -5,6 +5,8 @@ import ( "encoding/json" "errors" "fmt" + "github.com/antihax/optional" + "github.com/hashicorp/terraform-plugin-framework/diag" "net/http" "strings" "time" @@ -37,7 +39,8 @@ var ( errUnableToGetOpRes = errors.New("failed to get result of operation") // fallback error presented to the user in unexpected situations - errUnexpected = errors.New("An unexpected error occurred. Please try again, and if the problem persists, contact support@crusoecloud.com.") + errMultipleProjects = errors.New("User has multiple projects. Please specify a project to be used.") + errUnexpected = errors.New("An unexpected error occurred. Please try again, and if the problem persists, contact support@crusoecloud.com.") ) // NewAPIClient initializes a new Crusoe API client with the given configuration. @@ -105,6 +108,53 @@ func AwaitOperationAndResolve[T any](ctx context.Context, op *swagger.Operation, return result, op, nil } +// GetFallbackProject queries the API to get the list of projects belonging to the +// logged in user. If there is one project belonging to the user, it returns that project +// else it adds an error to the diagnostics and returns. +func GetFallbackProject(ctx context.Context, client *swagger.APIClient, diag *diag.Diagnostics) (string, error) { + + config, err := GetConfig() + if err != nil { + return "", fmt.Errorf("failed to get config: %v", err) + } + + var opts = &swagger.ProjectsApiListProjectsOpts{ + OrgId: optional.EmptyString(), + } + + if config.DefaultProject != "" { + opts.ProjectName = optional.NewString(config.DefaultProject) + } + + dataResp, httpResp, err := client.ProjectsApi.ListProjects(ctx, opts) + defer httpResp.Body.Close() + + if err != nil { + diag.AddError("Failed to retrieve project ID", + "Failed to retrieve project ID for the authenticated user.") + + return "", err + } + + if len(dataResp.Items) != 1 { + diag.AddError("Multiple projects found.", + "Multiple projects found for the authenticated user. Unable to determine which project to use.") + + return "", errMultipleProjects + } + + projectID := dataResp.Items[0].Id + + if config.DefaultProject == "" { + diag.AddWarning("Default project not specified", + fmt.Sprintf("A project_id was not specified in the configuration file. "+ + "Please specify a project in the terraform file or set a 'default_project' in your configuration file. "+ + "Falling back to project: %s.", dataResp.Items[0].Name)) + } + + return projectID, nil +} + func parseOpResult[T any](opResult interface{}) (*T, error) { b, err := json.Marshal(opResult) if err != nil { diff --git a/internal/disk/disk_resource.go b/internal/disk/disk_resource.go index d711be8..4a740f2 100644 --- a/internal/disk/disk_resource.go +++ b/internal/disk/disk_resource.go @@ -69,7 +69,8 @@ func (r *diskResource) Schema(ctx context.Context, req resource.SchemaRequest, r PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates }, "project_id": schema.StringAttribute{ - Required: true, + Optional: true, + Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace()}, }, @@ -119,21 +120,35 @@ func (r *diskResource) Create(ctx context.Context, req resource.CreateRequest, r diskType = defaultDiskType } + projectID := "" + if plan.ProjectID.ValueString() == "" { + project, err := common.GetFallbackProject(ctx, r.client, &resp.Diagnostics) + if err != nil { + + resp.Diagnostics.AddError("Failed to create disk", + fmt.Sprintf("No project was specified and it was not possible to determine which project to use: %v", err)) + return + } + projectID = project + } else { + projectID = plan.ProjectID.ValueString() + } + dataResp, httpResp, err := r.client.DisksApi.CreateDisk(ctx, swagger.DisksPostRequestV1Alpha5{ Name: plan.Name.ValueString(), Location: plan.Location.ValueString(), Type_: diskType, Size: plan.Size.ValueString(), - }, plan.ProjectID.ValueString()) + }, projectID) if err != nil { resp.Diagnostics.AddError("Failed to create disk", - fmt.Sprintf("There was an error starting a create disk operation: %s", common.UnpackAPIError(err))) + fmt.Sprintf("There was an error starting a create disk operation (%s): %s", projectID, common.UnpackAPIError(err))) return } defer httpResp.Body.Close() - disk, _, err := common.AwaitOperationAndResolve[swagger.Disk](ctx, dataResp.Operation, plan.ProjectID.ValueString(), r.client.DiskOperationsApi.GetStorageDisksOperation) + disk, _, err := common.AwaitOperationAndResolve[swagger.Disk](ctx, dataResp.Operation, projectID, r.client.DiskOperationsApi.GetStorageDisksOperation) if err != nil { resp.Diagnostics.AddError("Failed to create disk", fmt.Sprintf("There was an error creating a disk: %s", common.UnpackAPIError(err))) @@ -145,6 +160,7 @@ func (r *diskResource) Create(ctx context.Context, req resource.CreateRequest, r plan.Type = types.StringValue(disk.Type_) plan.Location = types.StringValue(disk.Location) plan.SerialNumber = types.StringValue(disk.SerialNumber) + plan.ProjectID = types.StringValue(projectID) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) diff --git a/internal/firewall_rule/firewall_rule_resource.go b/internal/firewall_rule/firewall_rule_resource.go index b090e02..bf868ee 100644 --- a/internal/firewall_rule/firewall_rule_resource.go +++ b/internal/firewall_rule/firewall_rule_resource.go @@ -69,7 +69,8 @@ func (r *firewallRuleResource) Schema(ctx context.Context, req resource.SchemaRe PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates }, "project_id": schema.StringAttribute{ - Required: true, + Optional: true, + Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "network": schema.StringAttribute{ @@ -125,6 +126,20 @@ func (r *firewallRuleResource) Create(ctx context.Context, req resource.CreateRe sourcePortsStr := strings.ReplaceAll(plan.SourcePorts.ValueString(), "*", "1-65535") destPortsStr := strings.ReplaceAll(plan.DestinationPorts.ValueString(), "*", "1-65535") + projectID := "" + if plan.ProjectID.ValueString() == ""{ + project, err := common.GetFallbackProject(ctx, r.client, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError("Failed to create firewall rule", + fmt.Sprintf("No project was specified and it was not possible to determine which project to use: %v", err)) + + return + } + projectID = project + } else { + projectID = plan.ProjectID.ValueString() + } + dataResp, httpResp, err := r.client.VPCFirewallRulesApi.CreateVPCFirewallRule(ctx, swagger.VpcFirewallRulesPostRequestV1Alpha5{ VpcNetworkId: plan.Network.ValueString(), Name: plan.Name.ValueString(), @@ -135,7 +150,7 @@ func (r *firewallRuleResource) Create(ctx context.Context, req resource.CreateRe SourcePorts: stringToSlice(sourcePortsStr, ","), Destinations: []swagger.FirewallRuleObject{toFirewallRuleObject(plan.Destination.ValueString())}, DestinationPorts: stringToSlice(destPortsStr, ","), - }, plan.ProjectID.ValueString()) + }, projectID) if err != nil { resp.Diagnostics.AddError("Failed to create firewall rule", fmt.Sprintf("There was an error starting a create firewall rule operation: %s", common.UnpackAPIError(err))) @@ -145,7 +160,7 @@ func (r *firewallRuleResource) Create(ctx context.Context, req resource.CreateRe defer httpResp.Body.Close() firewallRule, _, err := common.AwaitOperationAndResolve[swagger.VpcFirewallRule]( - ctx, dataResp.Operation, plan.ProjectID.ValueString(), + ctx, dataResp.Operation, projectID, r.client.VPCFirewallRuleOperationsApi.GetNetworkingVPCFirewallRulesOperation) if err != nil { resp.Diagnostics.AddError("Failed to create firewall rule", @@ -155,6 +170,7 @@ func (r *firewallRuleResource) Create(ctx context.Context, req resource.CreateRe } plan.ID = types.StringValue(firewallRule.Id) + plan.ProjectID = types.StringValue(projectID) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) diff --git a/internal/ib_partition/ib_partition_resource.go b/internal/ib_partition/ib_partition_resource.go index 93bcbc6..600dc08 100644 --- a/internal/ib_partition/ib_partition_resource.go +++ b/internal/ib_partition/ib_partition_resource.go @@ -68,7 +68,8 @@ func (r *ibPartitionResource) Schema(ctx context.Context, req resource.SchemaReq PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place }, "project_id": schema.StringAttribute{ - Required: true, + Optional: true, + Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace()}, // cannot be updated in place }, @@ -88,10 +89,24 @@ func (r *ibPartitionResource) Create(ctx context.Context, req resource.CreateReq return } + projectID := "" + if plan.ProjectID.ValueString() == "" { + project, err := common.GetFallbackProject(ctx, r.client, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError("Failed to create partition", + fmt.Sprintf("No project was specified and it was not possible to determine which project to use: %v", err)) + + return + } + projectID = project + } else { + projectID = plan.ProjectID.ValueString() + } + dataResp, httpResp, err := r.client.IBPartitionsApi.CreateIBPartition(ctx, swagger.IbPartitionsPostRequestV1Alpha5{ Name: plan.Name.ValueString(), IbNetworkId: plan.IBNetworkID.ValueString(), - }, plan.ProjectID.ValueString()) + }, projectID) if err != nil { resp.Diagnostics.AddError("Failed to create partition", fmt.Sprintf("There was an error creating an Infiniband partition: %s", common.UnpackAPIError(err))) @@ -101,6 +116,7 @@ func (r *ibPartitionResource) Create(ctx context.Context, req resource.CreateReq defer httpResp.Body.Close() plan.ID = types.StringValue(dataResp.Id) + plan.ProjectID = types.StringValue(projectID) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) diff --git a/internal/vm/vm_resource.go b/internal/vm/vm_resource.go index e001629..bc1b51e 100644 --- a/internal/vm/vm_resource.go +++ b/internal/vm/vm_resource.go @@ -94,7 +94,8 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place }, "project_id": schema.StringAttribute{ - Required: true, + Optional: true, + Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place }, "type": schema.StringAttribute{ @@ -135,7 +136,7 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res Optional: true, }, "attachment_type": schema.StringAttribute{ - Optional: true, + Optional: true, Validators: []validator.String{validators.StorageAttachmentTypeValidator{}}, }, }, @@ -228,6 +229,20 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res return } + projectID := "" + if plan.ProjectID.ValueString() == "" { + project, err := common.GetFallbackProject(ctx, r.client, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError("Failed to create instance", + fmt.Sprintf("No project was specified and it was not possible to determine which project to use: %v", err)) + + return + } + projectID = project + } else { + projectID = plan.ProjectID.ValueString() + } + diskIds := make([]swagger.DiskAttachment, 0, len(tDisks)) for _, d := range tDisks { diskIds = append(diskIds, swagger.DiskAttachment{ @@ -265,7 +280,7 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res IbPartitionId: plan.IBPartitionID.ValueString(), NetworkInterfaces: newNetworkInterfaces, Disks: diskIds, - }, plan.ProjectID.ValueString()) + }, projectID) if err != nil { resp.Diagnostics.AddError("Failed to create instance", fmt.Sprintf("There was an error starting a create instance operation: %s", common.UnpackAPIError(err))) @@ -275,7 +290,7 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res defer httpResp.Body.Close() instance, _, err := common.AwaitOperationAndResolve[swagger.InstanceV1Alpha5]( - ctx, dataResp.Operation, plan.ProjectID.ValueString(), r.client.VMOperationsApi.GetComputeVMsInstancesOperation) + ctx, dataResp.Operation, projectID, r.client.VMOperationsApi.GetComputeVMsInstancesOperation) if err != nil { resp.Diagnostics.AddError("Failed to create instance", fmt.Sprintf("There was an error creating a instance: %s", common.UnpackAPIError(err))) @@ -293,6 +308,7 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res return } plan.Disks = disks + plan.ProjectID = types.StringValue(projectID) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) From 4145d73d06b687da5856b25fc833bb47a5741735 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 28 Nov 2023 11:12:52 -0800 Subject: [PATCH 12/22] use fall back project --- examples/infiniband/main.tf | 4 ++-- examples/vms/main.tf | 23 ++++-------------- internal/disk/disks_data_source.go | 14 ++++++----- internal/ib_network/ib_network_data_source.go | 24 +++++++++---------- 4 files changed, 26 insertions(+), 39 deletions(-) diff --git a/examples/infiniband/main.tf b/examples/infiniband/main.tf index 8d76cde..a8195d2 100644 --- a/examples/infiniband/main.tf +++ b/examples/infiniband/main.tf @@ -8,7 +8,7 @@ terraform { locals { - my_ssh_key = file("~/.ssh/id_ed25519.pub") + my_ssh_key = file("~/.ssh/id_rsa.pub") } resource "crusoe_project" "my_project" { @@ -48,7 +48,7 @@ resource "crusoe_compute_instance" "my_vm1" { // disk attached at startup { id = crusoe_storage_disk.data_disk.id - attachment_type = "disk-readwrite" + attachment_type = "disk-readonly" } ] } diff --git a/examples/vms/main.tf b/examples/vms/main.tf index a45f8fe..307da5d 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -7,7 +7,7 @@ terraform { } locals { - my_ssh_key = file("~/.ssh/id_ed25519.pub") + my_ssh_key = file("~/.ssh/id_rsa.pub") } resource "crusoe_project" "my_project" { @@ -16,9 +16,10 @@ resource "crusoe_project" "my_project" { // new VM resource "crusoe_compute_instance" "my_vm" { - name = "my-new-vm" + count = 2 + name = "my-new-vm-${count.index}" type = "a100-80gb.1x" - location = "us-northcentral1-a" + location = "us-northcentraldevelopment1-a" # optionally specify a different base image #image = "nvidia-docker" @@ -41,20 +42,6 @@ resource "crusoe_storage_disk" "data_disk" { name = "data-disk" size = "200GiB" project_id = crusoe_project.my_project.id - location = "us-northcentral1-a" + location = "us-northcentraldevelopment1-a" } -// firewall rule -// note: this allows all ingress over TCP to our VM -resource "crusoe_vpc_firewall_rule" "open_fw_rule" { - network = crusoe_compute_instance.my_vm.network_interfaces[0].network - name = "example-terraform-rule" - action = "allow" - direction = "ingress" - protocols = "tcp" - source = "0.0.0.0/0" - source_ports = "1-65535" - destination = crusoe_compute_instance.my_vm.network_interfaces[0].public_ipv4.address - destination_ports = "1-65535" - project_id = crusoe_project.my_project.id -} diff --git a/internal/disk/disks_data_source.go b/internal/disk/disks_data_source.go index 5972c43..fe15ffc 100644 --- a/internal/disk/disks_data_source.go +++ b/internal/disk/disks_data_source.go @@ -2,6 +2,7 @@ package disk import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -90,14 +91,15 @@ func (ds *disksDataSource) Schema(ctx context.Context, request datasource.Schema //nolint:gocritic // Implements Terraform defined interface func (ds *disksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var config disksDataSourceFilter - diags := req.Config.Get(ctx, &config) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { + projectID, err := common.GetFallbackProject(ctx, ds.client, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError("Failed to fetch disks", + fmt.Sprintf("No project was specified and it was not possible to determine which project to use: %v", err)) + return } - dataResp, httpResp, err := ds.client.DisksApi.ListDisks(ctx, *config.ProjectID) + dataResp, httpResp, err := ds.client.DisksApi.ListDisks(ctx, projectID) if err != nil { resp.Diagnostics.AddError("Failed to Fetch Disks", "Could not fetch Disk data at this time.") @@ -117,6 +119,6 @@ func (ds *disksDataSource) Read(ctx context.Context, req datasource.ReadRequest, }) } - diags = resp.State.Set(ctx, &state) + diags := resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) } diff --git a/internal/ib_network/ib_network_data_source.go b/internal/ib_network/ib_network_data_source.go index 1d45d02..79dcd1e 100644 --- a/internal/ib_network/ib_network_data_source.go +++ b/internal/ib_network/ib_network_data_source.go @@ -19,14 +19,10 @@ type ibNetworksDataSourceModel struct { IBNetworks []ibNetworkModel `tfsdk:"ib_networks"` } -type ibNetworksDataSourceFilter struct { - ProjectID *string `tfsdk:"project_id"` -} - type ibNetworkModel struct { - ID string `tfsdk:"id"` - Name string `tfsdk:"name"` - Location string `tfsdk:"location"` + ID string `tfsdk:"id"` + Name string `tfsdk:"name"` + Location string `tfsdk:"location"` } func NewIBNetworkDataSource() datasource.DataSource { @@ -75,13 +71,15 @@ func (ds *ibNetworksDataSource) Schema(ctx context.Context, request datasource.S } func (ds *ibNetworksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var config ibNetworksDataSourceFilter - diags := req.Config.Get(ctx, &config) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { + projectID, err := common.GetFallbackProject(ctx, ds.client, &resp.Diagnostics) + if err != nil { + resp.Diagnostics.AddError("Failed to fetch IB networks", + fmt.Sprintf("No project was specified and it was not possible to determine which project to use: %v", err)) + return } - dataResp, httpResp, err := ds.client.IBNetworksApi.ListIBNetworks(ctx, *config.ProjectID) + + dataResp, httpResp, err := ds.client.IBNetworksApi.ListIBNetworks(ctx, projectID) if err != nil { resp.Diagnostics.AddError("Failed to Fetch IB Networks", fmt.Sprintf("Could not fetch Infiniband network data at this time: %s", common.UnpackAPIError(err))) @@ -99,6 +97,6 @@ func (ds *ibNetworksDataSource) Read(ctx context.Context, req datasource.ReadReq }) } - diags = resp.State.Set(ctx, &state) + diags := resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) } From 34d31fc97087f1451cd4fcdbfe55f08722fcb974 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Wed, 29 Nov 2023 16:37:14 -0800 Subject: [PATCH 13/22] v1alpha5 tweaks --- go.mod | 2 +- go.sum | 6 +- internal/disk/disk_resource.go | 6 +- .../storage_attachment_type_validator.go | 14 ++-- internal/vm/util.go | 4 +- internal/vm/vm_resource.go | 82 ++++++++++++++----- 6 files changed, 78 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index 7ac6b8f..97284f7 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/BurntSushi/toml v0.3.1 github.com/antihax/optional v1.0.0 - github.com/crusoecloud/client-go v0.1.29 + github.com/crusoecloud/client-go v0.1.31 github.com/hashicorp/terraform-plugin-framework v1.3.5 ) diff --git a/go.sum b/go.sum index 6765079..7fb38a3 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,10 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/crusoecloud/client-go v0.1.29 h1:RxjVBz2nw+qERmo7ZmbDd74hW1vkn+3jxzaaaAJ/GF8= -github.com/crusoecloud/client-go v0.1.29/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= +github.com/crusoecloud/client-go v0.1.30 h1:/nH1JGB2uGpwdFXIn4GZdSLuCT/r94l85UVWyagFj/4= +github.com/crusoecloud/client-go v0.1.30/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= +github.com/crusoecloud/client-go v0.1.31 h1:a/ojZrto+cheZYQn5Sblhw2pvNLNDu7uqnR3Rm4N3A8= +github.com/crusoecloud/client-go v0.1.31/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/disk/disk_resource.go b/internal/disk/disk_resource.go index 4a740f2..cb0324c 100644 --- a/internal/disk/disk_resource.go +++ b/internal/disk/disk_resource.go @@ -148,7 +148,7 @@ func (r *diskResource) Create(ctx context.Context, req resource.CreateRequest, r } defer httpResp.Body.Close() - disk, _, err := common.AwaitOperationAndResolve[swagger.Disk](ctx, dataResp.Operation, projectID, r.client.DiskOperationsApi.GetStorageDisksOperation) + disk, _, err := common.AwaitOperationAndResolve[swagger.DiskV1Alpha5](ctx, dataResp.Operation, projectID, r.client.DiskOperationsApi.GetStorageDisksOperation) if err != nil { resp.Diagnostics.AddError("Failed to create disk", fmt.Sprintf("There was an error creating a disk: %s", common.UnpackAPIError(err))) @@ -184,7 +184,7 @@ func (r *diskResource) Read(ctx context.Context, req resource.ReadRequest, resp } defer httpResp.Body.Close() - var disk *swagger.Disk + var disk *swagger.DiskV1Alpha5 for i := range dataResp.Items { if dataResp.Items[i].Id == state.ID.ValueString() { disk = &dataResp.Items[i] @@ -238,7 +238,7 @@ func (r *diskResource) Update(ctx context.Context, req resource.UpdateRequest, r } defer httpResp.Body.Close() - _, _, err = common.AwaitOperationAndResolve[swagger.Disk](ctx, dataResp.Operation, plan.ProjectID.ValueString(), r.client.DiskOperationsApi.GetStorageDisksOperation) + _, _, err = common.AwaitOperationAndResolve[swagger.DiskV1Alpha5](ctx, dataResp.Operation, plan.ProjectID.ValueString(), r.client.DiskOperationsApi.GetStorageDisksOperation) if err != nil { resp.Diagnostics.AddError("Failed to resize disk", fmt.Sprintf("There was an error resizing a disk: %s.\n\n"+ diff --git a/internal/validators/storage_attachment_type_validator.go b/internal/validators/storage_attachment_type_validator.go index 716e611..d004533 100644 --- a/internal/validators/storage_attachment_type_validator.go +++ b/internal/validators/storage_attachment_type_validator.go @@ -10,23 +10,23 @@ import ( type DiskAttachmentType string const ( - DiskReadOnly DiskAttachmentType = "disk-readonly" - DiskReadWrite DiskAttachmentType = "disk-readwrite" + DiskReadOnly DiskAttachmentType = "read-only" + DiskReadWrite DiskAttachmentType = "read-write" ) -// StorageAttachmentTypeValidator validates that a given data storage size is accepted by the storage API. -type StorageAttachmentTypeValidator struct{} +// StorageModeValidator validates that a given data storage size is accepted by the storage API. +type StorageModeValidator struct{} -func (v StorageAttachmentTypeValidator) Description(ctx context.Context) string { +func (v StorageModeValidator) Description(ctx context.Context) string { return "Disk attachment type must be either 'disk-readonly' or 'disk-readwrite'" } -func (v StorageAttachmentTypeValidator) MarkdownDescription(ctx context.Context) string { +func (v StorageModeValidator) MarkdownDescription(ctx context.Context) string { return "Disk attachment type must be either 'disk-readonly' or 'disk-readwrite'" } //nolint:gocritic // Implements Terraform defined interface -func (v StorageAttachmentTypeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { +func (v StorageModeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return } diff --git a/internal/vm/util.go b/internal/vm/util.go index ed2711d..17f111b 100644 --- a/internal/vm/util.go +++ b/internal/vm/util.go @@ -48,7 +48,7 @@ func getDisksDiff(origDisks, newDisks []vmDiskResourceModel) (disksAdded []swagg for _, newDisk := range newDisks { matched := false for _, origDisk := range origDisks { - if newDisk.ID == origDisk.ID && newDisk.AttachmentType == origDisk.AttachmentType { + if newDisk.ID == origDisk.ID && newDisk.Mode == origDisk.Mode { matched = true break @@ -62,7 +62,7 @@ func getDisksDiff(origDisks, newDisks []vmDiskResourceModel) (disksAdded []swagg for _, origDisk := range origDisks { matched := false for _, newDisk := range newDisks { - if newDisk.ID == origDisk.ID && newDisk.AttachmentType == origDisk.AttachmentType { + if newDisk.ID == origDisk.ID && newDisk.Mode == origDisk.Mode { matched = true break diff --git a/internal/vm/vm_resource.go b/internal/vm/vm_resource.go index bc1b51e..20951e8 100644 --- a/internal/vm/vm_resource.go +++ b/internal/vm/vm_resource.go @@ -24,18 +24,19 @@ type vmResource struct { } type vmResourceModel struct { - ID types.String `tfsdk:"id"` - ProjectID types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - Type types.String `tfsdk:"type"` - SSHKey types.String `tfsdk:"ssh_key"` - Location types.String `tfsdk:"location"` - Image types.String `tfsdk:"image"` - StartupScript types.String `tfsdk:"startup_script"` - ShutdownScript types.String `tfsdk:"shutdown_script"` - IBPartitionID types.String `tfsdk:"ib_partition_id"` - Disks types.List `tfsdk:"disks"` - NetworkInterfaces types.List `tfsdk:"network_interfaces"` + ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` + SSHKey types.String `tfsdk:"ssh_key"` + Location types.String `tfsdk:"location"` + Image types.String `tfsdk:"image"` + StartupScript types.String `tfsdk:"startup_script"` + ShutdownScript types.String `tfsdk:"shutdown_script"` + IBPartitionID types.String `tfsdk:"ib_partition_id"` + Disks types.List `tfsdk:"disks"` + NetworkInterfaces types.List `tfsdk:"network_interfaces"` + HostChannelAdapters types.List `tfsdk:"host_channel_adapters"` } type vmNetworkInterfaceResourceModel struct { @@ -57,6 +58,11 @@ type vmPublicIPv4ResourceModel struct { type vmDiskResourceModel struct { ID string `tfsdk:"id"` AttachmentType string `tfsdk:"attachment_type"` + Mode string `tfsdk:"mode"` +} + +type vmHostChannelAdapter struct { + IBPartitionID string `tfsdk:"ib_partition_id"` } func NewVMResource() resource.Resource { @@ -137,7 +143,10 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res }, "attachment_type": schema.StringAttribute{ Optional: true, - Validators: []validator.String{validators.StorageAttachmentTypeValidator{}}, + }, + "mode": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{validators.StorageModeValidator{}}, }, }, }, @@ -200,10 +209,20 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res }, }, }, - "ib_partition_id": schema.StringAttribute{ + "host_channel_adapters": schema.ListNestedAttribute{ + Computed: true, Optional: true, - Description: "Infiniband Partition ID", - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates + PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, // maintain across updates + NestedObject: schema.NestedAttributeObject{ + PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()}, // maintain across updates + Attributes: map[string]schema.Attribute{ + "ib_partition_id": schema.StringAttribute{ + Optional: true, + Description: "Infiniband Partition ID", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // maintain across updates + }, + }, + }, }, }, } @@ -248,6 +267,7 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res diskIds = append(diskIds, swagger.DiskAttachment{ AttachmentType: d.AttachmentType, DiskId: d.ID, + Mode: d.Mode, }) } @@ -269,17 +289,25 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res } } + var hostChannelAdapters []swagger.PartialHostChannelAdapter + if !plan.IBPartitionID.IsUnknown() && !plan.IBPartitionID.IsNull() { + hostChannelAdapters = make([]swagger.PartialHostChannelAdapter, 0, 1) + hostChannelAdapters = append(hostChannelAdapters, swagger.PartialHostChannelAdapter{ + IbPartitionId: plan.IBPartitionID.ValueString(), + }) + } + dataResp, httpResp, err := r.client.VMsApi.CreateInstance(ctx, swagger.InstancesPostRequestV1Alpha5{ Name: plan.Name.ValueString(), - ProductName: plan.Type.ValueString(), + Type_: plan.Type.ValueString(), Location: plan.Location.ValueString(), Image: plan.Image.ValueString(), SshPublicKey: plan.SSHKey.ValueString(), StartupScript: plan.StartupScript.ValueString(), ShutdownScript: plan.ShutdownScript.ValueString(), - IbPartitionId: plan.IBPartitionID.ValueString(), NetworkInterfaces: newNetworkInterfaces, Disks: diskIds, + HostChannelAdapters: hostChannelAdapters, }, projectID) if err != nil { resp.Diagnostics.AddError("Failed to create instance", @@ -333,24 +361,27 @@ func (r *vmResource) Read(ctx context.Context, req resource.ReadRequest, resp *r state.ID = types.StringValue(instance.Id) state.Name = types.StringValue(instance.Name) - state.Type = types.StringValue(instance.ProductName) + state.Type = types.StringValue(instance.Type_) networkInterfaces, _ := vmNetworkInterfacesToTerraformResourceModel(instance.NetworkInterfaces) state.NetworkInterfaces = networkInterfaces disks := make([]vmDiskResourceModel, 0, len(instance.Disks)) for _, disk := range instance.Disks { - if !disk.IsBootDisk { + if disk.AttachmentType != "os" { attachmentType := "" + mode := "" for _, attachment := range disk.AttachedTo { if attachment.VmId == instance.Id { attachmentType = attachment.AttachmentType + mode = attachment.Mode break } } disks = append(disks, vmDiskResourceModel{ ID: disk.Id, AttachmentType: attachmentType, + Mode: mode, }) } } @@ -459,10 +490,18 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res return } + var hostChannelAdapters []swagger.PartialHostChannelAdapter + if !plan.IBPartitionID.IsUnknown() && !plan.IBPartitionID.IsNull() { + hostChannelAdapters = make([]swagger.PartialHostChannelAdapter, 0, 1) + hostChannelAdapters = append(hostChannelAdapters, swagger.PartialHostChannelAdapter{ + IbPartitionId: plan.IBPartitionID.ValueString(), + }) + } + var tNetworkInterfaces []vmNetworkInterfaceResourceModel diags = plan.NetworkInterfaces.ElementsAs(ctx, &tNetworkInterfaces, true) resp.Diagnostics.Append(diags...) - patchResp, httpResp, err := r.client.VMsApi.UpdateInstance(ctx, swagger.InstancesPatchRequest{ + patchResp, httpResp, err := r.client.VMsApi.UpdateInstance(ctx, swagger.InstancesPatchRequestV1Alpha5{ Action: "UPDATE", NetworkInterfaces: []swagger.NetworkInterface{{ Ips: []swagger.IpAddresses{{ @@ -472,6 +511,7 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res }, }}, }}, + HostChannelAdapters: hostChannelAdapters, }, state.ProjectID.ValueString(), state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to update instance network interface", From 169ce7702dd571cf20d7952e44e9ca86d4c90817 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Thu, 30 Nov 2023 09:49:06 -0800 Subject: [PATCH 14/22] update api to reflect latest changes --- crusoe/provider.go | 2 +- examples/vms/main.tf | 32 ++++---- .../storage_attachment_type_validator.go | 10 +-- internal/vm/util.go | 22 ++++++ internal/vm/vm_data_source.go | 15 ++-- internal/vm/vm_resource.go | 73 ++++++++++++------- 6 files changed, 101 insertions(+), 53 deletions(-) diff --git a/crusoe/provider.go b/crusoe/provider.go index ee0260c..783cc9e 100644 --- a/crusoe/provider.go +++ b/crusoe/provider.go @@ -109,7 +109,7 @@ func (p *crusoeProvider) Configure(ctx context.Context, req provider.ConfigureRe resp.Diagnostics.AddAttributeWarning( path.Root("default_project"), "Missing Crusoe Default Project", - "The provider did not find a default project specified in the configuration file and will attempt to infer the project to use. "+ + "The provider did not find a default project specified in the configuration file and will attempt to infer the project to use if not specified. "+ "Set the value in ~/.crusoe/config. "+ "If either is already set, ensure the value is not empty.", ) diff --git a/examples/vms/main.tf b/examples/vms/main.tf index 307da5d..d0ead03 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -10,38 +10,42 @@ locals { my_ssh_key = file("~/.ssh/id_rsa.pub") } -resource "crusoe_project" "my_project" { - name = "my-new-project" +variable "project_id" { + type = string + default = "9da86c46-900f-49f3-b56b-67123c562c3c" } // new VM resource "crusoe_compute_instance" "my_vm" { count = 2 name = "my-new-vm-${count.index}" - type = "a100-80gb.1x" - location = "us-northcentraldevelopment1-a" + type = "a40.1x" + location = "us-northcentralstaging1-a" # optionally specify a different base image #image = "nvidia-docker" + disks = [ + // disk attached at startup + { + id = crusoe_storage_disk.data_disk.id + mode = "read-only" + attachment_type = "data" + } + ] + ssh_key = local.my_ssh_key startup_script = file("startup.sh") - project_id = crusoe_project.my_project.id + project_id = var.project_id + - disks = [ - // disk attached at startup - { - id = crusoe_storage_disk.data_disk.id - attachment_type = "disk-readonly" - } - ] } resource "crusoe_storage_disk" "data_disk" { name = "data-disk" size = "200GiB" - project_id = crusoe_project.my_project.id - location = "us-northcentraldevelopment1-a" + project_id = var.project_id + location = "us-northcentralstaging1-a" } diff --git a/internal/validators/storage_attachment_type_validator.go b/internal/validators/storage_attachment_type_validator.go index d004533..ddca94f 100644 --- a/internal/validators/storage_attachment_type_validator.go +++ b/internal/validators/storage_attachment_type_validator.go @@ -7,11 +7,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) -type DiskAttachmentType string +type DiskModeType string const ( - DiskReadOnly DiskAttachmentType = "read-only" - DiskReadWrite DiskAttachmentType = "read-write" + DiskReadOnly DiskModeType = "read-only" + DiskReadWrite DiskModeType = "read-write" ) // StorageModeValidator validates that a given data storage size is accepted by the storage API. @@ -35,8 +35,8 @@ func (v StorageModeValidator) ValidateString(ctx context.Context, req validator. input = strings.ToLower(input) if input != string(DiskReadOnly) && input != string(DiskReadWrite){ - resp.Diagnostics.AddAttributeError(req.Path, "Unsupported Disk Attachment Type", - "Disk attachment type must be either 'disk-readonly' or 'disk-readwrite'") + resp.Diagnostics.AddAttributeError(req.Path, "Unsupported Disk Mode Type", + "Disk mode type must be either 'read-only' or 'read-write'") } return diff --git a/internal/vm/util.go b/internal/vm/util.go index 17f111b..14db84c 100644 --- a/internal/vm/util.go +++ b/internal/vm/util.go @@ -39,6 +39,13 @@ var vmDiskAttachmentSchema = types.ObjectType{ AttrTypes: map[string]attr.Type{ "id": types.StringType, "attachment_type": types.StringType, + "mode": types.StringType, + }, +} + +var vmHostChannelAdapterSchema = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "ib_partition_id": types.StringType, }, } @@ -154,12 +161,27 @@ func vmNetworkInterfacesToTerraformResourceModel(networkInterfaces []swagger.Net return values, warning } +func vmPartialHostChannelAdaptersToTerraformResourceModel(hostChannelAdapters []swagger.HostChannelAdapter) (hostChannelAdaptersList types.List, warning string) { + hcas := make([]vmHostChannelAdapterResourceModel, 0, len(hostChannelAdapters)) + for _, hca := range hostChannelAdapters { + + hcas = append(hcas, vmHostChannelAdapterResourceModel{ + IBPartitionID: hca.IbPartitionId, + }) + } + + values, _ := types.ListValueFrom(context.Background(), vmHostChannelAdapterSchema, hcas) + + return values, warning +} + func vmDiskAttachmentToTerraformResourceModel(diskAttachments []swagger.DiskAttachment) (diskAttachmentsList types.List, diags diag.Diagnostics) { attachments := make([]vmDiskResourceModel, 0, len(diskAttachments)) for _, diskAttachment := range diskAttachments { attachments = append(attachments, vmDiskResourceModel{ ID: diskAttachment.DiskId, AttachmentType: diskAttachment.AttachmentType, + Mode: diskAttachment.Mode, }) } diff --git a/internal/vm/vm_data_source.go b/internal/vm/vm_data_source.go index cb0f4cc..fe16f13 100644 --- a/internal/vm/vm_data_source.go +++ b/internal/vm/vm_data_source.go @@ -90,6 +90,9 @@ func (ds *vmDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, re "attachment_type": schema.StringAttribute{ Required: true, }, + "mode": schema.StringAttribute{ + Required: true, + }, }, }, }, @@ -158,19 +161,21 @@ func (ds *vmDataSource) Read(ctx context.Context, req datasource.ReadRequest, re state.ID = &vm.Id state.ProjectID = &vm.ProjectId state.Name = &vm.Name - state.Type = &vm.ProductName + state.Type = &vm.Type_ attachedDisks := make([]vmDiskResourceModel, 0, len(vm.Disks)) - for _, disk := range vm.Disks{ + for _, disk := range vm.Disks { attachmentType := "" - for _,attachment:=range disk.AttachedTo { - if attachment.VmId ==vm.Id { + mode := "" + for _, attachment := range disk.AttachedTo { + if attachment.VmId == vm.Id { attachmentType = attachment.AttachmentType break } } attachedDisks = append(attachedDisks, vmDiskResourceModel{ - ID: disk.Id, + ID: disk.Id, AttachmentType: attachmentType, + Mode: mode, }) } diff --git a/internal/vm/vm_resource.go b/internal/vm/vm_resource.go index 20951e8..d070fcc 100644 --- a/internal/vm/vm_resource.go +++ b/internal/vm/vm_resource.go @@ -3,7 +3,6 @@ package vm import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -33,7 +32,6 @@ type vmResourceModel struct { Image types.String `tfsdk:"image"` StartupScript types.String `tfsdk:"startup_script"` ShutdownScript types.String `tfsdk:"shutdown_script"` - IBPartitionID types.String `tfsdk:"ib_partition_id"` Disks types.List `tfsdk:"disks"` NetworkInterfaces types.List `tfsdk:"network_interfaces"` HostChannelAdapters types.List `tfsdk:"host_channel_adapters"` @@ -61,7 +59,7 @@ type vmDiskResourceModel struct { Mode string `tfsdk:"mode"` } -type vmHostChannelAdapter struct { +type vmHostChannelAdapterResourceModel struct { IBPartitionID string `tfsdk:"ib_partition_id"` } @@ -142,7 +140,7 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res Optional: true, }, "attachment_type": schema.StringAttribute{ - Optional: true, + Optional: true, }, "mode": schema.StringAttribute{ Optional: true, @@ -290,23 +288,30 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res } var hostChannelAdapters []swagger.PartialHostChannelAdapter - if !plan.IBPartitionID.IsUnknown() && !plan.IBPartitionID.IsNull() { - hostChannelAdapters = make([]swagger.PartialHostChannelAdapter, 0, 1) - hostChannelAdapters = append(hostChannelAdapters, swagger.PartialHostChannelAdapter{ - IbPartitionId: plan.IBPartitionID.ValueString(), - }) + if !plan.HostChannelAdapters.IsUnknown() && !plan.HostChannelAdapters.IsNull() { + tHostChannelAdapters := make([]vmHostChannelAdapterResourceModel, 0, len(plan.HostChannelAdapters.Elements())) + diags = plan.HostChannelAdapters.ElementsAs(ctx, &tHostChannelAdapters, true) + resp.Diagnostics.Append(diags...) + + for _, hca := range tHostChannelAdapters { + hostChannelAdapters = []swagger.PartialHostChannelAdapter{ + { + IbPartitionId: hca.IBPartitionID, + }, + } + } } dataResp, httpResp, err := r.client.VMsApi.CreateInstance(ctx, swagger.InstancesPostRequestV1Alpha5{ - Name: plan.Name.ValueString(), - Type_: plan.Type.ValueString(), - Location: plan.Location.ValueString(), - Image: plan.Image.ValueString(), - SshPublicKey: plan.SSHKey.ValueString(), - StartupScript: plan.StartupScript.ValueString(), - ShutdownScript: plan.ShutdownScript.ValueString(), - NetworkInterfaces: newNetworkInterfaces, - Disks: diskIds, + Name: plan.Name.ValueString(), + Type_: plan.Type.ValueString(), + Location: plan.Location.ValueString(), + Image: plan.Image.ValueString(), + SshPublicKey: plan.SSHKey.ValueString(), + StartupScript: plan.StartupScript.ValueString(), + ShutdownScript: plan.ShutdownScript.ValueString(), + NetworkInterfaces: newNetworkInterfaces, + Disks: diskIds, HostChannelAdapters: hostChannelAdapters, }, projectID) if err != nil { @@ -330,13 +335,18 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res networkInterfaces, _ := vmNetworkInterfacesToTerraformResourceModel(instance.NetworkInterfaces) plan.NetworkInterfaces = networkInterfaces - disks, diags := vmDiskAttachmentToTerraformResourceModel(diskIds) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + if len(diskIds) > 0 { + disks, diags := vmDiskAttachmentToTerraformResourceModel(diskIds) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + plan.Disks = disks } - plan.Disks = disks + plan.ProjectID = types.StringValue(projectID) + hcas, _ := vmPartialHostChannelAdaptersToTerraformResourceModel(instance.HostChannelAdapters) + plan.HostChannelAdapters = hcas diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) @@ -491,11 +501,18 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res } var hostChannelAdapters []swagger.PartialHostChannelAdapter - if !plan.IBPartitionID.IsUnknown() && !plan.IBPartitionID.IsNull() { - hostChannelAdapters = make([]swagger.PartialHostChannelAdapter, 0, 1) - hostChannelAdapters = append(hostChannelAdapters, swagger.PartialHostChannelAdapter{ - IbPartitionId: plan.IBPartitionID.ValueString(), - }) + if !plan.HostChannelAdapters.IsUnknown() && !plan.HostChannelAdapters.IsNull() { + tHostChannelAdapters := make([]vmHostChannelAdapterResourceModel, 0, len(plan.HostChannelAdapters.Elements())) + diags = plan.HostChannelAdapters.ElementsAs(ctx, &tHostChannelAdapters, true) + resp.Diagnostics.Append(diags...) + + for _, hca := range tHostChannelAdapters { + hostChannelAdapters = []swagger.PartialHostChannelAdapter{ + { + IbPartitionId: hca.IBPartitionID, + }, + } + } } var tNetworkInterfaces []vmNetworkInterfaceResourceModel From 817b53130bb6d31bedd3b6c3140f7075e834d795 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Thu, 30 Nov 2023 15:58:31 -0800 Subject: [PATCH 15/22] update examples --- examples/infiniband/main.tf | 5 ++-- examples/project-variable/main.tf | 38 +++++++++++++++++++------------ examples/vms/main.tf | 31 +++++++++++++------------ 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/examples/infiniband/main.tf b/examples/infiniband/main.tf index a8195d2..d1b9cec 100644 --- a/examples/infiniband/main.tf +++ b/examples/infiniband/main.tf @@ -8,7 +8,7 @@ terraform { locals { - my_ssh_key = file("~/.ssh/id_rsa.pub") + my_ssh_key = file("~/.ssh/id_ed25519.pub") } resource "crusoe_project" "my_project" { @@ -48,7 +48,8 @@ resource "crusoe_compute_instance" "my_vm1" { // disk attached at startup { id = crusoe_storage_disk.data_disk.id - attachment_type = "disk-readonly" + attachment_type = "data" + mode = "read-only" } ] } diff --git a/examples/project-variable/main.tf b/examples/project-variable/main.tf index 718e456..cb1ac33 100644 --- a/examples/project-variable/main.tf +++ b/examples/project-variable/main.tf @@ -7,34 +7,43 @@ terraform { } locals { - my_ssh_key = file("~/.ssh/id_rsa.pub") + my_ssh_key = file("~/.ssh/id_ed25519.pub") +} + +variable "project_id" { + type = string + default = "" } // new VM resource "crusoe_compute_instance" "my_vm" { - name = "my-new-vm4" - type = "a100-80gb.1x" - location = "us-northcentraldevelopment1-a" + name = "my-new-vm" + type = "a40.1x" + location = "us-northcentral1-a" # optionally specify a different base image #image = "nvidia-docker" - ssh_key = local.my_ssh_key - disks = [ - // disk attached at startup - { - id = crusoe_storage_disk.data_disk.id - attachment_type = "disk-readonly" - } - ] + // disk attached at startup + { + id = crusoe_storage_disk.data_disk.id + mode = "read-only" + attachment_type = "data" + } + ] + + ssh_key = local.my_ssh_key + startup_script = file("startup.sh") + project_id = var.project_id } resource "crusoe_storage_disk" "data_disk" { - name = "data-disk5" + name = "data-disk" size = "200GiB" - location = "us-northcentraldevelopment1-a" + project_id = var.project_id + location = "us-northcentral1-a" } // firewall rule @@ -49,4 +58,5 @@ resource "crusoe_vpc_firewall_rule" "open_fw_rule" { source_ports = "1-65535" destination = crusoe_compute_instance.my_vm.network_interfaces[0].public_ipv4.address destination_ports = "1-65535" + project_id = var.project_id } diff --git a/examples/vms/main.tf b/examples/vms/main.tf index d0ead03..e5469c8 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -7,20 +7,14 @@ terraform { } locals { - my_ssh_key = file("~/.ssh/id_rsa.pub") -} - -variable "project_id" { - type = string - default = "9da86c46-900f-49f3-b56b-67123c562c3c" + my_ssh_key = file("~/.ssh/id_ed25519.pub") } // new VM resource "crusoe_compute_instance" "my_vm" { - count = 2 - name = "my-new-vm-${count.index}" + name = "my-new-vm" type = "a40.1x" - location = "us-northcentralstaging1-a" + location = "us-northcentral1-a" # optionally specify a different base image #image = "nvidia-docker" @@ -36,16 +30,25 @@ resource "crusoe_compute_instance" "my_vm" { ssh_key = local.my_ssh_key startup_script = file("startup.sh") - project_id = var.project_id - - } resource "crusoe_storage_disk" "data_disk" { name = "data-disk" size = "200GiB" - project_id = var.project_id - location = "us-northcentralstaging1-a" + location = "us-northcentral1-a" } +// firewall rule +// note: this allows all ingress over TCP to our VM +resource "crusoe_vpc_firewall_rule" "open_fw_rule" { + network = crusoe_compute_instance.my_vm.network_interfaces[0].network + name = "example-terraform-rule" + action = "allow" + direction = "ingress" + protocols = "tcp" + source = "0.0.0.0/0" + source_ports = "1-65535" + destination = crusoe_compute_instance.my_vm.network_interfaces[0].public_ipv4.address + destination_ports = "1-65535" +} From e9318cb33e058096b0e94545ed5ffa7b1ed526be Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 5 Dec 2023 11:40:35 -0800 Subject: [PATCH 16/22] api updates --- examples/project-variable/main.tf | 2 +- examples/vms/main.tf | 2 +- go.mod | 2 +- go.sum | 1 + internal/vm/vm_data_source.go | 12 ++---------- internal/vm/vm_resource.go | 13 ++----------- 6 files changed, 8 insertions(+), 24 deletions(-) diff --git a/examples/project-variable/main.tf b/examples/project-variable/main.tf index cb1ac33..fca2f42 100644 --- a/examples/project-variable/main.tf +++ b/examples/project-variable/main.tf @@ -22,7 +22,7 @@ resource "crusoe_compute_instance" "my_vm" { location = "us-northcentral1-a" # optionally specify a different base image - #image = "nvidia-docker" + image = "ubuntu20.04:latest" disks = [ // disk attached at startup diff --git a/examples/vms/main.tf b/examples/vms/main.tf index e5469c8..04ec517 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -17,7 +17,7 @@ resource "crusoe_compute_instance" "my_vm" { location = "us-northcentral1-a" # optionally specify a different base image - #image = "nvidia-docker" + image = "ubuntu20.04:latest" disks = [ // disk attached at startup diff --git a/go.mod b/go.mod index 97284f7..750b354 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/BurntSushi/toml v0.3.1 github.com/antihax/optional v1.0.0 - github.com/crusoecloud/client-go v0.1.31 + github.com/crusoecloud/client-go v0.1.32 github.com/hashicorp/terraform-plugin-framework v1.3.5 ) diff --git a/go.sum b/go.sum index 7fb38a3..bd6f1b7 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,7 @@ github.com/crusoecloud/client-go v0.1.30 h1:/nH1JGB2uGpwdFXIn4GZdSLuCT/r94l85UVW github.com/crusoecloud/client-go v0.1.30/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= github.com/crusoecloud/client-go v0.1.31 h1:a/ojZrto+cheZYQn5Sblhw2pvNLNDu7uqnR3Rm4N3A8= github.com/crusoecloud/client-go v0.1.31/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= +github.com/crusoecloud/client-go v0.1.32/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/vm/vm_data_source.go b/internal/vm/vm_data_source.go index fe16f13..1ea2614 100644 --- a/internal/vm/vm_data_source.go +++ b/internal/vm/vm_data_source.go @@ -164,18 +164,10 @@ func (ds *vmDataSource) Read(ctx context.Context, req datasource.ReadRequest, re state.Type = &vm.Type_ attachedDisks := make([]vmDiskResourceModel, 0, len(vm.Disks)) for _, disk := range vm.Disks { - attachmentType := "" - mode := "" - for _, attachment := range disk.AttachedTo { - if attachment.VmId == vm.Id { - attachmentType = attachment.AttachmentType - break - } - } attachedDisks = append(attachedDisks, vmDiskResourceModel{ ID: disk.Id, - AttachmentType: attachmentType, - Mode: mode, + AttachmentType: disk.AttachmentType, + Mode: disk.Mode, }) } diff --git a/internal/vm/vm_resource.go b/internal/vm/vm_resource.go index d070fcc..94f5f00 100644 --- a/internal/vm/vm_resource.go +++ b/internal/vm/vm_resource.go @@ -379,19 +379,10 @@ func (r *vmResource) Read(ctx context.Context, req resource.ReadRequest, resp *r disks := make([]vmDiskResourceModel, 0, len(instance.Disks)) for _, disk := range instance.Disks { if disk.AttachmentType != "os" { - attachmentType := "" - mode := "" - for _, attachment := range disk.AttachedTo { - if attachment.VmId == instance.Id { - attachmentType = attachment.AttachmentType - mode = attachment.Mode - break - } - } disks = append(disks, vmDiskResourceModel{ ID: disk.Id, - AttachmentType: attachmentType, - Mode: mode, + AttachmentType: disk.AttachmentType, + Mode: disk.Mode, }) } } From 524e5a81b60e34971e545cda69d69ac52b1138bc Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 5 Dec 2023 12:37:52 -0800 Subject: [PATCH 17/22] mr feedback --- examples/infiniband/main.tf | 16 ++++++++++------ examples/project-variable/main.tf | 6 +++--- examples/vms/main.tf | 2 +- go.sum | 5 +---- internal/common/util.go | 3 ++- internal/disk/disk_resource.go | 6 +++--- internal/firewall_rule/firewall_rule_resource.go | 5 +++-- internal/ib_partition/ib_partition_resource.go | 2 +- internal/project/project_resource.go | 7 ++++--- internal/project/projects_data_source.go | 6 ++++-- internal/vm/vm_data_source.go | 4 +++- 11 files changed, 35 insertions(+), 27 deletions(-) diff --git a/examples/infiniband/main.tf b/examples/infiniband/main.tf index d1b9cec..920189d 100644 --- a/examples/infiniband/main.tf +++ b/examples/infiniband/main.tf @@ -32,18 +32,22 @@ resource "crusoe_ib_partition" "my_partition" { project_id = crusoe_project.my_project.id } -# create two VMs, both in the same Infiniband partition +# create multiple VMs, all in the same Infiniband partition resource "crusoe_compute_instance" "my_vm1" { count = 8 name = "ib-vm-${count.index}" - type = "a100-80gb-sxm-ib.8x" # IB enabled VM type, `a100-80gb-sxm-ib.8x` or h100-80gb-sxm-ib.8x` - location = "us-east1-a" # IB currently only supported in `us-east1-a` - image = "ubuntu20.04-nvidia-sxm-docker:latest" # IB image, see full list at https://docs.crusoecloud.com/compute/images/overview/index.html#list-of-curated-images - ib_partition_id = crusoe_ib_partition.my_partition.id + type = "a100-80gb-sxm-ib.8x" # IB enabled VM type + location = "us-east1-a" # IB currently only supported at us-east1-a + image = "ubuntu20.04-nvidia-sxm-docker:ib-nccl2.18.3" # IB image + ssh_key = local.my_ssh_key - project_id = crusoe_project.my_project.id + host_channel_adapters = [ + { + ib_partition_id = crusoe_ib_partition.my_partition.id + } + ] disks = [ // disk attached at startup { diff --git a/examples/project-variable/main.tf b/examples/project-variable/main.tf index fca2f42..a0fa8e7 100644 --- a/examples/project-variable/main.tf +++ b/examples/project-variable/main.tf @@ -12,7 +12,7 @@ locals { variable "project_id" { type = string - default = "" + default = "" } // new VM @@ -21,14 +21,14 @@ resource "crusoe_compute_instance" "my_vm" { type = "a40.1x" location = "us-northcentral1-a" - # optionally specify a different base image + # specify the base image image = "ubuntu20.04:latest" disks = [ // disk attached at startup { id = crusoe_storage_disk.data_disk.id - mode = "read-only" + mode = "read-write" attachment_type = "data" } ] diff --git a/examples/vms/main.tf b/examples/vms/main.tf index 04ec517..de432bd 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -16,7 +16,7 @@ resource "crusoe_compute_instance" "my_vm" { type = "a40.1x" location = "us-northcentral1-a" - # optionally specify a different base image + # specify the base image image = "ubuntu20.04:latest" disks = [ diff --git a/go.sum b/go.sum index bd6f1b7..0d87329 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,7 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/crusoecloud/client-go v0.1.30 h1:/nH1JGB2uGpwdFXIn4GZdSLuCT/r94l85UVWyagFj/4= -github.com/crusoecloud/client-go v0.1.30/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= -github.com/crusoecloud/client-go v0.1.31 h1:a/ojZrto+cheZYQn5Sblhw2pvNLNDu7uqnR3Rm4N3A8= -github.com/crusoecloud/client-go v0.1.31/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= +github.com/crusoecloud/client-go v0.1.32 h1:c1f0lo8/T/3dkdF5KbxjWSjP1fnG+DE0luOo6q1jKWY= github.com/crusoecloud/client-go v0.1.32/go.mod h1:k1FgpUllEJtE53osEwsF+JfbFKILn5t3UuBdHYBVpdY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/internal/common/util.go b/internal/common/util.go index 5e6848d..5ac60b2 100644 --- a/internal/common/util.go +++ b/internal/common/util.go @@ -16,7 +16,7 @@ import ( const ( // TODO: pull from config set during build - version = "v0.4.1" + version = "v0.5.0" pollInterval = 2 * time.Second @@ -127,6 +127,7 @@ func GetFallbackProject(ctx context.Context, client *swagger.APIClient, diag *di } dataResp, httpResp, err := client.ProjectsApi.ListProjects(ctx, opts) + defer httpResp.Body.Close() if err != nil { diff --git a/internal/disk/disk_resource.go b/internal/disk/disk_resource.go index cb0324c..6a6e214 100644 --- a/internal/disk/disk_resource.go +++ b/internal/disk/disk_resource.go @@ -178,7 +178,7 @@ func (r *diskResource) Read(ctx context.Context, req resource.ReadRequest, resp dataResp, httpResp, err := r.client.DisksApi.ListDisks(ctx, state.ProjectID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to get disks", - fmt.Sprintf("Fetching Crusoe disks failed: %s\n\nIf the problem persists, contact support@crusoecloud.com", err.Error())) + fmt.Sprintf("Fetching Crusoe disks failed: %s\n\nIf the problem persists, contact support@crusoecloud.com", common.UnpackAPIError(err))) return } @@ -232,7 +232,7 @@ func (r *diskResource) Update(ctx context.Context, req resource.UpdateRequest, r resp.Diagnostics.AddError("Failed to resize disk", fmt.Sprintf("There was an error starting a resize operation: %s.\n\n"+ "Make sure the disk still exists, you are englarging the disk,"+ - " and if the disk is attached to a VM, the VM is powered off.", err.Error())) + " and if the disk is attached to a VM, the VM is powered off.", common.UnpackAPIError(err))) return } @@ -243,7 +243,7 @@ func (r *diskResource) Update(ctx context.Context, req resource.UpdateRequest, r resp.Diagnostics.AddError("Failed to resize disk", fmt.Sprintf("There was an error resizing a disk: %s.\n\n"+ "Make sure the disk still exists, you are englarging the disk,"+ - " and if the disk is attached to a VM, the VM is powered off.", err.Error())) + " and if the disk is attached to a VM, the VM is powered off.", common.UnpackAPIError(err))) return } diff --git a/internal/firewall_rule/firewall_rule_resource.go b/internal/firewall_rule/firewall_rule_resource.go index bf868ee..d63c40c 100644 --- a/internal/firewall_rule/firewall_rule_resource.go +++ b/internal/firewall_rule/firewall_rule_resource.go @@ -123,8 +123,6 @@ func (r *firewallRuleResource) Create(ctx context.Context, req resource.CreateRe if resp.Diagnostics.HasError() { return } - sourcePortsStr := strings.ReplaceAll(plan.SourcePorts.ValueString(), "*", "1-65535") - destPortsStr := strings.ReplaceAll(plan.DestinationPorts.ValueString(), "*", "1-65535") projectID := "" if plan.ProjectID.ValueString() == ""{ @@ -140,6 +138,9 @@ func (r *firewallRuleResource) Create(ctx context.Context, req resource.CreateRe projectID = plan.ProjectID.ValueString() } + sourcePortsStr := strings.ReplaceAll(plan.SourcePorts.ValueString(), "*", "1-65535") + destPortsStr := strings.ReplaceAll(plan.DestinationPorts.ValueString(), "*", "1-65535") + dataResp, httpResp, err := r.client.VPCFirewallRulesApi.CreateVPCFirewallRule(ctx, swagger.VpcFirewallRulesPostRequestV1Alpha5{ VpcNetworkId: plan.Network.ValueString(), Name: plan.Name.ValueString(), diff --git a/internal/ib_partition/ib_partition_resource.go b/internal/ib_partition/ib_partition_resource.go index 600dc08..a629f48 100644 --- a/internal/ib_partition/ib_partition_resource.go +++ b/internal/ib_partition/ib_partition_resource.go @@ -140,7 +140,7 @@ func (r *ibPartitionResource) Read(ctx context.Context, req resource.ReadRequest } resp.Diagnostics.AddError("Failed to get IB partition", - fmt.Sprintf("Fetching Crusoe Infiniband partition failed: %s\n\nIf the problem persists, contact support@crusoecloud.com", err.Error())) + fmt.Sprintf("Fetching Crusoe Infiniband partition failed: %s\n\nIf the problem persists, contact support@crusoecloud.com", common.UnpackAPIError(err))) return } diff --git a/internal/project/project_resource.go b/internal/project/project_resource.go index 488d4a4..0c77095 100644 --- a/internal/project/project_resource.go +++ b/internal/project/project_resource.go @@ -90,7 +90,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest }) if err != nil { resp.Diagnostics.AddError("Failed to create project", - fmt.Sprintf("There was an error starting a create project operation: %s", common.UnpackAPIError(err))) + fmt.Sprintf("There was an error starting a create project operation: %s.", common.UnpackAPIError(err))) return } @@ -114,10 +114,11 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re return } + project, httpResp, err := r.client.ProjectsApi.GetProject(ctx, state.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to get projects", - fmt.Sprintf("Fetching Crusoe projects failed: %s\n\nIf the problem persists, contact support@crusoecloud.com", err.Error())) + fmt.Sprintf("Fetching Crusoe projects failed: %s\n\nIf the problem persists, contact support@crusoecloud.com", common.UnpackAPIError(err))) return } @@ -152,7 +153,7 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest ) if err != nil { resp.Diagnostics.AddError("Failed to update project", - fmt.Sprintf("There was an error starting an update project operation: %s.", err.Error())) + fmt.Sprintf("There was an error starting an update project operation: %s.", common.UnpackAPIError(err))) return } diff --git a/internal/project/projects_data_source.go b/internal/project/projects_data_source.go index f30c684..4baf723 100644 --- a/internal/project/projects_data_source.go +++ b/internal/project/projects_data_source.go @@ -2,8 +2,9 @@ package project import ( "context" - + "fmt" "github.com/antihax/optional" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -76,7 +77,8 @@ func (ds *projectsDataSource) Read(ctx context.Context, req datasource.ReadReque dataResp, httpResp, err := ds.client.ProjectsApi.ListProjects(ctx, opts) if err != nil { - resp.Diagnostics.AddError("Failed to Fetch Projects", "Could not fetch Project data at this time.") + resp.Diagnostics.AddError("Failed to Fetch Projects", + fmt.Sprintf("Could not fetch Project data at this time: %v.", common.UnpackAPIError(err))) return } diff --git a/internal/vm/vm_data_source.go b/internal/vm/vm_data_source.go index 1ea2614..75769fb 100644 --- a/internal/vm/vm_data_source.go +++ b/internal/vm/vm_data_source.go @@ -2,6 +2,7 @@ package vm import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -153,7 +154,8 @@ func (ds *vmDataSource) Read(ctx context.Context, req datasource.ReadRequest, re if config.ID != nil { vm, err := getVM(ctx, ds.client, *config.ProjectID, *config.ID) if err != nil { - resp.Diagnostics.AddError("Failed to get Instance", err.Error()) + resp.Diagnostics.AddError("Failed to get Instance", fmt.Sprintf("Failed to get instance: %s.", + common.UnpackAPIError(err))) return } From dc0bd9d4c56e15b7c0cbc7a053ad9524d87897fb Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 5 Dec 2023 14:30:46 -0800 Subject: [PATCH 18/22] update default endpoint --- internal/common/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/common/config.go b/internal/common/config.go index a4820b3..24e9042 100644 --- a/internal/common/config.go +++ b/internal/common/config.go @@ -10,7 +10,7 @@ import ( const ( configFilePath = "/.crusoe/config" // full path is this appended to the user's home path - defaultApiEndpoint = "https://api.crusoecloud.com/v1alpha4" + defaultApiEndpoint = "https://api.crusoecloud.com/v1alpha5" ) // Config holds options that can be set via ~/.crusoe/config and env variables. From be44bacd6a136eddae55f0134046a2584e08b226 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 5 Dec 2023 15:51:31 -0800 Subject: [PATCH 19/22] updates' --- examples/infiniband/main.tf | 7 +------ examples/project-variable/main.tf | 14 +++++--------- examples/vms/main.tf | 2 +- internal/disk/disk_resource.go | 18 +++++++++++++++++- internal/vm/vm_resource.go | 29 +++++++++++++++++++++++------ 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/examples/infiniband/main.tf b/examples/infiniband/main.tf index 920189d..b50b47c 100644 --- a/examples/infiniband/main.tf +++ b/examples/infiniband/main.tf @@ -11,10 +11,6 @@ locals { my_ssh_key = file("~/.ssh/id_ed25519.pub") } -resource "crusoe_project" "my_project" { - name = "my-new-project" -} - # list IB networks data "crusoe_ib_networks" "ib_networks" {} output "crusoe_ib" { @@ -39,7 +35,7 @@ resource "crusoe_compute_instance" "my_vm1" { name = "ib-vm-${count.index}" type = "a100-80gb-sxm-ib.8x" # IB enabled VM type location = "us-east1-a" # IB currently only supported at us-east1-a - image = "ubuntu20.04-nvidia-sxm-docker:ib-nccl2.18.3" # IB image + image = "ubuntu22.04-nvidia-sxm-docker:latest" # IB image ssh_key = local.my_ssh_key @@ -63,5 +59,4 @@ resource "crusoe_storage_disk" "data_disk" { name = "data-disk" size = "1TiB" location = "us-east1-a" - project_id = crusoe_project.my_project.id } diff --git a/examples/project-variable/main.tf b/examples/project-variable/main.tf index a0fa8e7..34e186c 100644 --- a/examples/project-variable/main.tf +++ b/examples/project-variable/main.tf @@ -7,23 +7,20 @@ terraform { } locals { - my_ssh_key = file("~/.ssh/id_ed25519.pub") + my_ssh_key = file("~/.ssh/id_rsa.pub") } variable "project_id" { type = string - default = "" + default = "d64251a8-3a40-4692-a146-abc536e3922c" } // new VM resource "crusoe_compute_instance" "my_vm" { - name = "my-new-vm" + name = "ajeyaraj-my-new-vm" type = "a40.1x" location = "us-northcentral1-a" - # specify the base image - image = "ubuntu20.04:latest" - disks = [ // disk attached at startup { @@ -34,13 +31,12 @@ resource "crusoe_compute_instance" "my_vm" { ] ssh_key = local.my_ssh_key - startup_script = file("startup.sh") project_id = var.project_id } resource "crusoe_storage_disk" "data_disk" { - name = "data-disk" + name = "ajeyaraj-data-disk" size = "200GiB" project_id = var.project_id location = "us-northcentral1-a" @@ -50,7 +46,7 @@ resource "crusoe_storage_disk" "data_disk" { // note: this allows all ingress over TCP to our VM resource "crusoe_vpc_firewall_rule" "open_fw_rule" { network = crusoe_compute_instance.my_vm.network_interfaces[0].network - name = "example-terraform-rule" + name = "ajeyaraj-example-terraform-rule" action = "allow" direction = "ingress" protocols = "tcp" diff --git a/examples/vms/main.tf b/examples/vms/main.tf index de432bd..20f947f 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -7,7 +7,7 @@ terraform { } locals { - my_ssh_key = file("~/.ssh/id_ed25519.pub") + my_ssh_key = file("~/.ssh/id_rsa.pub") } // new VM diff --git a/internal/disk/disk_resource.go b/internal/disk/disk_resource.go index 6a6e214..c67e28c 100644 --- a/internal/disk/disk_resource.go +++ b/internal/disk/disk_resource.go @@ -175,7 +175,23 @@ func (r *diskResource) Read(ctx context.Context, req resource.ReadRequest, resp return } - dataResp, httpResp, err := r.client.DisksApi.ListDisks(ctx, state.ProjectID.ValueString()) + // We only have this parsing for transitioning from v1alpha4 to v1alpha5 because old tf state files will not + // have project ID stored. So we will try to get a fallback project to pass to the API. + projectID := "" + if state.ProjectID.ValueString() == "" { + project, err := common.GetFallbackProject(ctx, r.client, &resp.Diagnostics) + if err != nil { + + resp.Diagnostics.AddError("Failed to create disk", + fmt.Sprintf("No project was specified and it was not possible to determine which project to use: %v", err)) + return + } + projectID = project + } else { + projectID = state.ProjectID.ValueString() + } + + dataResp, httpResp, err := r.client.DisksApi.ListDisks(ctx, projectID) if err != nil { resp.Diagnostics.AddError("Failed to get disks", fmt.Sprintf("Fetching Crusoe disks failed: %s\n\nIf the problem persists, contact support@crusoecloud.com", common.UnpackAPIError(err))) diff --git a/internal/vm/vm_resource.go b/internal/vm/vm_resource.go index 94f5f00..55548b4 100644 --- a/internal/vm/vm_resource.go +++ b/internal/vm/vm_resource.go @@ -100,7 +100,7 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res "project_id": schema.StringAttribute{ Optional: true, Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace(), stringplanmodifier.UseStateForUnknown()}, // cannot be updated in place }, "type": schema.StringAttribute{ Required: true, @@ -122,7 +122,7 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res }, "image": schema.StringAttribute{ Optional: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, // cannot be updated in place + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, // cannot be updated in place }, "startup_script": schema.StringAttribute{ Optional: true, @@ -137,13 +137,13 @@ func (r *vmResource) Schema(ctx context.Context, req resource.SchemaRequest, res NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Optional: true, + Required: true, }, "attachment_type": schema.StringAttribute{ - Optional: true, + Required: true, }, "mode": schema.StringAttribute{ - Optional: true, + Required: true, Validators: []validator.String{validators.StorageModeValidator{}}, }, }, @@ -361,7 +361,23 @@ func (r *vmResource) Read(ctx context.Context, req resource.ReadRequest, resp *r return } - instance, err := getVM(ctx, r.client, state.ProjectID.ValueString(), state.ID.ValueString()) + // We only have this parsing for transitioning from v1alpha4 to v1alpha5 because old tf state files will not + // have project ID stored. So we will try to get a fallback project to pass to the API. + projectID := "" + if state.ProjectID.ValueString() == "" { + project, err := common.GetFallbackProject(ctx, r.client, &resp.Diagnostics) + if err != nil { + + resp.Diagnostics.AddError("Failed to create disk", + fmt.Sprintf("No project was specified and it was not possible to determine which project to use: %v", err)) + return + } + projectID = project + } else { + projectID = state.ProjectID.ValueString() + } + + instance, err := getVM(ctx, r.client, projectID, state.ID.ValueString()) if err != nil || instance == nil { // instance has most likely been deleted out of band, so we update Terraform state to match resp.State.RemoveResource(ctx) @@ -372,6 +388,7 @@ func (r *vmResource) Read(ctx context.Context, req resource.ReadRequest, resp *r state.ID = types.StringValue(instance.Id) state.Name = types.StringValue(instance.Name) state.Type = types.StringValue(instance.Type_) + state.ProjectID = types.StringValue(instance.ProjectId) networkInterfaces, _ := vmNetworkInterfacesToTerraformResourceModel(instance.NetworkInterfaces) state.NetworkInterfaces = networkInterfaces From 7aeec4640148299d8844048883f6f0c98db73274 Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 5 Dec 2023 16:08:23 -0800 Subject: [PATCH 20/22] updated examples --- examples/project-variable/main.tf | 8 ++++---- examples/vms/main.tf | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/project-variable/main.tf b/examples/project-variable/main.tf index 34e186c..d3e707b 100644 --- a/examples/project-variable/main.tf +++ b/examples/project-variable/main.tf @@ -7,17 +7,17 @@ terraform { } locals { - my_ssh_key = file("~/.ssh/id_rsa.pub") + my_ssh_key = file("~/.ssh/id_ed25519.pub") } variable "project_id" { type = string - default = "d64251a8-3a40-4692-a146-abc536e3922c" + default = "" } // new VM resource "crusoe_compute_instance" "my_vm" { - name = "ajeyaraj-my-new-vm" + name = "my-new-vm" type = "a40.1x" location = "us-northcentral1-a" @@ -54,5 +54,5 @@ resource "crusoe_vpc_firewall_rule" "open_fw_rule" { source_ports = "1-65535" destination = crusoe_compute_instance.my_vm.network_interfaces[0].public_ipv4.address destination_ports = "1-65535" - project_id = var.project_id + project_id = var.project_id } diff --git a/examples/vms/main.tf b/examples/vms/main.tf index 20f947f..04575f5 100644 --- a/examples/vms/main.tf +++ b/examples/vms/main.tf @@ -7,7 +7,7 @@ terraform { } locals { - my_ssh_key = file("~/.ssh/id_rsa.pub") + my_ssh_key = file("~/.ssh/id_ed25519.pub") } // new VM @@ -29,7 +29,6 @@ resource "crusoe_compute_instance" "my_vm" { ] ssh_key = local.my_ssh_key - startup_script = file("startup.sh") } From 1dfdcbd2d3c361200ab5e0e5f2cb74a2f211cfdf Mon Sep 17 00:00:00 2001 From: Aaron Jeyaraj Date: Tue, 5 Dec 2023 17:00:58 -0800 Subject: [PATCH 21/22] bugfix --- internal/vm/util.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/vm/util.go b/internal/vm/util.go index 14db84c..0572c5c 100644 --- a/internal/vm/util.go +++ b/internal/vm/util.go @@ -62,7 +62,11 @@ func getDisksDiff(origDisks, newDisks []vmDiskResourceModel) (disksAdded []swagg } } if !matched { - disksAdded = append(disksAdded, swagger.DiskAttachment{DiskId: newDisk.ID, AttachmentType: newDisk.AttachmentType}) + disksAdded = append(disksAdded, swagger.DiskAttachment{ + DiskId: newDisk.ID, + AttachmentType: newDisk.AttachmentType, + Mode: newDisk.Mode, + }) } } From 97b95e8465216c3c5715afe622773d2114b1b5aa Mon Sep 17 00:00:00 2001 From: andres gutierrez Date: Tue, 5 Dec 2023 17:15:29 -0800 Subject: [PATCH 22/22] update examples --- examples/project-variable/main.tf | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/project-variable/main.tf b/examples/project-variable/main.tf index d3e707b..60668ad 100644 --- a/examples/project-variable/main.tf +++ b/examples/project-variable/main.tf @@ -17,36 +17,36 @@ variable "project_id" { // new VM resource "crusoe_compute_instance" "my_vm" { - name = "my-new-vm" - type = "a40.1x" + name = "my-new-vm" + type = "a40.1x" location = "us-northcentral1-a" disks = [ - // disk attached at startup - { - id = crusoe_storage_disk.data_disk.id - mode = "read-write" - attachment_type = "data" - } - ] - - ssh_key = local.my_ssh_key + // disk attached at startup + { + id = crusoe_storage_disk.data_disk.id + mode = "read-write" + attachment_type = "data" + } + ] + + ssh_key = local.my_ssh_key project_id = var.project_id } resource "crusoe_storage_disk" "data_disk" { - name = "ajeyaraj-data-disk" - size = "200GiB" + name = "data-disk" + size = "200GiB" project_id = var.project_id - location = "us-northcentral1-a" + location = "us-northcentral1-a" } // firewall rule // note: this allows all ingress over TCP to our VM resource "crusoe_vpc_firewall_rule" "open_fw_rule" { network = crusoe_compute_instance.my_vm.network_interfaces[0].network - name = "ajeyaraj-example-terraform-rule" + name = "example-terraform-rule" action = "allow" direction = "ingress" protocols = "tcp"