diff --git a/edgecenter/converter/list.go b/edgecenter/converter/list.go new file mode 100644 index 00000000..b54c3bd3 --- /dev/null +++ b/edgecenter/converter/list.go @@ -0,0 +1,58 @@ +package converter + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" +) + +func ListInterfaceToListInstanceVolumeCreate(volumes []interface{}) ([]edgecloud.InstanceVolumeCreate, error) { + vols := make([]edgecloud.InstanceVolumeCreate, len(volumes)) + for i, volume := range volumes { + vol := volume.(map[string]interface{}) + var V edgecloud.InstanceVolumeCreate + if err := MapStructureDecoder(&V, &vol, decoderConfig); err != nil { + return nil, err + } + vols[i] = V + } + + return vols, nil +} + +func ListInterfaceToListInstanceInterface(interfaces []interface{}) ([]edgecloud.InstanceInterface, error) { + ifs := make([]edgecloud.InstanceInterface, len(interfaces)) + for idx, i := range interfaces { + inter := i.(map[string]interface{}) + var I edgecloud.InstanceInterface + if err := MapStructureDecoder(&I, &inter, decoderConfig); err != nil { + return nil, err + } + + floatingIPList := inter["floating_ip"].(*schema.Set).List() + if len(floatingIPList) > 0 { + fip := floatingIPList[0].(map[string]interface{}) + if fip["source"].(string) == "new" { + I.FloatingIP.Source = edgecloud.NewFloatingIP + } else { + I.FloatingIP = &edgecloud.InterfaceFloatingIP{ + Source: edgecloud.ExistingFloatingIP, + ExistingFloatingID: fip["existing_floating_id"].(string), + } + } + } + + sgList := inter["security_groups"].([]interface{}) + if len(sgList) > 0 { + sgs := make([]edgecloud.ID, 0, len(sgList)) + for _, sg := range sgList { + sgs = append(sgs, edgecloud.ID{ID: sg.(string)}) + } + I.SecurityGroups = sgs + } + + ifs[idx] = I + } + + return ifs, nil +} diff --git a/edgecenter/converter/map.go b/edgecenter/converter/map.go index 39578546..db41627f 100644 --- a/edgecenter/converter/map.go +++ b/edgecenter/converter/map.go @@ -1,6 +1,12 @@ package converter -import "fmt" +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +var decoderConfig = &mapstructure.DecoderConfig{TagName: "json"} // MapInterfaceToMapString converts a map[string]interface{} to map[string]string. func MapInterfaceToMapString(m map[string]interface{}) map[string]string { @@ -12,3 +18,38 @@ func MapInterfaceToMapString(m map[string]interface{}) map[string]string { return mapString } + +// MapStructureDecoder decodes the given map into the provided structure using the specified decoder configuration. +func MapStructureDecoder(strct interface{}, v *map[string]interface{}, config *mapstructure.DecoderConfig) error { + config.Result = strct + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return fmt.Errorf("failed to create decoder: %w", err) + } + + return decoder.Decode(*v) +} + +// MapLeftDiff returns all elements in Left that are not in Right. +func MapLeftDiff(left, right map[string]struct{}) map[string]struct{} { + out := make(map[string]struct{}) + for l := range left { + if _, ok := right[l]; !ok { + out[l] = struct{}{} + } + } + + return out +} + +// MapsIntersection returns all elements in Left that are in Right. +func MapsIntersection(left, right map[string]struct{}) map[string]struct{} { + out := make(map[string]struct{}) + for l := range left { + if _, ok := right[l]; ok { + out[l] = struct{}{} + } + } + + return out +} diff --git a/edgecenter/instance/datasource_instance.go b/edgecenter/instance/datasource_instance.go index b6bcb331..7063136c 100644 --- a/edgecenter/instance/datasource_instance.go +++ b/edgecenter/instance/datasource_instance.go @@ -15,7 +15,7 @@ import ( func DataSourceEdgeCenterInstance() *schema.Resource { return &schema.Resource{ ReadContext: dataSourceEdgeCenterInstanceRead, - Description: `A cloud instance is a virtual machine in a cloud environment. Could be used with baremetal also.`, + Description: `A cloud instance is a virtual machine in a cloud environment`, Schema: map[string]*schema.Schema{ "project_id": { @@ -53,11 +53,6 @@ then the first one will be used. it is recommended to use "id"`, Computed: true, Description: "name of the region", }, - "description": { - Type: schema.TypeString, - Computed: true, - Description: "instance description", - }, "vm_state": { Type: schema.TypeString, Computed: true, @@ -196,7 +191,6 @@ func dataSourceEdgeCenterInstanceRead(ctx context.Context, d *schema.ResourceDat d.Set("status", foundInstance.Status) d.Set("region", foundInstance.Region) - d.Set("description", foundInstance.Description) d.Set("vm_state", foundInstance.VMState) d.Set("keypair_name", foundInstance.KeypairName) diff --git a/edgecenter/instance/instances.go b/edgecenter/instance/instances.go new file mode 100644 index 00000000..c7d61a87 --- /dev/null +++ b/edgecenter/instance/instances.go @@ -0,0 +1,294 @@ +package instance + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" +) + +func instanceSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "uuid of the project", + }, + "region_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "uuid of the region", + }, + "name": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"name_templates"}, + Description: "the instance name", + }, + "name_templates": { + Type: schema.TypeList, + Optional: true, + ConflictsWith: []string{"name"}, + Description: "list of the instance names which will be changed by template: ip_octets, two_ip_octets, one_ip_octet", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "flavor": { + Type: schema.TypeString, + Required: true, + Description: "ID of the flavor, determining its compute and memory, for example 'g1-standard-2-4'.", + }, + "interface": { + Type: schema.TypeList, + Required: true, + Description: "list defining the network interfaces to be attached to the instance", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf( + "available values are '%s', '%s', '%s', '%s'", + edgecloud.SubnetInterfaceType, + edgecloud.AnySubnetInterfaceType, + edgecloud.ExternalInterfaceType, + edgecloud.ReservedFixedIPType, + ), + }, + "security_groups": { + Type: schema.TypeList, + Optional: true, + Description: "list of security group IDs", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "network_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + Description: fmt.Sprintf( + "ID of the network that the subnet belongs to, required if type is '%s' or '%s'", + edgecloud.SubnetInterfaceType, + edgecloud.AnySubnetInterfaceType, + ), + }, + "subnet_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + Description: fmt.Sprintf("required if type is '%s'", edgecloud.SubnetInterfaceType), + }, + "floating_ip": { + Type: schema.TypeSet, + Optional: true, + Description: "floating IP for this subnet attachment", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source": { + Type: schema.TypeString, + Required: true, + }, + "existing_floating_id": { + Type: schema.TypeString, + ValidateFunc: validation.IsUUID, + Optional: true, + }, + }, + }, + }, + "port_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + Description: fmt.Sprintf("required if type is '%s'", edgecloud.ReservedFixedIPType), + }, + }, + }, + }, + "volume": { + Type: schema.TypeList, + Required: true, + Description: "list of volumes for the instances", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source": { + Type: schema.TypeString, + Required: true, + Description: "volume source", + ValidateFunc: validation.StringInSlice( + []string{"new-volume", "existing-volume", "image"}, false, + ), + }, + "size": { + Type: schema.TypeInt, + Required: true, + Description: "size of the volume, specified in gigabytes (GB)", + ValidateFunc: validation.IntAtLeast(1), + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: "name of the volume", + }, + "type_name": { + Type: schema.TypeString, + Optional: true, + Description: "volume type with valid values. defaults to 'ssd_hiiops'", + ValidateFunc: validation.StringInSlice([]string{ + string(edgecloud.VolumeTypeSsdHiIops), + string(edgecloud.VolumeTypeSsdLocal), + string(edgecloud.VolumeTypeUltra), + string(edgecloud.VolumeTypeCold), + string(edgecloud.VolumeTypeStandard), + }, false), + Default: string(edgecloud.VolumeTypeSsdHiIops), + }, + "attachment_tag": { + Type: schema.TypeString, + Optional: true, + Description: "the block device attachment tag (exposed in the metadata)", + }, + "boot_index": { + Type: schema.TypeInt, + Description: `0 for the primary boot device. +unique positive values for other bootable devices. negative - the boot is prohibited`, + Optional: true, + }, + "image_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + Description: "ID of the image. this field is mandatory if creating a volume from an image", + }, + "volume_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + Description: "ID of the volume. this field is mandatory if the volume is a pre-existing volume", + }, + "metadata": { + Type: schema.TypeMap, + Optional: true, + Description: "map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + // computed attributes + "id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "delete_on_termination": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + }, + }, + }, + "metadata": { + Type: schema.TypeMap, + Optional: true, + Description: "map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "keypair_name": { + Type: schema.TypeString, + Optional: true, + Description: "the name of the keypair to inject into new instance(s)", + }, + "server_group_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + Description: "UUID of the anti-affinity or affinity server group (placement groups)", + }, + "security_groups": { + Type: schema.TypeList, + Optional: true, + Description: "list of security group (firewall) UUIDs", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsUUID, + }, + }, + "user_data": { + Type: schema.TypeString, + Optional: true, + Description: "a string in the base64 format. examples of user_data: https://cloudinit.readthedocs.io/en/latest/topics/examples.html", + }, + "username": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"password"}, + Description: "name of a new user on a Linux VM", + }, + "password": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"username"}, + Description: `this parameter is used to set the password either for the 'Admin' user on a Windows VM or +the default user or a new user on a Linux VM`, + }, + "allow_app_ports": { + Type: schema.TypeBool, + Optional: true, + Description: "if true, application ports will be allowed in the security group for the instances created from the marketplace application template", + }, + // computed attributes + "region": { + Type: schema.TypeString, + Computed: true, + Description: "name of the region", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "status of the VM", + }, + "vm_state": { + Type: schema.TypeString, + Computed: true, + Description: "state of the virtual machine", + }, + "addresses": { + Type: schema.TypeList, + Computed: true, + Description: "network addresses associated with the instance", + Elem: &schema.Schema{Type: schema.TypeMap}, + }, + "keypair_id": { + Type: schema.TypeString, + Computed: true, + Description: "uuid of the keypair", + }, + "metadata_detailed": { + Type: schema.TypeList, + Computed: true, + Description: "metadata in detailed format with system info", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + } +} diff --git a/edgecenter/instance/resource_instance.go b/edgecenter/instance/resource_instance.go new file mode 100644 index 00000000..013b074d --- /dev/null +++ b/edgecenter/instance/resource_instance.go @@ -0,0 +1,426 @@ +package instance + +import ( + "context" + "encoding/base64" + "fmt" + "log" + "slices" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/util" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/converter" +) + +func ResourceEdgeCenterInstance() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceEdgeCenterInstanceCreate, + ReadContext: resourceEdgeCenterInstanceRead, + UpdateContext: resourceEdgeCenterInstanceUpdate, + DeleteContext: resourceEdgeCenterInstanceDelete, + Description: `A cloud instance is a virtual machine in a cloud environment`, + Schema: instanceSchema(), + + CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + oldVolumesRaw, newVolumesRaw := diff.GetChange("volume") + oldVolumes, newVolumes := oldVolumesRaw.([]interface{}), newVolumesRaw.([]interface{}) + + newVolumesBootIndexes := getVolumesBootIndexList(newVolumes) + + if !slices.Contains(newVolumesBootIndexes, 0) { + return fmt.Errorf("one of volumes should be with boot_index = 0") + } + + // sequential means 0, 1, 2, 3 but not 0, 2, 3, 1 + if len(newVolumesBootIndexes) > 1 { + for i := 1; i < len(newVolumesBootIndexes); i++ { + if newVolumesBootIndexes[i]-newVolumesBootIndexes[i-1] != 1 { + return fmt.Errorf("volume boot_index order must be sequential") + } + } + } + + // check same volume changed + for _, v := range newVolumes { + volume := v.(map[string]interface{}) + oldVolumeWithSameID := getVolumeInfoByID(volume["id"].(string), oldVolumes) + + if oldVolumeWithSameID != nil { + if oldVolumeWithSameID["size"].(int) > volume["size"].(int) { + return fmt.Errorf("volumes `size` can only be expanded and not shrunk") + } + + if oldVolumeWithSameID["name"].(string) != volume["name"].(string) { + return fmt.Errorf("volume cannot be renamed. create a new one with the name you want") + } + + if oldVolumeWithSameID["type_name"].(string) != volume["type_name"].(string) { + return fmt.Errorf("volume type cannot changed. create a new one with the type you want") + } + } + } + + return nil + }, + } +} + +func resourceEdgeCenterInstanceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + opts := &edgecloud.InstanceCreateRequest{ + Flavor: d.Get("flavor").(string), + KeypairName: d.Get("keypair_name").(string), + ServerGroupID: d.Get("server_group_id").(string), + Username: d.Get("username").(string), + Password: d.Get("password").(string), + AllowAppPorts: d.Get("allow_app_ports").(bool), + } + + if userData, ok := d.GetOk("user_data"); ok { + opts.UserData = base64.StdEncoding.EncodeToString([]byte(userData.(string))) + } + + if v, ok := d.GetOk("name"); ok { + opts.Names = []string{v.(string)} + } else if v, ok := d.GetOk("name_templates"); ok { + nameTemplates := v.([]string) + opts.NameTemplates = nameTemplates + } + + if v, ok := d.GetOk("security_groups"); ok { + securityGroups := v.([]interface{}) + sgsList := make([]edgecloud.ID, 0, len(securityGroups)) + for _, sg := range securityGroups { + sgsList = append(sgsList, edgecloud.ID{ID: sg.(string)}) + } + opts.SecurityGroups = sgsList + } + + volumes := d.Get("volume").([]interface{}) + instanceVolumeCreateList, err := converter.ListInterfaceToListInstanceVolumeCreate(volumes) + if err != nil { + return diag.Errorf("error creating volume config: %s", err) + } + opts.Volumes = instanceVolumeCreateList + + if v, ok := d.GetOk("metadata"); ok { + metadata := converter.MapInterfaceToMapString(v.(map[string]interface{})) + opts.Metadata = metadata + } + + ifs := d.Get("interface").([]interface{}) + interfaceInstanceCreateOptsList, err := converter.ListInterfaceToListInstanceInterface(ifs) + if err != nil { + return diag.Errorf("error creating interface config: %s", err) + } + + if v, ok := d.GetOk("security_groups"); ok { + securityGroups := v.([]interface{}) + sgsList := make([]edgecloud.ID, 0, len(securityGroups)) + for _, sg := range securityGroups { + sgsList = append(sgsList, edgecloud.ID{ID: sg.(string)}) + } + opts.SecurityGroups = sgsList + } + + opts.Interfaces = interfaceInstanceCreateOptsList + + log.Printf("[DEBUG] Instance create configuration: %#v", opts) + + taskResult, err := util.ExecuteAndExtractTaskResult(ctx, client.Instances.Create, opts, client) + if err != nil { + return diag.Errorf("error creating volume: %s", err) + } + + d.SetId(taskResult.Instances[0]) + + log.Printf("[INFO] Instance: %s", d.Id()) + + return resourceEdgeCenterInstanceRead(ctx, d, meta) +} + +func resourceEdgeCenterInstanceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + // Retrieve the volume properties for updating the state + foundInstance, resp, err := client.Instances.Get(ctx, d.Id()) + if err != nil { + // check if instance no longer exists. + if resp != nil && resp.StatusCode == 404 { + log.Printf("[WARN] EdgeCenter Instance (%s) not found", d.Id()) + d.SetId("") + return nil + } + + return diag.Errorf("Error retrieving instance: %s", err) + } + + d.Set("status", foundInstance.Status) + d.Set("region", foundInstance.Region) + d.Set("vm_state", foundInstance.VMState) + d.Set("keypair_id", foundInstance.KeypairName) + + // desc sorting: first volume is the last added + volumes, _, err := client.Volumes.List(ctx, &edgecloud.VolumeListOptions{InstanceID: d.Id()}) + if err != nil { + return diag.Errorf("Error retrieving instance volumes: %s", err) + } + + getID := func(name string, volumeList []edgecloud.Volume) string { + for _, volume := range volumeList { + if volume.Name == name { + return volume.ID + } + } + + return "" + } + + // asc sorting: first volume is the first added + currentVolumes := d.Get("volume").([]interface{}) + for i, v := range currentVolumes { + volume := v.(map[string]interface{}) + volumeID := getID(volume["name"].(string), volumes) + if volumeID == "" { + return diag.Errorf("Error during get volume id") + } + currentVolumes[i].(map[string]interface{})["id"] = volumeID + } + + if err := d.Set("volume", currentVolumes); err != nil { + return diag.FromErr(err) + } + + if len(foundInstance.MetadataDetailed) > 0 { + metadata := make([]map[string]interface{}, 0, len(foundInstance.MetadataDetailed)) + for _, metadataItem := range foundInstance.MetadataDetailed { + metadata = append(metadata, map[string]interface{}{ + "key": metadataItem.Key, + "value": metadataItem.Value, + "read_only": metadataItem.ReadOnly, + }) + } + d.Set("metadata_detailed", metadata) + } + + addresses := make([]map[string]string, 0, len(foundInstance.Addresses)) + for networkName, networkInfo := range foundInstance.Addresses { + net := networkInfo[0] + address := map[string]string{ + "network_name": networkName, + "type": net.Type, + "addr": net.Address.String(), + "subnet_id": net.SubnetID, + "subnet_name": net.SubnetName, + } + addresses = append(addresses, address) + } + if err := d.Set("addresses", addresses); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceEdgeCenterInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { //nolint: gocognit + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + if d.HasChange("name") { + newName := d.Get("name").(string) + if _, _, err := client.Instances.Rename(ctx, d.Id(), &edgecloud.Name{Name: newName}); err != nil { + return diag.Errorf("Error when renaming the instance: %s", err) + } + } + + if d.HasChange("flavor") { + newFlavor := d.Get("flavor").(string) + instanceFlavorUpdateRequest := &edgecloud.InstanceFlavorUpdateRequest{FlavorID: newFlavor} + task, _, err := client.Instances.UpdateFlavor(ctx, d.Id(), instanceFlavorUpdateRequest) + if err != nil { + return diag.Errorf("Error when changing the instance flavor: %s", err) + } + + if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { + return diag.Errorf("Error while waiting for flavor change: %s", err) + } + } + + if d.HasChange("metadata") { + metadata := edgecloud.Metadata(converter.MapInterfaceToMapString(d.Get("metadata").(map[string]interface{}))) + + if _, err := client.Instances.MetadataUpdate(ctx, d.Id(), &metadata); err != nil { + return diag.Errorf("cannot update metadata. Error: %s", err) + } + } + + if d.HasChange("server_group_id") { + oldSgRaw, newSgRaw := d.GetChange("server_group_id") + oldSg, newSg := oldSgRaw.(string), newSgRaw.(string) + + // delete old server group + if oldSg != "" { + task, _, err := client.Instances.RemoveFromServerGroup(ctx, d.Id()) + if err != nil { + return diag.Errorf("Error when remove the instance from server group: %s", err) + } + + if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { + return diag.Errorf("Error while waiting for instance remove from server group: %s", err) + } + } + + // add new server group if needed + if newSg != "" { + instancePutIntoServerGroupRequest := &edgecloud.InstancePutIntoServerGroupRequest{ServerGroupID: newSg} + task, _, err := client.Instances.PutIntoServerGroup(ctx, d.Id(), instancePutIntoServerGroupRequest) + if err != nil { + return diag.Errorf("Error when put the instance to new server group: %s", err) + } + + if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { + return diag.Errorf("Error while waiting for instance put to new server group: %s", err) + } + } + } + + if d.HasChange("volume") { + oldVolumesRaw, newVolumesRaw := d.GetChange("volume") + oldVolumes, newVolumes := oldVolumesRaw.([]interface{}), newVolumesRaw.([]interface{}) + + oldIDs := getVolumeIDsSet(oldVolumes) + newIDs := getVolumeIDsSet(newVolumes) + + // detach volumes + for volumeID := range converter.MapLeftDiff(oldIDs, newIDs) { + volume := getVolumeInfoByID(volumeID, oldVolumes) + if volume["boot_index"].(int) == 0 { + return diag.Errorf("cannot detach primary boot device with boot_index=0. id: %s", volumeID) + } + + volumeDetachRequest := &edgecloud.VolumeDetachRequest{InstanceID: d.Id()} + if _, _, err := client.Volumes.Detach(ctx, volumeID, volumeDetachRequest); err != nil { + return diag.Errorf("Error while detaching the volume: %s", err) + } + } + + // attach volumes + for volumeID := range converter.MapLeftDiff(newIDs, oldIDs) { + volume := getVolumeInfoByID(volumeID, newVolumes) + attachmentTag := volume["attachment_tag"].(string) + + switch volume["source"].(string) { + case "image": + return diag.Errorf("cannot attach image-source volume, required 'existing-volume' or 'new-volume' source") + case "existing-volume": + volumeAttachRequest := &edgecloud.VolumeAttachRequest{ + InstanceID: d.Id(), + AttachmentTag: attachmentTag, + } + if _, _, err := client.Volumes.Attach(ctx, volume["volume_id"].(string), volumeAttachRequest); err != nil { + return diag.Errorf("cannot attach existing-volume: %s. Error: %s", volumeID, err) + } + case "new-volume": + volumeCreateRequest := &edgecloud.VolumeCreateRequest{ + AttachmentTag: attachmentTag, + Source: "new-volume", + InstanceIDToAttachTo: d.Id(), + Name: volume["name"].(string), + Size: volume["size"].(int), + TypeName: edgecloud.VolumeType(volume["type_name"].(string)), + } + task, _, err := client.Volumes.Create(ctx, volumeCreateRequest) + if err != nil { + return diag.Errorf("Error when creating a new instance volume: %s", err) + } + if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { + return diag.Errorf("Error while waiting for instance volume create: %s", err) + } + } + } + + // resize the same volume + for volumeID := range converter.MapsIntersection(newIDs, oldIDs) { + volumeOld := getVolumeInfoByID(volumeID, oldVolumes) + volumeNew := getVolumeInfoByID(volumeID, newVolumes) + + if volumeOld["size"].(int) != volumeNew["size"].(int) { + volumeExtendSizeRequest := &edgecloud.VolumeExtendSizeRequest{Size: volumeNew["size"].(int)} + task, _, err := client.Volumes.Extend(ctx, volumeID, volumeExtendSizeRequest) + if err != nil { + return diag.FromErr(err) + } + if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { + return diag.FromErr(err) + } + } + } + } + + return resourceEdgeCenterInstanceRead(ctx, d, meta) +} + +func resourceEdgeCenterInstanceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).EdgeCloudClient() + client.Region = d.Get("region_id").(int) + client.Project = d.Get("project_id").(int) + + log.Printf("[INFO] Deleting instance: %s", d.Id()) + task, _, err := client.Instances.Delete(ctx, d.Id(), nil) + if err != nil { + return diag.Errorf("Error deleting instance: %s", err) + } + + if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { + return diag.Errorf("Delete instance task failed with error: %s", err) + } + + if err = util.ResourceIsDeleted(ctx, client.Instances.Get, d.Id()); err != nil { + return diag.Errorf("Instance with id %s was not deleted: %s", d.Id(), err) + } + + d.SetId("") + + return nil +} + +func getVolumeIDsSet(volumes []interface{}) map[string]struct{} { + ids := make(map[string]struct{}, len(volumes)) + for _, volumeRaw := range volumes { + volume := volumeRaw.(map[string]interface{}) + ids[volume["id"].(string)] = struct{}{} + } + + return ids +} + +func getVolumeInfoByID(id string, volumeList []interface{}) map[string]interface{} { + for _, volumeRaw := range volumeList { + volume := volumeRaw.(map[string]interface{}) + if volume["id"].(string) == id { + return volume + } + } + + return nil +} + +func getVolumesBootIndexList(volumes []interface{}) []int { + idxList := make([]int, 0, len(volumes)) + for _, volumeRaw := range volumes { + volume := volumeRaw.(map[string]interface{}) + idxList = append(idxList, volume["boot_index"].(int)) + } + + return idxList +} diff --git a/edgecenter/provider.go b/edgecenter/provider.go index ffa6f96a..cddf1102 100644 --- a/edgecenter/provider.go +++ b/edgecenter/provider.go @@ -37,6 +37,7 @@ func Provider() *schema.Provider { }, ResourcesMap: map[string]*schema.Resource{ "edgecenter_floatingip": floatingip.ResourceEdgeCenterFloatingIP(), + "edgecenter_instance": instance.ResourceEdgeCenterInstance(), "edgecenter_volume": volume.ResourceEdgeCenterVolume(), }, }