diff --git a/ibm/provider/provider.go b/ibm/provider/provider.go index 93e4eb2a65..d9ad3bbde7 100644 --- a/ibm/provider/provider.go +++ b/ibm/provider/provider.go @@ -1300,6 +1300,7 @@ func Provider() *schema.Provider { "ibm_is_floating_ip": vpc.ResourceIBMISFloatingIP(), "ibm_is_flow_log": vpc.ResourceIBMISFlowLog(), "ibm_is_instance": vpc.ResourceIBMISInstance(), + "ibm_is_instance_boot_volume_manager": vpc.ResourceIBMISInstanceBootVolumeManager(), "ibm_is_instance_action": vpc.ResourceIBMISInstanceAction(), "ibm_is_instance_network_attachment": vpc.ResourceIBMIsInstanceNetworkAttachment(), "ibm_is_instance_network_interface": vpc.ResourceIBMIsInstanceNetworkInterface(), @@ -2031,6 +2032,7 @@ func Validator() validate.ValidatorDict { "ibm_is_image_export_job": vpc.ResourceIBMIsImageExportValidator(), "ibm_is_instance_template": vpc.ResourceIBMISInstanceTemplateValidator(), "ibm_is_instance": vpc.ResourceIBMISInstanceValidator(), + "ibm_is_instance_boot_volume_manager": vpc.ResourceIBMISInstanceBootVolumeManagerValidator(), "ibm_is_instance_action": vpc.ResourceIBMISInstanceActionValidator(), "ibm_is_instance_network_attachment": vpc.ResourceIBMIsInstanceNetworkAttachmentValidator(), "ibm_is_instance_network_interface": vpc.ResourceIBMIsInstanceNetworkInterfaceValidator(), diff --git a/ibm/service/vpc/resource_ibm_is_instance_boot_volume_manager.go b/ibm/service/vpc/resource_ibm_is_instance_boot_volume_manager.go new file mode 100644 index 0000000000..2fb88d11e7 --- /dev/null +++ b/ibm/service/vpc/resource_ibm_is_instance_boot_volume_manager.go @@ -0,0 +1,1035 @@ +// Copyright IBM Corp. 2017, 2025 All Rights Reserved. +// Licensed under the Mozilla Public License v2.0 + +package vpc + +import ( + "context" + "fmt" + "log" + "os" + "strings" + "time" + + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/flex" + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/validate" + "github.com/IBM/go-sdk-core/v5/core" + "github.com/IBM/vpc-go-sdk/vpcv1" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + isInstanceBootVolumeIdentifier = "boot_volume" + isInstanceBootVolumeManagerDelete = "delete_volume" +) + +func ResourceIBMISInstanceBootVolumeManager() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceIBMISInstanceBootVolumeManagerCreate, + ReadContext: resourceIBMISInstanceBootVolumeManagerRead, + UpdateContext: resourceIBMISInstanceBootVolumeManagerUpdate, + DeleteContext: resourceIBMISInstanceBootVolumeManagerDelete, + Exists: resourceIBMISInstanceBootVolumeManagerExists, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + return []*schema.ResourceData{d}, nil + }, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + CustomizeDiff: customdiff.All( + customdiff.Sequence( + func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + return flex.ResourceTagsCustomizeDiff(diff) + }, + ), + customdiff.Sequence( + func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + return flex.ResourceValidateAccessTags(diff, v) + }), + ), + + Schema: map[string]*schema.Schema{ + isInstanceBootVolumeIdentifier: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The unique identifier for the boot volume", + }, + isVolumeName: { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validate.InvokeValidator("ibm_is_instance_boot_volume_manager", isVolumeName), + Description: "The user-defined name for this boot volume", + }, + isVolumeProfileName: { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validate.InvokeValidator("ibm_is_instance_boot_volume_manager", isVolumeProfileName), + Description: "The globally unique name of the volume profile to use for this volume", + }, + isVolumeCapacity: { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validate.InvokeValidator("ibm_is_instance_boot_volume_manager", isVolumeCapacity), + Description: "The capacity of the volume in gigabytes", + }, + isVolumeIops: { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validate.InvokeValidator("ibm_is_instance_boot_volume_manager", isVolumeIops), + Description: "The maximum I/O operations per second (IOPS) for the volume", + }, + isInstanceBootVolumeManagerDelete: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "If set to true, the boot volume will be deleted when this resource is destroyed", + }, + isVolumeTags: { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString, ValidateFunc: validate.InvokeValidator("ibm_is_instance_boot_volume_manager", "tags")}, + Set: flex.ResourceIBMVPCHash, + Description: "User tags for the boot volume", + }, + isVolumeAccessTags: { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString, ValidateFunc: validate.InvokeValidator("ibm_is_instance_boot_volume_manager", "accesstag")}, + Set: flex.ResourceIBMVPCHash, + Description: "Access management tags for the boot volume", + }, + isVolumeDeleteAllSnapshots: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "If set to true, all snapshots created from this volume will be deleted when the volume is deleted", + }, + + // Computed attributes + isVolumeZone: { + Type: schema.TypeString, + Computed: true, + Description: "The zone where this volume resides", + }, + isVolumeEncryptionKey: { + Type: schema.TypeString, + Computed: true, + Description: "The CRN of the encryption key used to encrypt this volume", + }, + isVolumeEncryptionType: { + Type: schema.TypeString, + Computed: true, + Description: "The type of encryption used on the volume", + }, + isVolumeSourceSnapshot: { + Type: schema.TypeString, + Computed: true, + Description: "The unique identifier for the snapshot from which this volume was created", + }, + isVolumeResourceGroup: { + Type: schema.TypeList, + Computed: true, + Description: "The resource group for this volume", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "href": { + Type: schema.TypeString, + Computed: true, + Description: "The URL for this resource group", + }, + "id": { + Type: schema.TypeString, + Computed: true, + Description: "The unique identifier for this resource group", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The user-defined name for this resource group", + }, + }, + }, + }, + isVolumeCrn: { + Type: schema.TypeString, + Computed: true, + Description: "The CRN of the volume", + }, + isVolumeStatus: { + Type: schema.TypeString, + Computed: true, + Description: "The status of the volume", + }, + isVolumeHealthState: { + Type: schema.TypeString, + Computed: true, + Description: "The health state of this volume", + }, + isVolumeHealthReasons: { + Type: schema.TypeList, + Computed: true, + Description: "The reasons for the current health_state (if any)", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + isVolumeHealthReasonsCode: { + Type: schema.TypeString, + Computed: true, + Description: "A snake case string succinctly identifying the reason for this health state", + }, + isVolumeHealthReasonsMessage: { + Type: schema.TypeString, + Computed: true, + Description: "An explanation of the reason for this health state", + }, + isVolumeHealthReasonsMoreInfo: { + Type: schema.TypeString, + Computed: true, + Description: "Link to documentation about the reason for this health state", + }, + }, + }, + }, + isVolumeStatusReasons: { + Type: schema.TypeList, + Computed: true, + Description: "The reasons for the current status (if any)", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + isVolumeStatusReasonsCode: { + Type: schema.TypeString, + Computed: true, + Description: "A snake case string succinctly identifying the status reason", + }, + isVolumeStatusReasonsMessage: { + Type: schema.TypeString, + Computed: true, + Description: "An explanation of the status reason", + }, + isVolumeStatusReasonsMoreInfo: { + Type: schema.TypeString, + Computed: true, + Description: "Link to documentation about this status reason", + }, + }, + }, + }, + isVolumeBandwidth: { + Type: schema.TypeInt, + Computed: true, + Description: "The maximum bandwidth (in megabits per second) for the volume", + }, + "volume_attachments": { + Type: schema.TypeList, + Computed: true, + Description: "The volume attachments for this volume", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delete_volume_on_instance_delete": { + Type: schema.TypeBool, + Computed: true, + Description: "If set to true, this volume will be deleted when the instance is deleted", + }, + "device": { + Type: schema.TypeList, + Computed: true, + Description: "A unique identifier for the device which is exposed to the instance operating system", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "A unique identifier for the device which is exposed to the instance operating system", + }, + }, + }, + }, + "href": { + Type: schema.TypeString, + Computed: true, + Description: "The URL for this volume attachment", + }, + "id": { + Type: schema.TypeString, + Computed: true, + Description: "The unique identifier for this volume attachment", + }, + "instance": { + Type: schema.TypeList, + Computed: true, + Description: "The attached instance", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "crn": { + Type: schema.TypeString, + Computed: true, + Description: "The CRN for this virtual server instance", + }, + "href": { + Type: schema.TypeString, + Computed: true, + Description: "The URL for this virtual server instance", + }, + "id": { + Type: schema.TypeString, + Computed: true, + Description: "The unique identifier for this virtual server instance", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The user-defined name for this virtual server instance", + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The user-defined name for this volume attachment", + }, + "type": { + Type: schema.TypeString, + Computed: true, + Description: "The type of volume attachment", + }, + }, + }, + }, + flex.ResourceControllerURL: { + Type: schema.TypeString, + Computed: true, + Description: "The URL of the IBM Cloud dashboard that can be used to explore and view details about this instance", + }, + flex.ResourceName: { + Type: schema.TypeString, + Computed: true, + Description: "The name of the resource", + }, + flex.ResourceCRN: { + Type: schema.TypeString, + Computed: true, + Description: "The CRN of the resource", + }, + flex.ResourceStatus: { + Type: schema.TypeString, + Computed: true, + Description: "The status of the resource", + }, + flex.ResourceGroupName: { + Type: schema.TypeString, + Computed: true, + Description: "The resource group name in which resource is provisioned", + }, + }, + } +} + +func ResourceIBMISInstanceBootVolumeManagerValidator() *validate.ResourceValidator { + validateSchema := make([]validate.ValidateSchema, 0) + + validateSchema = append(validateSchema, + validate.ValidateSchema{ + Identifier: isVolumeName, + ValidateFunctionIdentifier: validate.ValidateRegexpLen, + Type: validate.TypeString, + Optional: true, + Regexp: `^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$`, + MinValueLength: 1, + MaxValueLength: 63}) + validateSchema = append(validateSchema, + validate.ValidateSchema{ + Identifier: isInstanceBootVolumeIdentifier, + ValidateFunctionIdentifier: validate.ValidateNoZeroValues, + Type: validate.TypeString}) + validateSchema = append(validateSchema, + validate.ValidateSchema{ + Identifier: "tags", + ValidateFunctionIdentifier: validate.ValidateRegexpLen, + Type: validate.TypeString, + Optional: true, + Regexp: `^[A-Za-z0-9:_ .-]+$`, + MinValueLength: 1, + MaxValueLength: 128}) + validateSchema = append(validateSchema, + validate.ValidateSchema{ + Identifier: isVolumeProfileName, + ValidateFunctionIdentifier: validate.ValidateAllowedStringValue, + Type: validate.TypeString, + Optional: true, + AllowedValues: "general-purpose, 5iops-tier, 10iops-tier, custom", + }) + validateSchema = append(validateSchema, + validate.ValidateSchema{ + Identifier: isVolumeCapacity, + ValidateFunctionIdentifier: validate.IntBetween, + Type: validate.TypeInt, + MinValue: "10", + MaxValue: "16000"}) + validateSchema = append(validateSchema, + validate.ValidateSchema{ + Identifier: isVolumeIops, + ValidateFunctionIdentifier: validate.IntBetween, + Type: validate.TypeInt, + MinValue: "100", + MaxValue: "48000"}) + validateSchema = append(validateSchema, + validate.ValidateSchema{ + Identifier: "accesstag", + ValidateFunctionIdentifier: validate.ValidateRegexpLen, + Type: validate.TypeString, + Optional: true, + Regexp: `^([A-Za-z0-9_.-]|[A-Za-z0-9_.-][A-Za-z0-9_ .-]*[A-Za-z0-9_.-]):([A-Za-z0-9_.-]|[A-Za-z0-9_.-][A-Za-z0-9_ .-]*[A-Za-z0-9_.-])$`, + MinValueLength: 1, + MaxValueLength: 128}) + + ibmISInstanceBootVolumeManagerResourceValidator := validate.ResourceValidator{ResourceName: "ibm_is_instance_boot_volume_manager", Schema: validateSchema} + return &ibmISInstanceBootVolumeManagerResourceValidator +} + +func resourceIBMISInstanceBootVolumeManagerCreate(context context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + sess, err := vpcClient(meta) + if err != nil { + tfErr := flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "create", "initialize-client") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + bootVolumeID := d.Get(isInstanceBootVolumeIdentifier).(string) + + // Verify the boot volume exists + getVolumeOptions := &vpcv1.GetVolumeOptions{ + ID: &bootVolumeID, + } + volume, _, err := sess.GetVolumeWithContext(context, getVolumeOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("GetVolumeWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "create") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + d.SetId(bootVolumeID) + + // Handle tags on creation + v := os.Getenv("IC_ENV_TAGS") + if _, ok := d.GetOk(isVolumeTags); ok || v != "" { + _, newList := d.GetChange(isVolumeTags) + userTagsArray := make([]string, 0) + if newList != nil { + for _, tag := range newList.(*schema.Set).List() { + userTagsArray = append(userTagsArray, tag.(string)) + } + } + if v != "" { + envTags := strings.Split(v, ",") + userTagsArray = append(userTagsArray, envTags...) + } + if len(userTagsArray) > 0 { + volumePatchModel := &vpcv1.VolumePatch{ + UserTags: userTagsArray, + } + volumePatch, err := volumePatchModel.AsPatch() + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("volumePatchModel.AsPatch() failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "create") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + updateVolumeOptions := &vpcv1.UpdateVolumeOptions{ + ID: &bootVolumeID, + VolumePatch: volumePatch, + } + _, _, err = sess.UpdateVolumeWithContext(context, updateVolumeOptions) + if err != nil { + log.Printf("Error on create of boot volume manager (%s) tags: %s", d.Id(), err) + } + } + } + + if _, ok := d.GetOk(isVolumeAccessTags); ok { + oldList, newList := d.GetChange(isVolumeAccessTags) + err = flex.UpdateGlobalTagsUsingCRN(oldList, newList, meta, *volume.CRN, "", isVolumeAccessTagType) + if err != nil { + log.Printf("Error on create of boot volume manager (%s) access tags: %s", d.Id(), err) + } + } + + return resourceIBMISInstanceBootVolumeManagerUpdate(context, d, meta) +} + +func resourceIBMISInstanceBootVolumeManagerRead(context context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + sess, err := vpcClient(meta) + if err != nil { + tfErr := flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "initialize-client") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + id := d.Id() + getVolumeOptions := &vpcv1.GetVolumeOptions{ + ID: &id, + } + volume, response, err := sess.GetVolumeWithContext(context, getVolumeOptions) + if err != nil { + if response != nil && response.StatusCode == 404 { + d.SetId("") + return nil + } + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("GetVolumeWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "read") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + if err = d.Set(isInstanceBootVolumeId, id); err != nil { + err = fmt.Errorf("Error setting boot_volume: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-boot_volume").GetDiag() + } + + if !core.IsNil(volume.Name) { + if err = d.Set(isVolumeName, *volume.Name); err != nil { + err = fmt.Errorf("Error setting name: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-name").GetDiag() + } + } + + if !core.IsNil(volume.Profile) { + if err = d.Set(isVolumeProfileName, *volume.Profile.Name); err != nil { + err = fmt.Errorf("Error setting profile: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-profile").GetDiag() + } + } + + if !core.IsNil(volume.Zone) { + if err = d.Set(isVolumeZone, *volume.Zone.Name); err != nil { + err = fmt.Errorf("Error setting zone: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-zone").GetDiag() + } + } + + if volume.EncryptionKey != nil { + if err = d.Set(isVolumeEncryptionKey, volume.EncryptionKey.CRN); err != nil { + err = fmt.Errorf("Error setting encryption_key: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-encryption_key").GetDiag() + } + } + + if volume.Encryption != nil { + if err = d.Set(isVolumeEncryptionType, *volume.Encryption); err != nil { + err = fmt.Errorf("Error setting encryption_type: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-encryption_type").GetDiag() + } + } + + if !core.IsNil(volume.Capacity) { + if err = d.Set(isVolumeCapacity, flex.IntValue(volume.Capacity)); err != nil { + err = fmt.Errorf("Error setting capacity: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-capacity").GetDiag() + } + } + + if !core.IsNil(volume.Iops) { + if err = d.Set(isVolumeIops, flex.IntValue(volume.Iops)); err != nil { + err = fmt.Errorf("Error setting iops: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-iops").GetDiag() + } + } + + if !core.IsNil(volume.CRN) { + if err = d.Set(isVolumeCrn, *volume.CRN); err != nil { + err = fmt.Errorf("Error setting crn: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-crn").GetDiag() + } + } + + if volume.SourceSnapshot != nil { + if err = d.Set(isVolumeSourceSnapshot, *volume.SourceSnapshot.ID); err != nil { + err = fmt.Errorf("Error setting source_snapshot: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-source_snapshot").GetDiag() + } + } + + if !core.IsNil(volume.Status) { + if err = d.Set(isVolumeStatus, *volume.Status); err != nil { + err = fmt.Errorf("Error setting status: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-status").GetDiag() + } + } + + if volume.HealthState != nil { + if err = d.Set(isVolumeHealthState, *volume.HealthState); err != nil { + err = fmt.Errorf("Error setting health_state: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-health_state").GetDiag() + } + } + + if !core.IsNil(volume.Bandwidth) { + if err = d.Set(isVolumeBandwidth, flex.IntValue(volume.Bandwidth)); err != nil { + err = fmt.Errorf("Error setting bandwidth: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-bandwidth").GetDiag() + } + } + + // Handle resource group + resourceGroupList := []map[string]interface{}{} + if volume.ResourceGroup != nil { + resourceGroupMap := map[string]interface{}{ + "href": *volume.ResourceGroup.Href, + "id": *volume.ResourceGroup.ID, + "name": *volume.ResourceGroup.Name, + } + resourceGroupList = append(resourceGroupList, resourceGroupMap) + if err = d.Set(flex.ResourceGroupName, *volume.ResourceGroup.Name); err != nil { + err = fmt.Errorf("Error setting resource_group_name: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-resource_group_name").GetDiag() + } + } + if err = d.Set(isVolumeResourceGroup, resourceGroupList); err != nil { + err = fmt.Errorf("Error setting resource_group: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-resource_group").GetDiag() + } + + // Handle status reasons + if volume.StatusReasons != nil { + statusReasonsList := make([]map[string]interface{}, 0) + for _, sr := range volume.StatusReasons { + currentSR := map[string]interface{}{} + if sr.Code != nil && sr.Message != nil { + currentSR[isVolumeStatusReasonsCode] = *sr.Code + currentSR[isVolumeStatusReasonsMessage] = *sr.Message + if sr.MoreInfo != nil { + currentSR[isVolumeStatusReasonsMoreInfo] = *sr.MoreInfo + } + statusReasonsList = append(statusReasonsList, currentSR) + } + } + if err = d.Set(isVolumeStatusReasons, statusReasonsList); err != nil { + err = fmt.Errorf("Error setting status_reasons: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-status_reasons").GetDiag() + } + } + + // Handle health reasons + if volume.HealthReasons != nil { + healthReasonsList := make([]map[string]interface{}, 0) + for _, hr := range volume.HealthReasons { + currentHR := map[string]interface{}{} + if hr.Code != nil && hr.Message != nil { + currentHR[isVolumeHealthReasonsCode] = *hr.Code + currentHR[isVolumeHealthReasonsMessage] = *hr.Message + if hr.MoreInfo != nil { + currentHR[isVolumeHealthReasonsMoreInfo] = *hr.MoreInfo + } + healthReasonsList = append(healthReasonsList, currentHR) + } + } + if err = d.Set(isVolumeHealthReasons, healthReasonsList); err != nil { + err = fmt.Errorf("Error setting health_reasons: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-health_reasons").GetDiag() + } + } + + // Handle volume attachments + if volume.VolumeAttachments != nil { + volumeAttachmentsList := make([]map[string]interface{}, 0) + for _, va := range volume.VolumeAttachments { + volumeAttachmentMap := map[string]interface{}{} + if va.DeleteVolumeOnInstanceDelete != nil { + volumeAttachmentMap["delete_volume_on_instance_delete"] = *va.DeleteVolumeOnInstanceDelete + } + if va.Device != nil { + deviceList := []map[string]interface{}{} + deviceMap := map[string]interface{}{ + "id": *va.Device.ID, + } + deviceList = append(deviceList, deviceMap) + volumeAttachmentMap["device"] = deviceList + } + if va.Href != nil { + volumeAttachmentMap["href"] = *va.Href + } + if va.ID != nil { + volumeAttachmentMap["id"] = *va.ID + } + if va.Instance != nil { + instanceList := []map[string]interface{}{} + instanceMap := map[string]interface{}{} + if va.Instance.CRN != nil { + instanceMap["crn"] = *va.Instance.CRN + } + if va.Instance.Href != nil { + instanceMap["href"] = *va.Instance.Href + } + if va.Instance.ID != nil { + instanceMap["id"] = *va.Instance.ID + } + if va.Instance.Name != nil { + instanceMap["name"] = *va.Instance.Name + } + instanceList = append(instanceList, instanceMap) + volumeAttachmentMap["instance"] = instanceList + } + if va.Name != nil { + volumeAttachmentMap["name"] = *va.Name + } + if va.Type != nil { + volumeAttachmentMap["type"] = *va.Type + } + volumeAttachmentsList = append(volumeAttachmentsList, volumeAttachmentMap) + } + if err = d.Set("volume_attachments", volumeAttachmentsList); err != nil { + err = fmt.Errorf("Error setting volume_attachments: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-volume_attachments").GetDiag() + } + } + + // Handle tags + if volume.UserTags != nil { + if err = d.Set(isVolumeTags, volume.UserTags); err != nil { + err = fmt.Errorf("Error setting tags: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-tags").GetDiag() + } + } + + accesstags, err := flex.GetGlobalTagsUsingCRN(meta, *volume.CRN, "", isVolumeAccessTagType) + if err != nil { + log.Printf("Error on get of boot volume manager (%s) access tags: %s", d.Id(), err) + } + if err = d.Set(isVolumeAccessTags, accesstags); err != nil { + err = fmt.Errorf("Error setting access_tags: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-access_tags").GetDiag() + } + + controller, err := flex.GetBaseController(meta) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("GetBaseController failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "read") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + if err = d.Set(flex.ResourceControllerURL, controller+"/vpc-ext/storage/storageVolumes"); err != nil { + err = fmt.Errorf("Error setting resource_controller_url: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-resource_controller_url").GetDiag() + } + if err = d.Set(flex.ResourceName, *volume.Name); err != nil { + err = fmt.Errorf("Error setting resource_name: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-resource_name").GetDiag() + } + if err = d.Set(flex.ResourceCRN, *volume.CRN); err != nil { + err = fmt.Errorf("Error setting resource_crn: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-resource_crn").GetDiag() + } + if err = d.Set(flex.ResourceStatus, *volume.Status); err != nil { + err = fmt.Errorf("Error setting resource_status: %s", err) + return flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "read", "set-resource_status").GetDiag() + } + + return nil +} + +func resourceIBMISInstanceBootVolumeManagerUpdate(context context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + sess, err := vpcClient(meta) + if err != nil { + tfErr := flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "update", "initialize-client") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + id := d.Id() + hasChanged := false + + // Handle volume property updates with instance state management + if d.HasChange(isVolumeProfileName) || d.HasChange(isVolumeIops) || d.HasChange(isVolumeCapacity) { + // Get volume details to check attachments + getVolumeOptions := &vpcv1.GetVolumeOptions{ + ID: &id, + } + volume, response, err := sess.GetVolumeWithContext(context, getVolumeOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("GetVolumeWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + // Check if volume is attached to an instance + if volume.VolumeAttachments != nil && len(volume.VolumeAttachments) > 0 { + instanceID := *volume.VolumeAttachments[0].Instance.ID + + // Get instance status + getInstanceOptions := &vpcv1.GetInstanceOptions{ + ID: &instanceID, + } + instance, _, err := sess.GetInstanceWithContext(context, getInstanceOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("GetInstanceWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + // Ensure instance is running for volume updates + if instance != nil && *instance.Status != "running" { + actionType := "start" + createInstanceActionOptions := &vpcv1.CreateInstanceActionOptions{ + InstanceID: &instanceID, + Type: &actionType, + } + _, _, err = sess.CreateInstanceActionWithContext(context, createInstanceActionOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("CreateInstanceActionWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + _, err = isWaitForInstanceAvailable(sess, instanceID, d.Timeout(schema.TimeoutUpdate), d) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("isWaitForInstanceAvailable failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + } + } + + // Get fresh ETag for the update + getVolumeOptions = &vpcv1.GetVolumeOptions{ + ID: &id, + } + _, response, err = sess.GetVolumeWithContext(context, getVolumeOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("GetVolumeWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + eTag := response.Headers.Get("ETag") + + volumePatchModel := &vpcv1.VolumePatch{} + + if d.HasChange(isVolumeProfileName) { + profile := d.Get(isVolumeProfileName).(string) + volumePatchModel.Profile = &vpcv1.VolumeProfileIdentity{ + Name: &profile, + } + } + + if d.HasChange(isVolumeIops) { + iops := int64(d.Get(isVolumeIops).(int)) + volumePatchModel.Iops = &iops + } + + if d.HasChange(isVolumeCapacity) { + capacity := int64(d.Get(isVolumeCapacity).(int)) + volumePatchModel.Capacity = &capacity + } + + volumePatch, err := volumePatchModel.AsPatch() + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("volumePatchModel.AsPatch() failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + updateVolumeOptions := &vpcv1.UpdateVolumeOptions{ + ID: &id, + VolumePatch: volumePatch, + IfMatch: &eTag, + } + + _, _, err = sess.UpdateVolumeWithContext(context, updateVolumeOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("UpdateVolumeWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + _, err = isWaitForVolumeAvailable(sess, id, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("isWaitForVolumeAvailable failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + hasChanged = true + } + + // Handle name update + if d.HasChange(isVolumeName) { + name := d.Get(isVolumeName).(string) + volumePatchModel := &vpcv1.VolumePatch{ + Name: &name, + } + volumePatch, err := volumePatchModel.AsPatch() + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("volumePatchModel.AsPatch() failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + updateVolumeOptions := &vpcv1.UpdateVolumeOptions{ + ID: &id, + VolumePatch: volumePatch, + } + + _, _, err = sess.UpdateVolumeWithContext(context, updateVolumeOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("UpdateVolumeWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + hasChanged = true + } + + // Handle tags update + if d.HasChange(isVolumeTags) { + userTagsSet := d.Get(isVolumeTags).(*schema.Set) + userTagsArray := make([]string, 0) + if userTagsSet != nil && userTagsSet.Len() > 0 { + for _, userTag := range userTagsSet.List() { + userTagsArray = append(userTagsArray, userTag.(string)) + } + } + + // Add environment tags + envTags := os.Getenv("IC_ENV_TAGS") + if envTags != "" { + envTagsArray := strings.Split(envTags, ",") + userTagsArray = append(userTagsArray, envTagsArray...) + } + + volumePatchModel := &vpcv1.VolumePatch{ + UserTags: userTagsArray, + } + volumePatch, err := volumePatchModel.AsPatch() + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("volumePatchModel.AsPatch() failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + updateVolumeOptions := &vpcv1.UpdateVolumeOptions{ + ID: &id, + VolumePatch: volumePatch, + } + + _, _, err = sess.UpdateVolumeWithContext(context, updateVolumeOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("UpdateVolumeWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + hasChanged = true + } + + // Handle access tags update + if d.HasChange(isVolumeAccessTags) { + getVolumeOptions := &vpcv1.GetVolumeOptions{ + ID: &id, + } + volume, _, err := sess.GetVolumeWithContext(context, getVolumeOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("GetVolumeWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "update") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + oldList, newList := d.GetChange(isVolumeAccessTags) + err = flex.UpdateGlobalTagsUsingCRN(oldList, newList, meta, *volume.CRN, "", isVolumeAccessTagType) + if err != nil { + log.Printf("Error on update of boot volume manager (%s) access tags: %s", id, err) + } + hasChanged = true + } + + // Handle snapshot deletion if requested + if deleteAllSnapshots, ok := d.GetOk(isVolumeDeleteAllSnapshots); ok && deleteAllSnapshots.(bool) { + deleteSnapshotsOptions := &vpcv1.DeleteSnapshotsOptions{ + SourceVolumeID: &id, + } + _, err := sess.DeleteSnapshotsWithContext(context, deleteSnapshotsOptions) + if err != nil { + log.Printf("Error deleting snapshots from boot volume (%s): %s", id, err) + } + } + + if hasChanged { + return resourceIBMISInstanceBootVolumeManagerRead(context, d, meta) + } + + return nil +} + +func resourceIBMISInstanceBootVolumeManagerDelete(context context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + sess, err := vpcClient(meta) + if err != nil { + tfErr := flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "delete", "initialize-client") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + id := d.Id() + + // Check if force delete is enabled + if d.Get(isInstanceBootVolumeManagerDelete).(bool) { + // Handle snapshot deletion if requested + if deleteAllSnapshots, ok := d.GetOk(isVolumeDeleteAllSnapshots); ok && deleteAllSnapshots.(bool) { + deleteSnapshotsOptions := &vpcv1.DeleteSnapshotsOptions{ + SourceVolumeID: &id, + } + _, err := sess.DeleteSnapshotsWithContext(context, deleteSnapshotsOptions) + if err != nil { + log.Printf("Error deleting snapshots from boot volume (%s): %s", id, err) + } + } + + deleteVolumeOptions := &vpcv1.DeleteVolumeOptions{ + ID: &id, + } + _, err := sess.DeleteVolumeWithContext(context, deleteVolumeOptions) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("DeleteVolumeWithContext failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "delete") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + + _, err = isWaitForVolumeDeleted(sess, id, d.Timeout(schema.TimeoutDelete)) + if err != nil { + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("isWaitForVolumeDeleted failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "delete") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return tfErr.GetDiag() + } + } + + d.SetId("") + return nil +} + +func resourceIBMISInstanceBootVolumeManagerExists(d *schema.ResourceData, meta interface{}) (bool, error) { + sess, err := vpcClient(meta) + if err != nil { + tfErr := flex.DiscriminatedTerraformErrorf(err, err.Error(), "ibm_is_instance_boot_volume_manager", "exists", "initialize-client") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return false, tfErr + } + + id := d.Id() + getVolumeOptions := &vpcv1.GetVolumeOptions{ + ID: &id, + } + _, response, err := sess.GetVolume(getVolumeOptions) + if err != nil { + if response != nil && response.StatusCode == 404 { + return false, nil + } + tfErr := flex.TerraformErrorf(err, fmt.Sprintf("GetVolume failed: %s", err.Error()), "ibm_is_instance_boot_volume_manager", "exists") + log.Printf("[DEBUG]\n%s", tfErr.GetDebugMessage()) + return false, tfErr + } + return true, nil +} diff --git a/ibm/service/vpc/resource_ibm_is_instance_boot_volume_manager_test.go b/ibm/service/vpc/resource_ibm_is_instance_boot_volume_manager_test.go new file mode 100644 index 0000000000..723f4d0e56 --- /dev/null +++ b/ibm/service/vpc/resource_ibm_is_instance_boot_volume_manager_test.go @@ -0,0 +1,530 @@ +// Copyright IBM Corp. 2017, 2025 All Rights Reserved. +// Licensed under the Mozilla Public License v2.0 + +package vpc_test + +import ( + "errors" + "fmt" + "strings" + "testing" + + acc "github.com/IBM-Cloud/terraform-provider-ibm/ibm/acctest" + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/conns" + + "github.com/IBM/vpc-go-sdk/vpcv1" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccIBMISInstanceBootVolumeManager_basic(t *testing.T) { + var volumeID string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMISInstanceBootVolumeManagerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "boot_volume"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "bandwidth"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "capacity"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "crn"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "encryption_type"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "health_state"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "id"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "iops"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "name"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "profile"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "zone"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "status"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "resource_group.#"), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "volume_attachments.#"), + ), + }, + }, + }) +} + +func TestAccIBMISInstanceBootVolumeManager_name_update(t *testing.T) { + var volumeID string + name1 := fmt.Sprintf("tfbootvol-%d", acctest.RandIntRange(10, 100)) + name2 := fmt.Sprintf("tfbootvol-updated-%d", acctest.RandIntRange(10, 100)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMISInstanceBootVolumeManagerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithName(name1), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "name", name1), + ), + }, + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithName(name2), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "name", name2), + ), + }, + }, + }) +} + +func TestAccIBMISInstanceBootVolumeManager_capacity_update(t *testing.T) { + var volumeID string + capacity1 := 120 + capacity2 := 180 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMISInstanceBootVolumeManagerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "capacity"), + ), + }, + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithCapacity(capacity1), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "capacity", fmt.Sprintf("%d", capacity1)), + ), + }, + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithCapacity(capacity2), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "capacity", fmt.Sprintf("%d", capacity2)), + ), + }, + }, + }) +} + +func TestAccIBMISInstanceBootVolumeManager_profile_update(t *testing.T) { + var volumeID string + profile := "10iops-tier" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMISInstanceBootVolumeManagerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "profile"), + ), + }, + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithProfile(profile), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "profile", profile), + ), + }, + }, + }) +} + +func TestAccIBMISInstanceBootVolumeManager_iops_update(t *testing.T) { + var volumeID string + iops1 := 600 + iops2 := 900 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMISInstanceBootVolumeManagerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithIOPS("custom", iops1), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "iops", fmt.Sprintf("%d", iops1)), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "profile", "custom"), + ), + }, + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithIOPS("custom", iops2), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "iops", fmt.Sprintf("%d", iops2)), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "profile", "custom"), + ), + }, + }, + }) +} + +func TestAccIBMISInstanceBootVolumeManager_tags(t *testing.T) { + var volumeID string + tag1 := "env:test" + tag2 := "team:dev" + tag3 := "boot:managed" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMISInstanceBootVolumeManagerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithTags(tag1, tag2), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "tags.#", "2"), + ), + }, + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithTags(tag1, tag2, tag3), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "tags.#", "3"), + ), + }, + }, + }) +} + +func TestAccIBMISInstanceBootVolumeManager_access_tags(t *testing.T) { + var volumeID string + accessTag := "project:test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMISInstanceBootVolumeManagerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithAccessTags(accessTag), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "access_tags.#", "1"), + ), + }, + { + ResourceName: "ibm_is_instance_boot_volume_manager.test_boot_volume", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"delete_volume", "delete_all_snapshots"}, + }, + }, + }) +} + +func TestAccIBMISInstanceBootVolumeManager_complete_update(t *testing.T) { + var volumeID string + name := fmt.Sprintf("tfbootvol-%d", acctest.RandIntRange(10, 100)) + updatedName := fmt.Sprintf("tfbootvol-updated-%d", acctest.RandIntRange(10, 100)) + profile := "10iops-tier" + capacity := 200 + tag1 := "env:prod" + tag2 := "boot:managed" + accessTag := "project:production" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMISInstanceBootVolumeManagerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttrSet( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "name"), + ), + }, + { + Config: testAccCheckIBMISInstanceBootVolumeManagerCompleteConfig(name, profile, capacity, tag1, tag2, accessTag), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "name", name), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "profile", profile), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "capacity", fmt.Sprintf("%d", capacity)), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "tags.#", "2"), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "access_tags.#", "1"), + ), + }, + { + Config: testAccCheckIBMISInstanceBootVolumeManagerCompleteConfig(updatedName, profile, capacity+50, tag1, tag2, accessTag), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "name", updatedName), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "capacity", fmt.Sprintf("%d", capacity+50)), + ), + }, + }, + }) +} + +func TestAccIBMISInstanceBootVolumeManager_delete_volume(t *testing.T) { + var volumeID string + vpcName := fmt.Sprintf("tfvpc-%d", acctest.RandIntRange(10, 100)) + subnetName := fmt.Sprintf("tfsubnet-%d", acctest.RandIntRange(10, 100)) + sshName := fmt.Sprintf("tfssh-%d", acctest.RandIntRange(10, 100)) + instanceName := fmt.Sprintf("tfinstance-%d", acctest.RandIntRange(10, 100)) + bootVolumeName := fmt.Sprintf("tfbootvol-%d", acctest.RandIntRange(10, 100)) + publicKey := strings.TrimSpace(` +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCKVmnMOlHKcZK8tpt3MP1lqOLAcqcJzhsvJcjscgVERRN7/9484SOBJ3HSKxxNG5JN8owAjy5f9yYwcUg+JaUVuytn5Pv3aeYROHGGg+5G346xaq3DAwX6Y5ykr2fvjObgncQBnuU5KHWCECO/4h8uWuwh/kfniXPVjFToc+gnkqA+3RKpAecZhFXwfalQ9mMuYGFxn+fwn8cYEApsJbsEmb0iJwPiZ5hjFC8wREuiTlhPHDgkBLOiycd20op2nXzDbHfCHInquEe/gYxEitALONxm0swBOwJZwlTDOB7C6y2dzlrtxr1L59m7pCkWI4EtTRLvleehBoj3u7jB4usR +`) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMISInstanceBootVolumeManagerDestroyWithDeletion, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMISInstanceBootVolumeManagerConfigWithDeletion(vpcName, subnetName, sshName, publicKey, instanceName, bootVolumeName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMISInstanceBootVolumeManagerExists("ibm_is_instance_boot_volume_manager.test_boot_volume", volumeID), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "name", bootVolumeName), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "delete_volume", "true"), + resource.TestCheckResourceAttr( + "ibm_is_instance_boot_volume_manager.test_boot_volume", "delete_all_snapshots", "true"), + ), + }, + }, + }) +} + +func testAccCheckIBMISInstanceBootVolumeManagerDestroy(s *terraform.State) error { + sess, _ := acc.TestAccProvider.Meta().(conns.ClientSession).VpcV1API() + for _, rs := range s.RootModule().Resources { + if rs.Type != "ibm_is_instance_boot_volume_manager" { + continue + } + + getVolumeOptions := &vpcv1.GetVolumeOptions{ + ID: &rs.Primary.ID, + } + _, response, err := sess.GetVolume(getVolumeOptions) + + if err == nil { + // Volume still exists, which is expected if delete_volume was false + continue + } + if response != nil && response.StatusCode != 404 { + return fmt.Errorf("Error checking for boot volume (%s) deletion: %s", rs.Primary.ID, err) + } + } + return nil +} + +func testAccCheckIBMISInstanceBootVolumeManagerDestroyWithDeletion(s *terraform.State) error { + sess, _ := acc.TestAccProvider.Meta().(conns.ClientSession).VpcV1API() + for _, rs := range s.RootModule().Resources { + if rs.Type != "ibm_is_instance_boot_volume_manager" { + continue + } + + getVolumeOptions := &vpcv1.GetVolumeOptions{ + ID: &rs.Primary.ID, + } + _, response, err := sess.GetVolume(getVolumeOptions) + + if err == nil { + return fmt.Errorf("Boot volume still exists: %s", rs.Primary.ID) + } + if response != nil && response.StatusCode != 404 { + return fmt.Errorf("Error checking for boot volume (%s) deletion: %s", rs.Primary.ID, err) + } + } + return nil +} + +func testAccCheckIBMISInstanceBootVolumeManagerExists(n, volumeID string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return errors.New("No Record ID is set") + } + + sess, _ := acc.TestAccProvider.Meta().(conns.ClientSession).VpcV1API() + getVolumeOptions := &vpcv1.GetVolumeOptions{ + ID: &rs.Primary.ID, + } + foundVolume, _, err := sess.GetVolume(getVolumeOptions) + if err != nil { + return err + } + volumeID = *foundVolume.ID + return nil + } +} + +func testAccCheckIBMISInstanceBootVolumeManagerConfig() string { + return fmt.Sprintf(` +resource "ibm_is_instance_boot_volume_manager" "test_boot_volume" { + boot_volume = "%s" +}`, acc.VSIUnattachedBootVolumeID) +} + +func testAccCheckIBMISInstanceBootVolumeManagerConfigWithName(name string) string { + return fmt.Sprintf(` +resource "ibm_is_instance_boot_volume_manager" "test_boot_volume" { + boot_volume = "%s" + name = "%s" +}`, acc.VSIUnattachedBootVolumeID, name) +} + +func testAccCheckIBMISInstanceBootVolumeManagerConfigWithCapacity(capacity int) string { + return fmt.Sprintf(` +resource "ibm_is_instance_boot_volume_manager" "test_boot_volume" { + boot_volume = "%s" + capacity = %d +}`, acc.VSIUnattachedBootVolumeID, capacity) +} + +func testAccCheckIBMISInstanceBootVolumeManagerConfigWithProfile(profile string) string { + return fmt.Sprintf(` +resource "ibm_is_instance_boot_volume_manager" "test_boot_volume" { + boot_volume = "%s" + profile = "%s" +}`, acc.VSIUnattachedBootVolumeID, profile) +} + +func testAccCheckIBMISInstanceBootVolumeManagerConfigWithIOPS(profile string, iops int) string { + return fmt.Sprintf(` +resource "ibm_is_instance_boot_volume_manager" "test_boot_volume" { + boot_volume = "%s" + profile = "%s" + iops = %d +}`, acc.VSIUnattachedBootVolumeID, profile, iops) +} + +func testAccCheckIBMISInstanceBootVolumeManagerConfigWithTags(tags ...string) string { + tagList := `["` + strings.Join(tags, `", "`) + `"]` + return fmt.Sprintf(` +resource "ibm_is_instance_boot_volume_manager" "test_boot_volume" { + boot_volume = "%s" + tags = %s +}`, acc.VSIUnattachedBootVolumeID, tagList) +} + +func testAccCheckIBMISInstanceBootVolumeManagerConfigWithAccessTags(accessTag string) string { + return fmt.Sprintf(` +resource "ibm_is_instance_boot_volume_manager" "test_boot_volume" { + boot_volume = "%s" + access_tags = ["%s"] +}`, acc.VSIUnattachedBootVolumeID, accessTag) +} + +func testAccCheckIBMISInstanceBootVolumeManagerCompleteConfig(name, profile string, capacity int, tag1, tag2, accessTag string) string { + return fmt.Sprintf(` +resource "ibm_is_instance_boot_volume_manager" "test_boot_volume" { + boot_volume = "%s" + name = "%s" + profile = "%s" + capacity = %d + tags = ["%s", "%s"] + access_tags = ["%s"] +}`, acc.VSIUnattachedBootVolumeID, name, profile, capacity, tag1, tag2, accessTag) +} + +func testAccCheckIBMISInstanceBootVolumeManagerConfigWithDeletion(vpcName, subnetName, sshName, publicKey, instanceName, bootVolumeName string) string { + return fmt.Sprintf(` +resource "ibm_is_vpc" "test_vpc" { + name = "%s" +} + +resource "ibm_is_subnet" "test_subnet" { + name = "%s" + vpc = ibm_is_vpc.test_vpc.id + zone = "%s" + total_ipv4_address_count = 16 +} + +resource "ibm_is_ssh_key" "test_ssh_key" { + name = "%s" + public_key = "%s" +} + +resource "ibm_is_volume" "test_boot_volume" { + name = "%s" + profile = "general-purpose" + zone = "%s" + capacity = 100 +} + +resource "ibm_is_instance" "test_instance" { + name = "%s" + image = "%s" + profile = "%s" + + boot_volume { + volume_id = ibm_is_instance_boot_volume_manager.test_boot_volume.id + auto_delete_volume = false + } + + primary_network_interface { + subnet = ibm_is_subnet.test_subnet.id + } + + vpc = ibm_is_vpc.test_vpc.id + zone = "%s" + keys = [ibm_is_ssh_key.test_ssh_key.id] +} + +resource "ibm_is_instance_boot_volume_manager" "test_boot_volume" { + boot_volume = ibm_is_volume.test_boot_volume.id + name = "%s" + delete_volume = true + delete_all_snapshots = true +}`, vpcName, subnetName, acc.ISZoneName, sshName, publicKey, bootVolumeName, acc.ISZoneName, instanceName, acc.IsImage, acc.InstanceProfileName, acc.ISZoneName, bootVolumeName) +} diff --git a/website/docs/r/is_instance_boot_volume_manager.html.markdown b/website/docs/r/is_instance_boot_volume_manager.html.markdown new file mode 100644 index 0000000000..9deea01a15 --- /dev/null +++ b/website/docs/r/is_instance_boot_volume_manager.html.markdown @@ -0,0 +1,256 @@ +--- + +subcategory: "VPC infrastructure" +layout: "ibm" +page_title: "IBM : ibm_is_instance_boot_volume_manager" +description: |- + Manages IBM VPC instance boot volume. +--- + +# ibm_is_instance_boot_volume_manager + +Provides a resource to manage the boot volume of a VPC virtual server instance. This resource allows you to manage boot volumes that may have been orphaned or need to be managed independently of their instance lifecycle. + +~> **NOTE:** This is an advanced resource with special caveats. Please read this document in its entirety before using this resource. The `ibm_is_instance_boot_volume_manager` resource behaves differently from normal resources. Terraform does not _create_ this resource but instead attempts to "adopt" it into management. + +This resource is particularly useful for managing boot volumes that: +- Have been orphaned after instance deletion +- Need independent lifecycle management from their instance +- Require specific configuration changes that affect the boot volume properties + +For more information, about VPC virtual server instances, see [getting started with Virtual Private Cloud](https://cloud.ibm.com/docs/vpc?topic=vpc-getting-started). For more information about VPC storage, see [About Block Storage for VPC](https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-about). + +**Note:** +VPC infrastructure services are a regional specific based endpoint, by default targets to `us-south`. Please make sure to target right region in the provider block as shown in the `provider.tf` file, if VPC service is created in region other than `us-south`. + +**provider.tf** + +```terraform +provider "ibm" { + region = "eu-gb" +} +``` + +## Example usage + +### Basic boot volume management +```terraform +resource "ibm_is_instance_boot_volume_manager" "example" { + boot_volume = "r006-10cc2ee8-f395-47a1-b043-4e7a855a6dd0" + name = "my-managed-boot-volume" +} +``` + +### Boot volume with performance configuration +```terraform +resource "ibm_is_instance_boot_volume_manager" "example" { + boot_volume = "r006-10cc2ee8-f395-47a1-b043-4e7a855a6dd0" + name = "high-performance-boot" + profile = "10iops-tier" + capacity = 120 + iops = 3000 +} +``` + +### Boot volume with tags and conditional deletion +```terraform +resource "ibm_is_instance_boot_volume_manager" "example" { + boot_volume = "r006-10cc2ee8-f395-47a1-b043-4e7a855a6dd0" + name = "managed-boot-volume" + profile = "10iops-tier" + tags = ["env:production", "managed:terraform"] + access_tags = ["project:web-app"] + delete_volume = true + delete_all_snapshots = true +} +``` + +### Managing boot volume from instance +```terraform +resource "ibm_is_instance" "example" { + name = "example-instance" + image = "r006-14140f94-fcc4-11e9-96e7-a72723715315" + profile = "bx2-2x8" + + primary_network_interface { + subnet = ibm_is_subnet.example.id + } + + vpc = ibm_is_vpc.example.id + zone = "us-south-1" + keys = [ibm_is_ssh_key.example.id] +} + +resource "ibm_is_instance_boot_volume_manager" "example" { + boot_volume = ibm_is_instance.example.boot_volume[0].volume_id + name = "managed-boot-volume" + profile = "10iops-tier" + delete_volume = false # Don't delete when resource is destroyed +} +``` + +## Argument reference +Review the argument references that you can specify for your resource. + +- `access_tags` - (Optional, List of Strings) A list of access management tags to attach to the boot volume. + + ~> **Note:** + **•** You can attach only those access tags that already exists.
+ **•** For more information, about creating access tags, see [working with tags](https://cloud.ibm.com/docs/account?topic=account-tag&interface=ui#create-access-console).
+ **•** You must have the access listed in the [Granting users access to tag resources](https://cloud.ibm.com/docs/account?topic=account-access) for `access_tags`
+ **•** `access_tags` must be in the format `key:value`. +- `boot_volume` - (Required, Forces new resource, String) The unique identifier for the boot volume. +- `capacity` - (Optional, Integer) The capacity of the volume in gigabytes. The minimum capacity is 10 GB and the maximum capacity is 16,000 GB. + + ~> **NOTE:** Supports only expansion on update (must be attached to a running instance and must not be less than the current volume capacity). Can be updated only if volume is attached to a running virtual server instance. Stopped instance will be started automatically on update of capacity. +- `delete_all_snapshots` - (Optional, Boolean) If set to true, all snapshots created from this volume will be deleted when the volume is deleted. Default value is `false`. +- `delete_volume` - (Optional, Boolean) If set to true, the boot volume will be deleted when this resource is destroyed. Default value is `false`. +- `iops` - (Optional, Integer) The maximum I/O operations per second (IOPS) for the volume. This value is required for `custom` storage profiles only. + + ~> **NOTE:** `iops` value can be upgraded and downgraded if volume is attached to a running virtual server instance. Stopped instances will be started automatically on update of volume. + + This table shows how storage size affects the `iops` ranges: + + | Size range (GB) | IOPS range | + |--------------------|----------------| + | 10 - 39 | 100 - 1000 | + | 40 - 79 | 100 - 2000 | + | 80 - 99 | 100 - 4000 | + | 100 - 499 | 100 - 6000 | + | 500 - 999 | 100 - 10000 | + | 1000 - 1999 | 100 - 20000 | + | 2000 - 3999 | 100 - 40000 | + | 4000 - 7999 | 100 - 40000 | + | 8000 - 9999 | 100 - 48000 | + | 10000 - 16000 | 100 - 48000 | + +- `name` - (Optional, String) The user-defined name for this boot volume. If unspecified, the name will be automatically assigned. +- `profile` - (Optional, String) The globally unique name of the volume profile to use for this volume. Valid profiles are `general-purpose`, `5iops-tier`, `10iops-tier`, and `custom`. + + ~> **NOTE:** Tiered profiles [`general-purpose`, `5iops-tier`, `10iops-tier`] can be upgraded and downgraded into each other if volume is attached to a running virtual server instance. Stopped instances will be started automatically on update of volume. +- `tags` - (Optional, List of Strings) User tags for the boot volume. + +## Attribute reference +In addition to all argument reference list, you can access the following attribute reference after your resource is created. + +- `bandwidth` - (Integer) The maximum bandwidth (in megabits per second) for the volume. +- `crn` - (String) The CRN for the volume. +- `encryption_key` - (String) The CRN of the encryption key used to encrypt this volume. +- `encryption_type` - (String) The type of encryption used on the volume. +- `health_reasons` - (List) The reasons for the current health_state (if any). + + Nested scheme for `health_reasons`: + - `code` - (String) A snake case string succinctly identifying the reason for this health state. + - `message` - (String) An explanation of the reason for this health state. + - `more_info` - (String) Link to documentation about the reason for this health state. +- `health_state` - (String) The health state of this volume. +- `id` - (String) The unique identifier of the volume. +- `resource_group` - (List) The resource group for this volume. + + Nested scheme for `resource_group`: + - `href` - (String) The URL for this resource group. + - `id` - (String) The unique identifier for this resource group. + - `name` - (String) The user-defined name for this resource group. +- `source_snapshot` - (String) The unique identifier for the snapshot from which this volume was created. +- `status` - (String) The status of volume. Supported values are **available**, **failed**, **pending**, **unusable**, or **pending_deletion**. +- `status_reasons` - (List) Array of reasons for the current status. + + Nested scheme for `status_reasons`: + - `code` - (String) A string with an underscore as a special character identifying the status reason. + - `message` - (String) An explanation of the status reason. + - `more_info` - (String) Link to documentation about this status reason. +- `volume_attachments` - (List) The volume attachments for this volume. + + Nested scheme for `volume_attachments`: + - `delete_volume_on_instance_delete` - (Boolean) If set to true, this volume will be deleted when the instance is deleted. + - `device` - (List) Information about how the volume is exposed to the instance operating system. + + Nested scheme for `device`: + - `id` - (String) A unique identifier for the device which is exposed to the instance operating system. + - `href` - (String) The URL for this volume attachment. + - `id` - (String) The unique identifier for this volume attachment. + - `instance` - (List) The attached instance. + + Nested scheme for `instance`: + - `crn` - (String) The CRN for this virtual server instance. + - `href` - (String) The URL for this virtual server instance. + - `id` - (String) The unique identifier for this virtual server instance. + - `name` - (String) The user-defined name for this virtual server instance. + - `name` - (String) The user-defined name for this volume attachment. + - `type` - (String) The type of volume attachment. +- `zone` - (String) The zone where this volume resides. + +## Timeouts +The `ibm_is_instance_boot_volume_manager` resource provides the following [Timeouts](https://www.terraform.io/docs/language/resources/syntax.html) configuration options: + +- **create** - (Default 10 minutes) Used for adopting the boot volume. +- **update** - (Default 10 minutes) Used for updating boot volume properties. +- **delete** - (Default 10 minutes) Used for deleting boot volume (when `delete_volume` is true). + +## Import +The `ibm_is_instance_boot_volume_manager` resource can be imported by using the boot volume ID. + +**Syntax** + +``` +$ terraform import ibm_is_instance_boot_volume_manager.example +``` + +**Example** + +``` +$ terraform import ibm_is_instance_boot_volume_manager.example d7bec597-4726-451f-8a63-e62e6f19c32c +``` + +## Usage scenarios + +### Orphaned boot volume management +When an instance is deleted but the boot volume remains (either by design or accident), you can use this resource to manage the orphaned volume: + +```terraform +resource "ibm_is_instance_boot_volume_manager" "orphaned_volume" { + boot_volume = "r006-orphaned-volume-id" + name = "recovered-boot-volume" + profile = "10iops-tier" + delete_volume = true # Clean up when done +} +``` + +### Instance-independent management +Manage boot volume properties independently of the instance lifecycle: + +```terraform +# Instance with minimal boot volume configuration +resource "ibm_is_instance" "app_server" { + name = "app-server" + # ... other configuration +} + +# Separate management of the boot volume +resource "ibm_is_instance_boot_volume_manager" "boot_config" { + boot_volume = ibm_is_instance.app_server.boot_volume[0].volume_id + name = "app-server-boot-managed" + profile = "10iops-tier" + tags = ["env:production", "managed:terraform"] + delete_volume = false # Keep volume when resource is destroyed + delete_all_snapshots = false # Preserve snapshots +} +``` + +## Important notes + +- **Instance state management**: When updating volume properties like capacity, profile, or IOPS, the attached instance must be running. This resource will automatically start stopped instances when necessary for updates. +- **Boot volume lifecycle**: Boot volumes cannot be detached from running instances. This resource manages the volume properties while it remains attached. +- **Deletion behavior**: By default, this resource does not delete the actual boot volume when destroyed (only removes it from Terraform state). Set `delete_volume = true` to actually delete the volume. +- **Snapshot management**: Set `delete_all_snapshots = true` to automatically clean up all snapshots when the volume is deleted. +- **Performance updates**: Volume performance characteristics (profile, IOPS, capacity) can be modified while the volume is in use, but may require brief instance restart. +- **Profile compatibility**: Some profile changes require specific IOPS values. Refer to the IOPS table for valid combinations. + +## Error handling + +Common scenarios and resolutions: + +- **Volume not found**: Ensure the boot volume ID is correct and the volume exists in the target region. +- **Volume attached to running instance**: Some operations require the instance to be running. The resource will attempt to start stopped instances automatically. +- **Invalid IOPS for profile**: Ensure IOPS values are within the valid range for the selected profile and capacity. +- **Capacity decrease**: Boot volume capacity can only be increased, never decreased.