diff --git a/alicloud/provider.go b/alicloud/provider.go index a3c2f5d1540c..71efcce4c51c 100644 --- a/alicloud/provider.go +++ b/alicloud/provider.go @@ -1638,6 +1638,8 @@ func Provider() terraform.ResourceProvider { "alicloud_adb_db_cluster": resourceAliCloudAdbDbCluster(), "alicloud_ecs_disk": resourceAliCloudEcsDisk(), "alicloud_ecs_disk_attachment": resourceAlicloudEcsDiskAttachment(), + "alicloud_ecs_disk_encryption_by_default": resourceAliCloudEcsDiskEncryptionByDefault(), + "alicloud_ecs_disk_default_kms_key_id": resourceAliCloudEcsDiskDefaultKmsKeyId(), "alicloud_ecs_auto_snapshot_policy_attachment": resourceAliCloudEcsAutoSnapshotPolicyAttachment(), "alicloud_ddoscoo_domain_resource": resourceAliCloudDdosCooDomainResource(), "alicloud_ddoscoo_port": resourceAliCloudDdosCooPort(), diff --git a/alicloud/resource_alicloud_ecs_disk_default_kms_key_id.go b/alicloud/resource_alicloud_ecs_disk_default_kms_key_id.go new file mode 100644 index 000000000000..0f6af9c787ea --- /dev/null +++ b/alicloud/resource_alicloud_ecs_disk_default_kms_key_id.go @@ -0,0 +1,125 @@ +package alicloud + +import ( + "log" + "time" + + "github.com/aliyun/terraform-provider-alicloud/alicloud/connectivity" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceAliCloudEcsDiskDefaultKmsKeyId() *schema.Resource { + return &schema.Resource{ + Create: resourceAliCloudEcsDiskDefaultKmsKeyIdCreate, + Read: resourceAliCloudEcsDiskDefaultKmsKeyIdRead, + Update: resourceAliCloudEcsDiskDefaultKmsKeyIdUpdate, + Delete: resourceAliCloudEcsDiskDefaultKmsKeyIdDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + Schema: map[string]*schema.Schema{ + "kms_key_id": { + Type: schema.TypeString, + Required: true, + Description: "The KMS key ID used for ECS disk encryption by default.", + }, + }, + } +} + +func resourceAliCloudEcsDiskDefaultKmsKeyIdCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*connectivity.AliyunClient) + + // Use region as the resource ID since this is a region-scoped setting + d.SetId(client.RegionId) + + return resourceAliCloudEcsDiskDefaultKmsKeyIdUpdate(d, meta) +} + +func resourceAliCloudEcsDiskDefaultKmsKeyIdRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*connectivity.AliyunClient) + ecsServiceV2 := EcsServiceV2{client} + object, err := ecsServiceV2.DescribeEcsDiskDefaultKMSKeyId(d.Id()) + if err != nil { + if NotFoundError(err) { + log.Printf("[DEBUG] Resource alicloud_ecs_disk_default_kms_key_id ecsServiceV2.DescribeEcsDiskDefaultKMSKeyId Failed!!! %s", err) + d.SetId("") + return nil + } + return WrapError(err) + } + + if kmsKeyId, ok := object["KMSKeyId"]; ok && kmsKeyId != nil { + d.Set("kms_key_id", kmsKeyId) + } + return nil +} + +func resourceAliCloudEcsDiskDefaultKmsKeyIdUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*connectivity.AliyunClient) + var request map[string]interface{} + var query map[string]interface{} + action := "ModifyDiskDefaultKMSKeyId" + request = make(map[string]interface{}) + query = make(map[string]interface{}) + request["RegionId"] = client.RegionId + request["KMSKeyId"] = d.Get("kms_key_id") + + wait := incrementalWait(3*time.Second, 5*time.Second) + err := resource.Retry(client.GetRetryTimeout(d.Timeout(schema.TimeoutUpdate)), func() *resource.RetryError { + _, err := client.RpcPost("Ecs", "2014-05-26", action, query, request, true) + + if err != nil { + if NeedRetry(err) { + wait() + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + addDebug(action, nil, request) + + if err != nil { + return WrapErrorf(err, DefaultErrorMsg, d.Id(), action, AlibabaCloudSdkGoERROR) + } + + return resourceAliCloudEcsDiskDefaultKmsKeyIdRead(d, meta) +} + +func resourceAliCloudEcsDiskDefaultKmsKeyIdDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*connectivity.AliyunClient) + var request map[string]interface{} + var query map[string]interface{} + action := "ResetDiskDefaultKMSKeyId" + request = make(map[string]interface{}) + query = make(map[string]interface{}) + request["RegionId"] = client.RegionId + + wait := incrementalWait(3*time.Second, 5*time.Second) + err := resource.Retry(client.GetRetryTimeout(d.Timeout(schema.TimeoutDelete)), func() *resource.RetryError { + _, err := client.RpcPost("Ecs", "2014-05-26", action, query, request, true) + + if err != nil { + if NeedRetry(err) { + wait() + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + addDebug(action, nil, request) + + if err != nil { + return WrapErrorf(err, DefaultErrorMsg, d.Id(), action, AlibabaCloudSdkGoERROR) + } + + return nil +} diff --git a/alicloud/resource_alicloud_ecs_disk_default_kms_key_id_test.go b/alicloud/resource_alicloud_ecs_disk_default_kms_key_id_test.go new file mode 100644 index 000000000000..df1b9eaab3b9 --- /dev/null +++ b/alicloud/resource_alicloud_ecs_disk_default_kms_key_id_test.go @@ -0,0 +1,399 @@ +package alicloud + +import ( + "fmt" + "os" + "reflect" + "testing" + + "github.com/agiledragon/gomonkey/v2" + "github.com/alibabacloud-go/tea-rpc/client" + util "github.com/alibabacloud-go/tea-utils/service" + "github.com/alibabacloud-go/tea/tea" + "github.com/aliyun/terraform-provider-alicloud/alicloud/connectivity" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/stretchr/testify/assert" +) + +func TestAccAliCloudEcsDiskDefaultKmsKeyId_basic(t *testing.T) { + var v map[string]interface{} + resourceId := "alicloud_ecs_disk_default_kms_key_id.default" + ra := resourceAttrInit(resourceId, AliCloudEcsDiskDefaultKmsKeyIdMap) + rc := resourceCheckInitWithDescribeMethod(resourceId, &v, func() interface{} { + return &EcsServiceV2{testAccProvider.Meta().(*connectivity.AliyunClient)} + }, "DescribeEcsDiskDefaultKMSKeyId") + rac := resourceAttrCheckInit(rc, ra) + testAccCheck := rac.resourceAttrMapUpdateSet() + rand := acctest.RandIntRange(10000, 99999) + name := fmt.Sprintf("tf-testAccEcsDiskDefaultKmsKeyId%d", rand) + testAccConfig := resourceTestAccConfigFunc(resourceId, name, AliCloudEcsDiskDefaultKmsKeyIdBasicDependence) + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + IDRefreshName: resourceId, + Providers: testAccProviders, + CheckDestroy: testAccCheckEcsDiskDefaultKmsKeyIdDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConfig(map[string]interface{}{ + "kms_key_id": "${alicloud_kms_key.example.id}", + }), + Check: resource.ComposeTestCheckFunc( + testAccCheck(map[string]string{ + "kms_key_id": CHECKSET, + }), + ), + }, + { + Config: testAccConfig(map[string]interface{}{ + "kms_key_id": "${alicloud_kms_key.example2.id}", + }), + Check: resource.ComposeTestCheckFunc( + testAccCheck(map[string]string{ + "kms_key_id": CHECKSET, + }), + ), + }, + { + ResourceName: resourceId, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckEcsDiskDefaultKmsKeyIdDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*connectivity.AliyunClient) + ecsServiceV2 := EcsServiceV2{client} + + for _, rs := range s.RootModule().Resources { + if rs.Type != "alicloud_ecs_disk_default_kms_key_id" { + continue + } + + // Check the encryption status to determine if the resource is "destroyed" + // When the default KMS key is reset, encryption by default should be disabled + object, err := ecsServiceV2.DescribeEcsDiskEncryptionByDefaultStatus(rs.Primary.ID) + if err != nil { + // If we can't get the encryption status, consider it as destroyed + if NotFoundError(err) { + continue + } + return WrapError(err) + } + + // Check if encryption is disabled (this means the default KMS key is reset) + if encrypted, ok := object["Encrypted"].(bool); ok && !encrypted { + continue // Resource is properly "destroyed" (encryption disabled, meaning no default KMS key) + } + + // If encryption is still enabled, check if there's actually a KMS key set + kmsObject, err := ecsServiceV2.DescribeEcsDiskDefaultKMSKeyId(rs.Primary.ID) + if err != nil { + // If describe KMS key fails, consider it as destroyed + continue + } + + // If there's still a KMS key set, the resource wasn't properly destroyed + if kmsKeyId, ok := kmsObject["KMSKeyId"]; ok && kmsKeyId != nil && kmsKeyId != "" { + return WrapError(Error("ECS disk default KMS key ID is still set to %v in region %s", kmsKeyId, rs.Primary.ID)) + } + } + + return nil +} + +var AliCloudEcsDiskDefaultKmsKeyIdMap = map[string]string{} + +func AliCloudEcsDiskDefaultKmsKeyIdBasicDependence(name string) string { + return fmt.Sprintf(` + variable "name" { + default = "%s" + } + + data "alicloud_vpcs" "default" { + name_regex = "^default-NODELETING$" + } + + data "alicloud_vswitches" "default" { + vpc_id = data.alicloud_vpcs.default.ids.0 + zone_id = "cn-hangzhou-i" + } + + data "alicloud_vswitches" "default2" { + vpc_id = data.alicloud_vpcs.default.ids.0 + zone_id = "cn-hangzhou-j" + } + + # Create KMS instance + resource "alicloud_kms_instance" "example" { + product_version = "3" + vpc_num = "1" + key_num = "1000" + secret_num = "100" + spec = "1000" + vpc_id = data.alicloud_vpcs.default.ids.0 + vswitch_ids = [ + data.alicloud_vswitches.default.ids.0, + data.alicloud_vswitches.default2.ids.0 + ] + zone_ids = [ + "cn-hangzhou-i", + "cn-hangzhou-j" + ] + payment_type = "PayAsYouGo" + force_delete_without_backup = "true" + + timeouts { + delete = "20m" + } + } + + # Create a KMS key in the instance + resource "alicloud_kms_key" "example" { + description = "KMS key for ECS disk encryption" + pending_window_in_days = 7 + key_usage = "ENCRYPT/DECRYPT" + key_spec = "Aliyun_AES_256" + dkms_instance_id = alicloud_kms_instance.example.id + + timeouts { + delete = "20m" + } + } + + # Create a second KMS key for testing update + resource "alicloud_kms_key" "example2" { + description = "Second KMS key for ECS disk encryption update test" + pending_window_in_days = 7 + key_usage = "ENCRYPT/DECRYPT" + key_spec = "Aliyun_AES_256" + dkms_instance_id = alicloud_kms_instance.example.id + + timeouts { + delete = "20m" + } + } + + # Enable ECS disk encryption by default first + resource "alicloud_ecs_disk_encryption_by_default" "example" { + enabled = true + } + +`, name) +} + +func TestUnitAliCloudEcsDiskDefaultKmsKeyId(t *testing.T) { + p := Provider().(*schema.Provider).ResourcesMap + var dExisted2Data *schema.ResourceData + d, _ := schema.InternalMap(p["alicloud_ecs_disk_default_kms_key_id"].Schema).Data(nil, nil) + dCreate, _ := schema.InternalMap(p["alicloud_ecs_disk_default_kms_key_id"].Schema).Data(nil, nil) + dCreate.MarkNewResource() + kmsKeyId := "7906979c-8e06-46a2-be2d-68e3ccbc****" + d.Set("kms_key_id", kmsKeyId) + dCreate.Set("kms_key_id", kmsKeyId) + region := os.Getenv("ALICLOUD_REGION") + rawClient, err := sharedClientForRegion(region) + if err != nil { + t.Skipf("Skipping the test case with err: %s", err) + t.Skipped() + } + rawClient = rawClient.(*connectivity.AliyunClient) + ReadMockResponse := map[string]interface{}{ + // DescribeDiskDefaultKMSKeyId + "KMSKeyId": kmsKeyId, + } + CreateMockResponse := map[string]interface{}{ + // ModifyDiskDefaultKMSKeyId + } + failedResponseMock := func(errorCode string) (map[string]interface{}, error) { + return nil, &tea.SDKError{ + Code: String(errorCode), + Data: String(errorCode), + Message: String(errorCode), + StatusCode: tea.Int(400), + } + } + successResponseMock := func(operationMockResponse map[string]interface{}) (map[string]interface{}, error) { + if len(operationMockResponse) > 0 { + mapMerge(ReadMockResponse, operationMockResponse) + } + return ReadMockResponse, nil + } + + // Create + patches := gomonkey.ApplyMethod(reflect.TypeOf(&connectivity.AliyunClient{}), "NewEcsClient", func(_ *connectivity.AliyunClient) (*client.Client, error) { + return nil, &tea.SDKError{ + Code: String("loadEndpoint error"), + Data: String("loadEndpoint error"), + Message: String("loadEndpoint error"), + StatusCode: tea.Int(400), + } + }) + err = resourceAliCloudEcsDiskDefaultKmsKeyIdCreate(dCreate, rawClient) + patches.Reset() + assert.NotNil(t, err) + errorCodes := []string{"NonRetryableError", "Throttling", "nil"} + for index, errorCode := range errorCodes { + retryIndex := index - 1 // a counter used to cover retry scenario; -1 means no retry scenario; 0 means retry based on error code; 1 means retry based on status code + if errorCode == "nil" { + retryIndex = -1 + } + patches := gomonkey.ApplyMethod(reflect.TypeOf(&client.Client{}), "DoRequest", func(_ *client.Client, action *string, _ *string, _ *string, _ *string, _ *string, _ map[string]interface{}, _ map[string]interface{}, _ *util.RuntimeOptions) (map[string]interface{}, error) { + if retryIndex == 0 { + retryIndex++ + return failedResponseMock(errorCode) + } else if retryIndex == 1 { + retryIndex++ + return successResponseMock(CreateMockResponse) + } + return CreateMockResponse, nil + }) + err := resourceAliCloudEcsDiskDefaultKmsKeyIdCreate(dCreate, rawClient) + patches.Reset() + switch errorCode { + case "NonRetryableError": + assert.NotNil(t, err) + default: + assert.Nil(t, err) + dCompare, _ := schema.InternalMap(p["alicloud_ecs_disk_default_kms_key_id"].Schema).Data(dCreate.State(), nil) + _ = dCompare.Set("kms_key_id", attributes2["kms_key_id"]) + assert.Equal(t, dCompare.State().Attributes, dCreate.State().Attributes) + } + if retryIndex >= 0 { + patches.Reset() + } + } + + // Update + patches = gomonkey.ApplyMethod(reflect.TypeOf(&connectivity.AliyunClient{}), "NewEcsClient", func(_ *connectivity.AliyunClient) (*client.Client, error) { + return nil, &tea.SDKError{ + Code: String("loadEndpoint error"), + Data: String("loadEndpoint error"), + Message: String("loadEndpoint error"), + StatusCode: tea.Int(400), + } + }) + err = resourceAliCloudEcsDiskDefaultKmsKeyIdUpdate(d, rawClient) + patches.Reset() + assert.NotNil(t, err) + // ModifyDiskDefaultKMSKeyId + newKmsKeyId := "7906979c-8e06-46a2-be2d-68e3ccbc****" + attributesDiff := map[string]interface{}{ + "kms_key_id": newKmsKeyId, + } + diff, err := newInstanceDiff("alicloud_ecs_disk_default_kms_key_id", attributes2, attributesDiff, dCreate.State()) + if err != nil { + t.Error(err) + } + dExisted2Data, _ = schema.InternalMap(p["alicloud_ecs_disk_default_kms_key_id"].Schema).Data(dCreate.State(), diff) + ReadMockResponseDiff := map[string]interface{}{ + // DescribeDiskDefaultKMSKeyId Response + "KMSKeyId": newKmsKeyId, + } + errorCodes = []string{"NonRetryableError", "Throttling", "nil"} + for index, errorCode := range errorCodes { + retryIndex := index - 1 + if errorCode == "nil" { + retryIndex = -1 + } + patches := gomonkey.ApplyMethod(reflect.TypeOf(&client.Client{}), "DoRequest", func(_ *client.Client, action *string, _ *string, _ *string, _ *string, _ *string, _ map[string]interface{}, _ map[string]interface{}, _ *util.RuntimeOptions) (map[string]interface{}, error) { + if retryIndex == 0 { + retryIndex++ + return failedResponseMock(errorCode) + } else if retryIndex == 1 { + retryIndex++ + return successResponseMock(ReadMockResponseDiff) + } + return ReadMockResponse, nil + }) + err := resourceAliCloudEcsDiskDefaultKmsKeyIdUpdate(dExisted2Data, rawClient) + patches.Reset() + switch errorCode { + case "NonRetryableError": + assert.NotNil(t, err) + default: + assert.Nil(t, err) + dCompare, _ := schema.InternalMap(p["alicloud_ecs_disk_default_kms_key_id"].Schema).Data(dExisted2Data.State(), nil) + _ = dCompare.Set("kms_key_id", attributesDiff["kms_key_id"]) + assert.Equal(t, dCompare.State().Attributes, dExisted2Data.State().Attributes) + } + if retryIndex >= 0 { + patches.Reset() + } + } + + // Read + errorCodes = []string{"NonRetryableError", "Throttling", "nil", "{}"} + for index, errorCode := range errorCodes { + retryIndex := index - 1 + if errorCode == "nil" { + retryIndex = -1 + } + patches := gomonkey.ApplyMethod(reflect.TypeOf(&client.Client{}), "DoRequest", func(_ *client.Client, action *string, _ *string, _ *string, _ *string, _ *string, _ map[string]interface{}, _ map[string]interface{}, _ *util.RuntimeOptions) (map[string]interface{}, error) { + if retryIndex == 0 { + retryIndex++ + return failedResponseMock(errorCode) + } else if retryIndex == 1 { + retryIndex++ + return successResponseMock(ReadMockResponse) + } + return ReadMockResponse, nil + }) + err := resourceAliCloudEcsDiskDefaultKmsKeyIdRead(d, rawClient) + patches.Reset() + switch errorCode { + case "NonRetryableError": + assert.NotNil(t, err) + case "{}": + assert.Nil(t, err) + } + } + + // Delete + patches = gomonkey.ApplyMethod(reflect.TypeOf(&connectivity.AliyunClient{}), "NewEcsClient", func(_ *connectivity.AliyunClient) (*client.Client, error) { + return nil, &tea.SDKError{ + Code: String("loadEndpoint error"), + Data: String("loadEndpoint error"), + Message: String("loadEndpoint error"), + StatusCode: tea.Int(400), + } + }) + err = resourceAliCloudEcsDiskDefaultKmsKeyIdDelete(d, rawClient) + patches.Reset() + assert.NotNil(t, err) + errorCodes = []string{"NonRetryableError", "Throttling", "nil"} + for index, errorCode := range errorCodes { + retryIndex := index - 1 + if errorCode == "nil" { + retryIndex = -1 + } + patches := gomonkey.ApplyMethod(reflect.TypeOf(&client.Client{}), "DoRequest", func(_ *client.Client, action *string, _ *string, _ *string, _ *string, _ *string, _ map[string]interface{}, _ map[string]interface{}, _ *util.RuntimeOptions) (map[string]interface{}, error) { + if retryIndex == 0 { + retryIndex++ + return failedResponseMock(errorCode) + } else if retryIndex == 1 { + retryIndex++ + return successResponseMock(ReadMockResponse) + } + return ReadMockResponse, nil + }) + err := resourceAliCloudEcsDiskDefaultKmsKeyIdDelete(d, rawClient) + patches.Reset() + switch errorCode { + case "NonRetryableError": + assert.NotNil(t, err) + default: + assert.Nil(t, err) + } + } +} + +var attributes2 = map[string]interface{}{ + "kms_key_id": "7906979c-8e06-46a2-be2d-68e3ccbc****", +} diff --git a/alicloud/resource_alicloud_ecs_disk_encryption_by_default.go b/alicloud/resource_alicloud_ecs_disk_encryption_by_default.go new file mode 100644 index 000000000000..7343282511d5 --- /dev/null +++ b/alicloud/resource_alicloud_ecs_disk_encryption_by_default.go @@ -0,0 +1,156 @@ +package alicloud + +import ( + "log" + "time" + + "github.com/aliyun/terraform-provider-alicloud/alicloud/connectivity" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceAliCloudEcsDiskEncryptionByDefault() *schema.Resource { + return &schema.Resource{ + Create: resourceAliCloudEcsDiskEncryptionByDefaultCreate, + Read: resourceAliCloudEcsDiskEncryptionByDefaultRead, + Update: resourceAliCloudEcsDiskEncryptionByDefaultUpdate, + Delete: resourceAliCloudEcsDiskEncryptionByDefaultDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether to enable ECS disk encryption by default.", + }, + }, + } +} + +func resourceAliCloudEcsDiskEncryptionByDefaultCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*connectivity.AliyunClient) + + // Use region as the resource ID since this is a region-scoped setting + d.SetId(client.RegionId) + + enabled := d.Get("enabled").(bool) + if enabled { + return resourceAliCloudEcsDiskEncryptionByDefaultEnableEncryption(d, meta) + } else { + // If we want to disable encryption, we need to explicitly disable it + // in case it's currently enabled in the region + return resourceAliCloudEcsDiskEncryptionByDefaultDisableEncryption(d, meta) + } +} + +func resourceAliCloudEcsDiskEncryptionByDefaultRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*connectivity.AliyunClient) + ecsServiceV2 := EcsServiceV2{client} + + object, err := ecsServiceV2.DescribeEcsDiskEncryptionByDefaultStatus(d.Id()) + if err != nil { + if NotFoundError(err) { + log.Printf("[DEBUG] Resource alicloud_ecs_disk_encryption_by_default ecsServiceV2.DescribeEcsDiskEncryptionByDefaultStatus Failed!!! %s", err) + d.SetId("") + return nil + } + return WrapError(err) + } + + d.Set("enabled", object["Encrypted"]) + return nil +} + +func resourceAliCloudEcsDiskEncryptionByDefaultUpdate(d *schema.ResourceData, meta interface{}) error { + if d.HasChange("enabled") { + enabled := d.Get("enabled").(bool) + if enabled { + return resourceAliCloudEcsDiskEncryptionByDefaultEnableEncryption(d, meta) + } else { + return resourceAliCloudEcsDiskEncryptionByDefaultDisableEncryption(d, meta) + } + } + return resourceAliCloudEcsDiskEncryptionByDefaultRead(d, meta) +} + +func resourceAliCloudEcsDiskEncryptionByDefaultDelete(d *schema.ResourceData, meta interface{}) error { + // When deleting the resource, disable encryption by default + return resourceAliCloudEcsDiskEncryptionByDefaultDisableEncryption(d, meta) +} + +func resourceAliCloudEcsDiskEncryptionByDefaultEnableEncryption(d *schema.ResourceData, meta interface{}) error { + client := meta.(*connectivity.AliyunClient) + var request map[string]interface{} + var query map[string]interface{} + action := "EnableDiskEncryptionByDefault" + request = make(map[string]interface{}) + query = make(map[string]interface{}) + request["RegionId"] = client.RegionId + + wait := incrementalWait(3*time.Second, 5*time.Second) + err := resource.Retry(client.GetRetryTimeout(d.Timeout(schema.TimeoutCreate)), func() *resource.RetryError { + _, err := client.RpcPost("Ecs", "2014-05-26", action, query, request, true) + + if err != nil { + // If encryption is already enabled, treat it as success + if IsExpectedErrors(err, []string{"InvalidOperation.DefaultEncryptionAlreadyEnabled"}) { + return nil + } + if NeedRetry(err) { + wait() + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + addDebug(action, nil, request) + + if err != nil { + return WrapErrorf(err, DefaultErrorMsg, d.Id(), action, AlibabaCloudSdkGoERROR) + } + + return resourceAliCloudEcsDiskEncryptionByDefaultRead(d, meta) +} + +func resourceAliCloudEcsDiskEncryptionByDefaultDisableEncryption(d *schema.ResourceData, meta interface{}) error { + client := meta.(*connectivity.AliyunClient) + var request map[string]interface{} + var query map[string]interface{} + action := "DisableDiskEncryptionByDefault" + request = make(map[string]interface{}) + query = make(map[string]interface{}) + request["RegionId"] = client.RegionId + + wait := incrementalWait(3*time.Second, 5*time.Second) + err := resource.Retry(client.GetRetryTimeout(d.Timeout(schema.TimeoutDelete)), func() *resource.RetryError { + _, err := client.RpcPost("Ecs", "2014-05-26", action, query, request, true) + + if err != nil { + // If encryption is already disabled, treat it as success + if IsExpectedErrors(err, []string{"InvalidOperation.DefaultEncryptionAlreadyDisabled"}) { + return nil + } + if NeedRetry(err) { + wait() + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + addDebug(action, nil, request) + + if err != nil { + return WrapErrorf(err, DefaultErrorMsg, d.Id(), action, AlibabaCloudSdkGoERROR) + } + + return resourceAliCloudEcsDiskEncryptionByDefaultRead(d, meta) +} diff --git a/alicloud/resource_alicloud_ecs_disk_encryption_by_default_test.go b/alicloud/resource_alicloud_ecs_disk_encryption_by_default_test.go new file mode 100644 index 000000000000..bc1829cf3f20 --- /dev/null +++ b/alicloud/resource_alicloud_ecs_disk_encryption_by_default_test.go @@ -0,0 +1,313 @@ +package alicloud + +import ( + "os" + "reflect" + "testing" + + "github.com/agiledragon/gomonkey/v2" + "github.com/alibabacloud-go/tea-rpc/client" + util "github.com/alibabacloud-go/tea-utils/service" + "github.com/alibabacloud-go/tea/tea" + "github.com/aliyun/terraform-provider-alicloud/alicloud/connectivity" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/stretchr/testify/assert" +) + +func TestAccAliCloudEcsDiskEncryptionByDefault_basic(t *testing.T) { + var v map[string]interface{} + resourceId := "alicloud_ecs_disk_encryption_by_default.default" + ra := resourceAttrInit(resourceId, AliCloudEcsDiskEncryptionByDefaultMap) + rc := resourceCheckInitWithDescribeMethod(resourceId, &v, func() interface{} { + return &EcsServiceV2{testAccProvider.Meta().(*connectivity.AliyunClient)} + }, "DescribeEcsDiskEncryptionByDefaultStatus") + rac := resourceAttrCheckInit(rc, ra) + testAccCheck := rac.resourceAttrMapUpdateSet() + testAccConfig := resourceTestAccConfigFunc(resourceId, "", AliCloudEcsDiskEncryptionByDefaultBasicDependence) + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + IDRefreshName: resourceId, + Providers: testAccProviders, + CheckDestroy: testAccCheckEcsDiskEncryptionByDefaultDestroy, + Steps: []resource.TestStep{ + { + Config: testAccConfig(map[string]interface{}{ + "enabled": "false", + }), + Check: resource.ComposeTestCheckFunc( + testAccCheck(map[string]string{ + "enabled": "false", + }), + ), + }, + { + Config: testAccConfig(map[string]interface{}{ + "enabled": "true", + }), + Check: resource.ComposeTestCheckFunc( + testAccCheck(map[string]string{ + "enabled": "true", + }), + ), + }, + { + ResourceName: resourceId, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckEcsDiskEncryptionByDefaultDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*connectivity.AliyunClient) + ecsServiceV2 := EcsServiceV2{client} + + for _, rs := range s.RootModule().Resources { + if rs.Type != "alicloud_ecs_disk_encryption_by_default" { + continue + } + + // For this region-level setting resource, we check that encryption is disabled + object, err := ecsServiceV2.DescribeEcsDiskEncryptionByDefaultStatus(rs.Primary.ID) + if err != nil { + // If we can't get the status, consider it as destroyed + if NotFoundError(err) { + continue + } + return WrapError(err) + } + + // Check if encryption is disabled (this means the resource is in "destroyed" state) + if encrypted, ok := object["Encrypted"].(bool); ok && !encrypted { + continue // Resource is properly "destroyed" (encryption disabled) + } + + // If encryption is still enabled, the resource wasn't properly destroyed + return WrapError(Error("ECS disk encryption by default is still enabled in region %s", rs.Primary.ID)) + } + + return nil +} + +var AliCloudEcsDiskEncryptionByDefaultMap = map[string]string{} + +func AliCloudEcsDiskEncryptionByDefaultBasicDependence(name string) string { + return "" +} + +func TestUnitAliCloudEcsDiskEncryptionByDefault(t *testing.T) { + p := Provider().(*schema.Provider).ResourcesMap + var dExisted2Data *schema.ResourceData + d, _ := schema.InternalMap(p["alicloud_ecs_disk_encryption_by_default"].Schema).Data(nil, nil) + dCreate, _ := schema.InternalMap(p["alicloud_ecs_disk_encryption_by_default"].Schema).Data(nil, nil) + dCreate.MarkNewResource() + d.Set("enabled", false) + dCreate.Set("enabled", true) + region := os.Getenv("ALICLOUD_REGION") + rawClient, err := sharedClientForRegion(region) + if err != nil { + t.Skipf("Skipping the test case with err: %s", err) + t.Skipped() + } + rawClient = rawClient.(*connectivity.AliyunClient) + ReadMockResponse := map[string]interface{}{ + // DescribeDiskEncryptionByDefaultStatus + "Encrypted": false, + } + CreateMockResponse := map[string]interface{}{ + // EnableDiskEncryptionByDefault + } + failedResponseMock := func(errorCode string) (map[string]interface{}, error) { + return nil, &tea.SDKError{ + Code: String(errorCode), + Data: String(errorCode), + Message: String(errorCode), + StatusCode: tea.Int(400), + } + } + successResponseMock := func(operationMockResponse map[string]interface{}) (map[string]interface{}, error) { + if len(operationMockResponse) > 0 { + mapMerge(ReadMockResponse, operationMockResponse) + } + return ReadMockResponse, nil + } + + // Create + patches := gomonkey.ApplyMethod(reflect.TypeOf(&connectivity.AliyunClient{}), "NewEcsClient", func(_ *connectivity.AliyunClient) (*client.Client, error) { + return nil, &tea.SDKError{ + Code: String("loadEndpoint error"), + Data: String("loadEndpoint error"), + Message: String("loadEndpoint error"), + StatusCode: tea.Int(400), + } + }) + err = resourceAliCloudEcsDiskEncryptionByDefaultCreate(dCreate, rawClient) + patches.Reset() + assert.NotNil(t, err) + ReadMockResponseDiff := map[string]interface{}{ + // DescribeDiskEncryptionByDefaultStatus Response + "Encrypted": true, + } + errorCodes := []string{"NonRetryableError", "Throttling", "nil"} + for index, errorCode := range errorCodes { + retryIndex := index - 1 // a counter used to cover retry scenario; -1 means no retry scenario; 0 means retry based on error code; 1 means retry based on status code + if errorCode == "nil" { + retryIndex = -1 + } + patches := gomonkey.ApplyMethod(reflect.TypeOf(&client.Client{}), "DoRequest", func(_ *client.Client, action *string, _ *string, _ *string, _ *string, _ *string, _ map[string]interface{}, _ map[string]interface{}, _ *util.RuntimeOptions) (map[string]interface{}, error) { + if retryIndex == 0 { + retryIndex++ + return failedResponseMock(errorCode) + } else if retryIndex == 1 { + retryIndex++ + return successResponseMock(CreateMockResponse) + } + return CreateMockResponse, nil + }) + err := resourceAliCloudEcsDiskEncryptionByDefaultCreate(dCreate, rawClient) + patches.Reset() + switch errorCode { + case "NonRetryableError": + assert.NotNil(t, err) + default: + assert.Nil(t, err) + dCompare, _ := schema.InternalMap(p["alicloud_ecs_disk_encryption_by_default"].Schema).Data(dCreate.State(), nil) + _ = dCompare.Set("enabled", attributes["enabled"]) + assert.Equal(t, dCompare.State().Attributes, dCreate.State().Attributes) + } + if retryIndex >= 0 { + patches.Reset() + } + } + + // Update + patches = gomonkey.ApplyMethod(reflect.TypeOf(&connectivity.AliyunClient{}), "NewEcsClient", func(_ *connectivity.AliyunClient) (*client.Client, error) { + return nil, &tea.SDKError{ + Code: String("loadEndpoint error"), + Data: String("loadEndpoint error"), + Message: String("loadEndpoint error"), + StatusCode: tea.Int(400), + } + }) + err = resourceAliCloudEcsDiskEncryptionByDefaultUpdate(d, rawClient) + patches.Reset() + assert.NotNil(t, err) + // DisableDiskEncryptionByDefault + attributesDiff := map[string]interface{}{ + "enabled": false, + } + diff, err := newInstanceDiff("alicloud_ecs_disk_encryption_by_default", attributes, attributesDiff, dCreate.State()) + if err != nil { + t.Error(err) + } + dExisted2Data, _ = schema.InternalMap(p["alicloud_ecs_disk_encryption_by_default"].Schema).Data(dCreate.State(), diff) + ReadMockResponseDiff = map[string]interface{}{ + // DescribeDiskEncryptionByDefaultStatus Response + "Encrypted": false, + } + errorCodes = []string{"NonRetryableError", "Throttling", "nil"} + for index, errorCode := range errorCodes { + retryIndex := index - 1 + if errorCode == "nil" { + retryIndex = -1 + } + patches := gomonkey.ApplyMethod(reflect.TypeOf(&client.Client{}), "DoRequest", func(_ *client.Client, action *string, _ *string, _ *string, _ *string, _ *string, _ map[string]interface{}, _ map[string]interface{}, _ *util.RuntimeOptions) (map[string]interface{}, error) { + if retryIndex == 0 { + retryIndex++ + return failedResponseMock(errorCode) + } else if retryIndex == 1 { + retryIndex++ + return successResponseMock(ReadMockResponseDiff) + } + return ReadMockResponse, nil + }) + err := resourceAliCloudEcsDiskEncryptionByDefaultUpdate(dExisted2Data, rawClient) + patches.Reset() + switch errorCode { + case "NonRetryableError": + assert.NotNil(t, err) + default: + assert.Nil(t, err) + dCompare, _ := schema.InternalMap(p["alicloud_ecs_disk_encryption_by_default"].Schema).Data(dExisted2Data.State(), nil) + _ = dCompare.Set("enabled", attributesDiff["enabled"]) + assert.Equal(t, dCompare.State().Attributes, dExisted2Data.State().Attributes) + } + if retryIndex >= 0 { + patches.Reset() + } + } + + // Read + errorCodes = []string{"NonRetryableError", "Throttling", "nil", "{}"} + for index, errorCode := range errorCodes { + retryIndex := index - 1 + if errorCode == "nil" { + retryIndex = -1 + } + patches := gomonkey.ApplyMethod(reflect.TypeOf(&client.Client{}), "DoRequest", func(_ *client.Client, action *string, _ *string, _ *string, _ *string, _ *string, _ map[string]interface{}, _ map[string]interface{}, _ *util.RuntimeOptions) (map[string]interface{}, error) { + if retryIndex == 0 { + retryIndex++ + return failedResponseMock(errorCode) + } else if retryIndex == 1 { + retryIndex++ + return successResponseMock(ReadMockResponse) + } + return ReadMockResponse, nil + }) + err := resourceAliCloudEcsDiskEncryptionByDefaultRead(d, rawClient) + patches.Reset() + switch errorCode { + case "NonRetryableError": + assert.NotNil(t, err) + case "{}": + assert.Nil(t, err) + } + } + + // Delete + patches = gomonkey.ApplyMethod(reflect.TypeOf(&connectivity.AliyunClient{}), "NewEcsClient", func(_ *connectivity.AliyunClient) (*client.Client, error) { + return nil, &tea.SDKError{ + Code: String("loadEndpoint error"), + Data: String("loadEndpoint error"), + Message: String("loadEndpoint error"), + StatusCode: tea.Int(400), + } + }) + err = resourceAliCloudEcsDiskEncryptionByDefaultDelete(d, rawClient) + patches.Reset() + assert.NotNil(t, err) + errorCodes = []string{"NonRetryableError", "Throttling", "nil"} + for index, errorCode := range errorCodes { + retryIndex := index - 1 + if errorCode == "nil" { + retryIndex = -1 + } + patches := gomonkey.ApplyMethod(reflect.TypeOf(&client.Client{}), "DoRequest", func(_ *client.Client, action *string, _ *string, _ *string, _ *string, _ *string, _ map[string]interface{}, _ map[string]interface{}, _ *util.RuntimeOptions) (map[string]interface{}, error) { + if retryIndex == 0 { + retryIndex++ + return failedResponseMock(errorCode) + } else if retryIndex == 1 { + retryIndex++ + return successResponseMock(ReadMockResponse) + } + return ReadMockResponse, nil + }) + err := resourceAliCloudEcsDiskEncryptionByDefaultDelete(d, rawClient) + patches.Reset() + switch errorCode { + case "NonRetryableError": + assert.NotNil(t, err) + default: + assert.Nil(t, err) + } + } +} + +var attributes = map[string]interface{}{ + "enabled": true, +} diff --git a/alicloud/service_alicloud_ecs_v2.go b/alicloud/service_alicloud_ecs_v2.go index 0888875503ce..0e02c5b6d0c7 100644 --- a/alicloud/service_alicloud_ecs_v2.go +++ b/alicloud/service_alicloud_ecs_v2.go @@ -965,3 +965,74 @@ func (s *EcsServiceV2) EcsAutoSnapshotPolicyAttachmentStateRefreshFuncWithApi(id } // DescribeEcsAutoSnapshotPolicyAttachment >>> Encapsulated. + +// DescribeEcsDiskDefaultKMSKeyId <<< Encapsulated get interface for Ecs Disk Default KMS Key ID. + +func (s *EcsServiceV2) DescribeEcsDiskDefaultKMSKeyId(id string) (object map[string]interface{}, err error) { + client := s.client + var request map[string]interface{} + var response map[string]interface{} + var query map[string]interface{} + action := "DescribeDiskDefaultKMSKeyId" + request = make(map[string]interface{}) + query = make(map[string]interface{}) + request["RegionId"] = id // Use region ID as the identifier + + wait := incrementalWait(3*time.Second, 5*time.Second) + err = resource.Retry(1*time.Minute, func() *resource.RetryError { + response, err = client.RpcPost("Ecs", "2014-05-26", action, query, request, true) + + if err != nil { + if NeedRetry(err) { + wait() + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + addDebug(action, response, request) + if err != nil { + return object, WrapErrorf(err, DefaultErrorMsg, id, action, AlibabaCloudSdkGoERROR) + } + + return response, nil +} + +// DescribeEcsDiskDefaultKMSKeyId >>> Encapsulated. + +// DescribeEcsDiskEncryptionByDefaultStatus <<< Encapsulated get interface for Ecs Disk Encryption By Default Status. + +func (s *EcsServiceV2) DescribeEcsDiskEncryptionByDefaultStatus(id string) (object map[string]interface{}, err error) { + client := s.client + var request map[string]interface{} + var response map[string]interface{} + var query map[string]interface{} + action := "DescribeDiskEncryptionByDefaultStatus" + request = make(map[string]interface{}) + query = make(map[string]interface{}) + request["RegionId"] = id // Use region ID as the identifier + + wait := incrementalWait(3*time.Second, 5*time.Second) + err = resource.Retry(1*time.Minute, func() *resource.RetryError { + response, err = client.RpcPost("Ecs", "2014-05-26", action, query, request, true) + + if err != nil { + if NeedRetry(err) { + wait() + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + addDebug(action, response, request) + if err != nil { + return object, WrapErrorf(err, DefaultErrorMsg, id, action, AlibabaCloudSdkGoERROR) + } + + // Return the response normally regardless of encryption status + return response, nil +} + +// DescribeEcsDiskEncryptionByDefaultStatus >>> Encapsulated. diff --git a/website/docs/r/ecs_disk_default_kms_key_id.html.markdown b/website/docs/r/ecs_disk_default_kms_key_id.html.markdown new file mode 100644 index 000000000000..e8e15d93312c --- /dev/null +++ b/website/docs/r/ecs_disk_default_kms_key_id.html.markdown @@ -0,0 +1,108 @@ +--- +subcategory: "ECS" +layout: "alicloud" +page_title: "Alicloud: alicloud_ecs_disk_default_kms_key_id" +description: |- + Provides a Alicloud ECS Disk Default KMS Key ID resource. +--- + +# alicloud_ecs_disk_default_kms_key_id + +Provides an ECS Disk Default KMS Key ID resource to configure the default KMS key used for account-level disk encryption. + +For information about ECS Disk Default KMS Key ID and how to use it, see [What is Disk Default KMS Key ID](https://www.alibabacloud.com/help/en/doc-detail/59643.htm). + +-> **NOTE:** Available since v1.274.0. + +-> **NOTE:** This resource manages the default KMS key for account-level disk encryption in the current region. + +-> **NOTE:** You need to enable ECS disk encryption by default first using `alicloud_ecs_disk_encryption_by_default`. + +-> **NOTE:** You need to have KMS (Key Management Service) enabled and appropriate permissions configured. + +## Example Usage + +Basic Usage + +```terraform +# Use existing VPC and VSwitch +data "alicloud_vpcs" "default" { + name_regex = "^default-NODELETING$" +} + +data "alicloud_vswitches" "default" { + vpc_id = data.alicloud_vpcs.default.ids.0 + zone_id = "cn-hangzhou-i" +} + +data "alicloud_vswitches" "default2" { + vpc_id = data.alicloud_vpcs.default.ids.0 + zone_id = "cn-hangzhou-j" +} + +# Create KMS instance +resource "alicloud_kms_instance" "example" { + product_version = "3" + vpc_id = data.alicloud_vpcs.default.ids.0 + zone_ids = [ + "cn-hangzhou-i", + "cn-hangzhou-j" + ] + vswitch_ids = [ + data.alicloud_vswitches.default.ids.0, + data.alicloud_vswitches.default2.ids.0 + ] + vpc_num = "1" + key_num = "1000" + secret_num = "100" + spec = "1000" + payment_type = "PayAsYouGo" + + timeouts { + delete = "20m" + } +} + +# Create a KMS key in the instance +resource "alicloud_kms_key" "example" { + description = "KMS key for ECS disk encryption" + pending_window_in_days = 7 + key_usage = "ENCRYPT/DECRYPT" + key_spec = "Aliyun_AES_256" + dkms_instance_id = alicloud_kms_instance.example.id + + timeouts { + delete = "20m" + } +} + +# Enable ECS disk encryption by default first +resource "alicloud_ecs_disk_encryption_by_default" "example" { + enabled = true +} + +# Configure the default KMS key for disk encryption +resource "alicloud_ecs_disk_default_kms_key_id" "example" { + kms_key_id = alicloud_kms_key.example.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `kms_key_id` - (Required) The KMS key ID used for ECS disk encryption by default. You can use the KMS key ID or the alias. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The resource ID. The value is the region ID where the resource is located. + +## Import + +ECS Disk Default KMS Key ID can be imported using the region id, e.g. + +```shell +$ terraform import alicloud_ecs_disk_default_kms_key_id.example cn-hangzhou +``` diff --git a/website/docs/r/ecs_disk_encryption_by_default.html.markdown b/website/docs/r/ecs_disk_encryption_by_default.html.markdown new file mode 100644 index 000000000000..621d4f38907c --- /dev/null +++ b/website/docs/r/ecs_disk_encryption_by_default.html.markdown @@ -0,0 +1,50 @@ +--- +subcategory: "ECS" +layout: "alicloud" +page_title: "Alicloud: alicloud_ecs_disk_encryption_by_default" +description: |- + Provides a Alicloud ECS Disk Encryption By Default resource. +--- + +# alicloud_ecs_disk_encryption_by_default + +Provides an ECS Disk Encryption By Default resource to enable or disable account-level default disk encryption. + +For information about ECS Disk Encryption By Default and how to use it, see [What is Disk Encryption By Default](https://www.alibabacloud.com/help/en/doc-detail/59643.htm). + +-> **NOTE:** Available since v1.274.0. + +-> **NOTE:** This resource manages account-level disk encryption settings for the current region. Once enabled, all new disks created in the region will be encrypted by default. + +-> **NOTE:** You need to have KMS (Key Management Service) enabled before using this resource. + +## Example Usage + +Basic Usage + +```terraform +# Enable ECS disk encryption by default +resource "alicloud_ecs_disk_encryption_by_default" "default" { + enabled = true +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enabled` - (Optional) Whether to enable ECS disk encryption by default. Default value: `false`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The resource ID. The value is the region ID where the resource is located. + +## Import + +ECS Disk Encryption By Default can be imported using the region id, e.g. + +```shell +$ terraform import alicloud_ecs_disk_encryption_by_default.example cn-hangzhou +``` \ No newline at end of file