diff --git a/internal/services/storage/parse/storage_container_immutability_policy.go b/internal/services/storage/parse/storage_container_immutability_policy.go new file mode 100644 index 0000000000000..78c6126924c21 --- /dev/null +++ b/internal/services/storage/parse/storage_container_immutability_policy.go @@ -0,0 +1,90 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +type StorageContainerImmutabilityPolicyId struct { + SubscriptionId string + ResourceGroup string + StorageAccountName string + BlobServiceName string + ContainerName string + ImmutabilityPolicyName string +} + +func NewStorageContainerImmutabilityPolicyID(subscriptionId, resourceGroup, storageAccountName, blobServiceName, containerName, immutabilityPolicyName string) StorageContainerImmutabilityPolicyId { + return StorageContainerImmutabilityPolicyId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + StorageAccountName: storageAccountName, + BlobServiceName: blobServiceName, + ContainerName: containerName, + ImmutabilityPolicyName: immutabilityPolicyName, + } +} + +func (id StorageContainerImmutabilityPolicyId) String() string { + segments := []string{ + fmt.Sprintf("Immutability Policy Name %q", id.ImmutabilityPolicyName), + fmt.Sprintf("Container Name %q", id.ContainerName), + fmt.Sprintf("Blob Service Name %q", id.BlobServiceName), + fmt.Sprintf("Storage Account Name %q", id.StorageAccountName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Storage Container Immutability Policy", segmentsStr) +} + +func (id StorageContainerImmutabilityPolicyId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts/%s/blobServices/%s/containers/%s/immutabilityPolicies/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.StorageAccountName, id.BlobServiceName, id.ContainerName, id.ImmutabilityPolicyName) +} + +// StorageContainerImmutabilityPolicyID parses a StorageContainerImmutabilityPolicy ID into an StorageContainerImmutabilityPolicyId struct +func StorageContainerImmutabilityPolicyID(input string) (*StorageContainerImmutabilityPolicyId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("parsing %q as an StorageContainerImmutabilityPolicy ID: %+v", input, err) + } + + resourceId := StorageContainerImmutabilityPolicyId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.StorageAccountName, err = id.PopSegment("storageAccounts"); err != nil { + return nil, err + } + if resourceId.BlobServiceName, err = id.PopSegment("blobServices"); err != nil { + return nil, err + } + if resourceId.ContainerName, err = id.PopSegment("containers"); err != nil { + return nil, err + } + if resourceId.ImmutabilityPolicyName, err = id.PopSegment("immutabilityPolicies"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/internal/services/storage/parse/storage_container_immutability_policy_test.go b/internal/services/storage/parse/storage_container_immutability_policy_test.go new file mode 100644 index 0000000000000..209f3694b40d0 --- /dev/null +++ b/internal/services/storage/parse/storage_container_immutability_policy_test.go @@ -0,0 +1,163 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +var _ resourceids.Id = StorageContainerImmutabilityPolicyId{} + +func TestStorageContainerImmutabilityPolicyIDFormatter(t *testing.T) { + actual := NewStorageContainerImmutabilityPolicyID("12345678-1234-9876-4563-123456789012", "resGroup1", "storageAccount1", "default", "container1", "default").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/default/containers/container1/immutabilityPolicies/default" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestStorageContainerImmutabilityPolicyID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *StorageContainerImmutabilityPolicyId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing StorageAccountName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/", + Error: true, + }, + + { + // missing value for StorageAccountName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/", + Error: true, + }, + + { + // missing BlobServiceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/", + Error: true, + }, + + { + // missing value for BlobServiceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/", + Error: true, + }, + + { + // missing ContainerName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/default/", + Error: true, + }, + + { + // missing value for ContainerName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/default/containers/", + Error: true, + }, + + { + // missing ImmutabilityPolicyName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/default/containers/container1/", + Error: true, + }, + + { + // missing value for ImmutabilityPolicyName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/default/containers/container1/immutabilityPolicies/default", + Expected: &StorageContainerImmutabilityPolicyId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + StorageAccountName: "storageAccount1", + BlobServiceName: "default", + ContainerName: "container1", + ImmutabilityPolicyName: "default", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.STORAGE/STORAGEACCOUNTS/STORAGEACCOUNT1/BLOBSERVICES/DEFAULT/CONTAINERS/CONTAINER1/IMMUTABILITYPOLICIES/DEFAULT", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := StorageContainerImmutabilityPolicyID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.StorageAccountName != v.Expected.StorageAccountName { + t.Fatalf("Expected %q but got %q for StorageAccountName", v.Expected.StorageAccountName, actual.StorageAccountName) + } + if actual.BlobServiceName != v.Expected.BlobServiceName { + t.Fatalf("Expected %q but got %q for BlobServiceName", v.Expected.BlobServiceName, actual.BlobServiceName) + } + if actual.ContainerName != v.Expected.ContainerName { + t.Fatalf("Expected %q but got %q for ContainerName", v.Expected.ContainerName, actual.ContainerName) + } + if actual.ImmutabilityPolicyName != v.Expected.ImmutabilityPolicyName { + t.Fatalf("Expected %q but got %q for ImmutabilityPolicyName", v.Expected.ImmutabilityPolicyName, actual.ImmutabilityPolicyName) + } + } +} diff --git a/internal/services/storage/registration.go b/internal/services/storage/registration.go index 176faafc272e3..5bcb04e0d142f 100644 --- a/internal/services/storage/registration.go +++ b/internal/services/storage/registration.go @@ -81,5 +81,6 @@ func (r Registration) DataSources() []sdk.DataSource { func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ LocalUserResource{}, + StorageContainerImmutabilityPolicyResource{}, } } diff --git a/internal/services/storage/resourceids.go b/internal/services/storage/resourceids.go index 140efc6913daf..1007377350e82 100644 --- a/internal/services/storage/resourceids.go +++ b/internal/services/storage/resourceids.go @@ -7,3 +7,4 @@ package storage //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=StorageQueueResourceManager -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/queueServices/default/queues/queue1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=StorageShareResourceManager -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/fileServices/fileService1/fileshares/share1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=StorageAccountManagementPolicy -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/managementPolicies/policy1 +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=StorageContainerImmutabilityPolicy -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/default/containers/container1/immutabilityPolicies/default diff --git a/internal/services/storage/storage_container_immutability_policy_resource.go b/internal/services/storage/storage_container_immutability_policy_resource.go new file mode 100644 index 0000000000000..b05819b284628 --- /dev/null +++ b/internal/services/storage/storage_container_immutability_policy_resource.go @@ -0,0 +1,354 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package storage + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/storage/2023-01-01/blobcontainers" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/storage/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/storage/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" +) + +type StorageContainerImmutabilityPolicyResource struct{} + +var _ sdk.ResourceWithUpdate = StorageContainerImmutabilityPolicyResource{} + +type ContainerImmutabilityPolicyModel struct { + StorageContainerResourceManagerId string `tfschema:"storage_container_resource_manager_id"` + ImmutabilityPeriodInDays int `tfschema:"immutability_period_in_days"` + Locked bool `tfschema:"locked"` + ProtectedAppendWritesAllEnabled bool `tfschema:"protected_append_writes_all_enabled"` + ProtectedAppendWritesEnabled bool `tfschema:"protected_append_writes_enabled"` +} + +func (r StorageContainerImmutabilityPolicyResource) ResourceType() string { + return "azurerm_storage_container_immutability_policy" +} + +func (r StorageContainerImmutabilityPolicyResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.StorageContainerImmutabilityPolicyID +} + +func (r StorageContainerImmutabilityPolicyResource) ModelObject() interface{} { + return &ContainerImmutabilityPolicyModel{} +} + +func (r StorageContainerImmutabilityPolicyResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "storage_container_resource_manager_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: commonids.ValidateStorageContainerID, + }, + + "immutability_period_in_days": { + Type: pluginsdk.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 146000), + }, + + "locked": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + + "protected_append_writes_all_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + + "protected_append_writes_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + } +} + +func (r StorageContainerImmutabilityPolicyResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (r StorageContainerImmutabilityPolicyResource) CustomizeDiff() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + diff := metadata.ResourceDiff + + protectedAppendWritesAllEnabled := diff.Get("protected_append_writes_all_enabled").(bool) + protectedAppendWritesEnabled := diff.Get("protected_append_writes_enabled").(bool) + + if protectedAppendWritesAllEnabled && protectedAppendWritesEnabled { + return fmt.Errorf("`protected_append_writes_all_enabled` and `protected_append_writes_enabled` cannot be set at the same time") + } + + lockedOld, lockedNew := diff.GetChange("locked") + + if lockedOld.(bool) && !lockedNew.(bool) { + return fmt.Errorf("unable to set `locked = false` - once an immutability policy locked it cannot be unlocked") + } + + if lockedOld.(bool) { + if diff.HasChange("immutability_period_in_days") { + if periodOld, periodNew := diff.GetChange("immutability_period_in_days"); periodOld.(int) < periodNew.(int) { + return fmt.Errorf("`immutability_period_in_days` cannot be decreased once an immutability policy has been locked") + } + } + + if diff.HasChange("protected_append_writes_all_enabled") { + return fmt.Errorf("`protected_append_writes_all_enabled` cannot be changed once an immutability policy has been locked") + } + + if diff.HasChange("protected_append_writes_enabled") { + return fmt.Errorf("`protected_append_writes_enabled` cannot be changed once an immutability policy has been locked") + } + } + + return nil + }, + } +} + +func (r StorageContainerImmutabilityPolicyResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 10 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Storage.ResourceManager.BlobContainers + + var model ContainerImmutabilityPolicyModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding %+v", err) + } + + containerId, err := commonids.ParseStorageContainerID(model.StorageContainerResourceManagerId) + if err != nil { + return err + } + + id := parse.NewStorageContainerImmutabilityPolicyID(containerId.SubscriptionId, containerId.ResourceGroupName, containerId.StorageAccountName, "default", containerId.ContainerName, "default") + + existing, err := client.GetImmutabilityPolicy(ctx, *containerId, blobcontainers.DefaultGetImmutabilityPolicyOperationOptions()) + if err != nil { + if !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for presence of existing %s: %+v", id, err) + } + } + if !response.WasNotFound(existing.HttpResponse) && !r.isDeleted(existing.Model) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + input := blobcontainers.ImmutabilityPolicy{ + Properties: blobcontainers.ImmutabilityPolicyProperty{ + AllowProtectedAppendWrites: pointer.To(model.ProtectedAppendWritesEnabled), + AllowProtectedAppendWritesAll: pointer.To(model.ProtectedAppendWritesAllEnabled), + ImmutabilityPeriodSinceCreationInDays: pointer.To(int64(model.ImmutabilityPeriodInDays)), + }, + } + + resp, err := client.CreateOrUpdateImmutabilityPolicy(ctx, *containerId, input, blobcontainers.DefaultCreateOrUpdateImmutabilityPolicyOperationOptions()) + if err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + + // Lock the policy if requested - note that this is a one-way operation that prevents subsequent changes or + // deletion to the policy, the container it applies to, and the storage account where it resides. + if model.Locked { + if resp.Model == nil { + return fmt.Errorf("preparing to lock %s: model was nil", id) + } + + options := blobcontainers.LockImmutabilityPolicyOperationOptions{ + IfMatch: resp.Model.Etag, + } + + if _, err = client.LockImmutabilityPolicy(ctx, *containerId, options); err != nil { + return fmt.Errorf("locking %s: %+v", id, err) + } + } + + return nil + }, + } +} + +func (r StorageContainerImmutabilityPolicyResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 10 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Storage.ResourceManager.BlobContainers + + id, err := parse.StorageContainerImmutabilityPolicyID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + var model ContainerImmutabilityPolicyModel + if err = metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding %+v", err) + } + + containerId, err := commonids.ParseStorageContainerID(model.StorageContainerResourceManagerId) + if err != nil { + return err + } + + resp, err := client.GetImmutabilityPolicy(ctx, *containerId, blobcontainers.DefaultGetImmutabilityPolicyOperationOptions()) + if err != nil { + if response.WasNotFound(resp.HttpResponse) || r.isDeleted(resp.Model) { + return nil + } + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + if resp.Model == nil { + return fmt.Errorf("retrieving %s: model was nil", id) + } + + input := blobcontainers.ImmutabilityPolicy{ + Properties: blobcontainers.ImmutabilityPolicyProperty{ + AllowProtectedAppendWrites: pointer.To(model.ProtectedAppendWritesEnabled), + AllowProtectedAppendWritesAll: pointer.To(model.ProtectedAppendWritesAllEnabled), + ImmutabilityPeriodSinceCreationInDays: pointer.To(int64(model.ImmutabilityPeriodInDays)), + }, + } + + options := blobcontainers.CreateOrUpdateImmutabilityPolicyOperationOptions{ + IfMatch: resp.Model.Etag, + } + + updateResp, err := client.CreateOrUpdateImmutabilityPolicy(ctx, *containerId, input, options) + if err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + + // Lock the policy if requested - note that this is a one-way operation that prevents subsequent changes or + // deletion to the policy, the container it applies to, and the storage account where it resides. + if model.Locked { + if updateResp.Model == nil { + return fmt.Errorf("preparing to lock %s: model was nil", id) + } + + lockOptions := blobcontainers.LockImmutabilityPolicyOperationOptions{ + IfMatch: updateResp.Model.Etag, + } + + if _, err = client.LockImmutabilityPolicy(ctx, *containerId, lockOptions); err != nil { + return fmt.Errorf("locking %s: %+v", id, err) + } + } + + return nil + }, + } +} + +func (r StorageContainerImmutabilityPolicyResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Storage.ResourceManager.BlobContainers + + id, err := parse.StorageContainerImmutabilityPolicyID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + containerId := commonids.NewStorageContainerID(id.SubscriptionId, id.ResourceGroup, id.StorageAccountName, id.ContainerName) + + resp, err := client.GetImmutabilityPolicy(ctx, containerId, blobcontainers.DefaultGetImmutabilityPolicyOperationOptions()) + if err != nil { + if response.WasNotFound(resp.HttpResponse) || r.isDeleted(resp.Model) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + state := ContainerImmutabilityPolicyModel{ + StorageContainerResourceManagerId: containerId.ID(), + } + + if resp.Model != nil { + props := resp.Model.Properties + if props.AllowProtectedAppendWrites != nil { + state.ProtectedAppendWritesEnabled = *props.AllowProtectedAppendWrites + } + if props.AllowProtectedAppendWritesAll != nil { + state.ProtectedAppendWritesAllEnabled = *props.AllowProtectedAppendWritesAll + } + if props.ImmutabilityPeriodSinceCreationInDays != nil { + state.ImmutabilityPeriodInDays = int(*props.ImmutabilityPeriodSinceCreationInDays) + } + if props.State != nil { + state.Locked = *props.State == blobcontainers.ImmutabilityPolicyStateLocked + } + } + + return metadata.Encode(&state) + }, + } +} + +func (r StorageContainerImmutabilityPolicyResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 10 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Storage.ResourceManager.BlobContainers + + id, err := parse.StorageContainerImmutabilityPolicyID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + containerId := commonids.NewStorageContainerID(id.SubscriptionId, id.ResourceGroup, id.StorageAccountName, id.ContainerName) + + resp, err := client.GetImmutabilityPolicy(ctx, containerId, blobcontainers.DefaultGetImmutabilityPolicyOperationOptions()) + if err != nil { + if response.WasNotFound(resp.HttpResponse) || r.isDeleted(resp.Model) { + return nil + } + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + if resp.Model == nil { + return fmt.Errorf("retrieving %s: model was nil", id) + } + + options := blobcontainers.DeleteImmutabilityPolicyOperationOptions{ + IfMatch: resp.Model.Etag, + } + + if _, err := client.DeleteImmutabilityPolicy(ctx, containerId, options); err != nil { + return fmt.Errorf("deleting %s: %+v", id, err) + } + + return nil + }, + } +} + +func (r StorageContainerImmutabilityPolicyResource) isDeleted(input *blobcontainers.ImmutabilityPolicy) bool { + if input == nil { + return true + } + if input.Properties.State != nil && strings.EqualFold(string(*input.Properties.State), "Deleted") { + return true + } + return false +} diff --git a/internal/services/storage/storage_container_immutability_policy_resource_test.go b/internal/services/storage/storage_container_immutability_policy_resource_test.go new file mode 100644 index 0000000000000..5d160d9eecbb7 --- /dev/null +++ b/internal/services/storage/storage_container_immutability_policy_resource_test.go @@ -0,0 +1,198 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package storage_test + +import ( + "context" + "fmt" + "regexp" + "strings" + "testing" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/storage/2023-01-01/blobcontainers" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/storage/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type StorageContainerImmutabilityPolicyResource struct{} + +func TestAccStorageContainerImmutabilityPolicy_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_container_immutability_policy", "test") + r := StorageContainerImmutabilityPolicyResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccStorageContainerImmutabilityPolicy_completeUnlocked(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_container_immutability_policy", "test") + r := StorageContainerImmutabilityPolicyResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.completeUnlocked(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccStorageContainerImmutabilityPolicy_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_container_immutability_policy", "test") + r := StorageContainerImmutabilityPolicyResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.completeUnlocked(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccStorageContainerImmutabilityPolicy_completeLocked(t *testing.T) { + // This test has been written for manual testing of the `locked` property. Ordinarily we do not want to test this in automation, + // since locking an immutability policy renders the container and its storage account **immutable**. This test will always + // fail during cleanup for this reason. Uncomment the t.Skip() call to continue... + t.Skip("this test for manual execution only") + + data := acceptance.BuildTestData(t, "azurerm_storage_container_immutability_policy", "test") + r := StorageContainerImmutabilityPolicyResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.completeUnlocked(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.completeLocked(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + ExpectError: regexp.MustCompile("unable to set `locked = false` - once an immutability policy locked it cannot be unlocked"), + }, + }) +} + +func (r StorageContainerImmutabilityPolicyResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.StorageContainerImmutabilityPolicyID(state.ID) + if err != nil { + return nil, err + } + + containerId := commonids.NewStorageContainerID(id.SubscriptionId, id.ResourceGroup, id.StorageAccountName, id.ContainerName) + + resp, err := client.Storage.ResourceManager.BlobContainers.GetImmutabilityPolicy(ctx, containerId, blobcontainers.DefaultGetImmutabilityPolicyOperationOptions()) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %+v", id, err) + } + + return utils.Bool(resp.Model != nil && resp.Model.Properties.State != nil && !strings.EqualFold(string(*resp.Model.Properties.State), "Deleted")), nil +} + +func (r StorageContainerImmutabilityPolicyResource) basic(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_storage_container_immutability_policy" "test" { + storage_container_resource_manager_id = azurerm_storage_container.test.resource_manager_id + immutability_period_in_days = 1 +} +`, template) +} + +func (r StorageContainerImmutabilityPolicyResource) completeUnlocked(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_storage_container_immutability_policy" "test" { + storage_container_resource_manager_id = azurerm_storage_container.test.resource_manager_id + immutability_period_in_days = 2 + protected_append_writes_all_enabled = false + protected_append_writes_enabled = true +} +`, template) +} + +func (r StorageContainerImmutabilityPolicyResource) completeLocked(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_storage_container_immutability_policy" "test" { + storage_container_resource_manager_id = azurerm_storage_container.test.resource_manager_id + immutability_period_in_days = 2 + protected_append_writes_all_enabled = true + protected_append_writes_enabled = false + + locked = true +} +`, template) +} + +func (r StorageContainerImmutabilityPolicyResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestacc%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "retention" + storage_account_name = azurerm_storage_account.test.name + container_access_type = "private" +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} diff --git a/internal/services/storage/validate/storage_container_immutability_policy_id.go b/internal/services/storage/validate/storage_container_immutability_policy_id.go new file mode 100644 index 0000000000000..abc27dcf935bf --- /dev/null +++ b/internal/services/storage/validate/storage_container_immutability_policy_id.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/storage/parse" +) + +func StorageContainerImmutabilityPolicyID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.StorageContainerImmutabilityPolicyID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/storage/validate/storage_container_immutability_policy_id_test.go b/internal/services/storage/validate/storage_container_immutability_policy_id_test.go new file mode 100644 index 0000000000000..3085cef4a6a32 --- /dev/null +++ b/internal/services/storage/validate/storage_container_immutability_policy_id_test.go @@ -0,0 +1,115 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestStorageContainerImmutabilityPolicyID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing StorageAccountName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/", + Valid: false, + }, + + { + // missing value for StorageAccountName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/", + Valid: false, + }, + + { + // missing BlobServiceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/", + Valid: false, + }, + + { + // missing value for BlobServiceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/", + Valid: false, + }, + + { + // missing ContainerName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/default/", + Valid: false, + }, + + { + // missing value for ContainerName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/default/containers/", + Valid: false, + }, + + { + // missing ImmutabilityPolicyName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/default/containers/container1/", + Valid: false, + }, + + { + // missing value for ImmutabilityPolicyName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Storage/storageAccounts/storageAccount1/blobServices/default/containers/container1/immutabilityPolicies/default", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.STORAGE/STORAGEACCOUNTS/STORAGEACCOUNT1/BLOBSERVICES/DEFAULT/CONTAINERS/CONTAINER1/IMMUTABILITYPOLICIES/DEFAULT", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := StorageContainerImmutabilityPolicyID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/website/docs/r/storage_container_immutability_policy.html.markdown b/website/docs/r/storage_container_immutability_policy.html.markdown new file mode 100644 index 0000000000000..cc404c19763c1 --- /dev/null +++ b/website/docs/r/storage_container_immutability_policy.html.markdown @@ -0,0 +1,82 @@ +--- +subcategory: "Storage" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_storage_container" +description: |- + Manages a Container within an Azure Storage Account. +--- + +# azurerm_storage_container_immutability_policy + +Manages an Immutability Policy for a Container within an Azure Storage Account. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_storage_account" "example" { + name = "examplestoraccount" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + account_tier = "Standard" + account_replication_type = "LRS" + + tags = { + environment = "staging" + } +} + +resource "azurerm_storage_container" "example" { + name = "example" + storage_account_name = azurerm_storage_account.example.name + container_access_type = "private" +} + +resource "azurerm_storage_container_immutability_policy" "example" { + storage_container_resource_manager_id = azurerm_storage_container.example.resource_manager_id + immutability_period_in_days = 14 + protected_append_writes_all_enabled = false + protected_append_writes_enabled = true +} +``` + +## Argument Reference + +The following arguments are supported: + +* `storage_container_resource_manager_id` - (Required) The Resource Manager ID of the Storage Container where this Immutability Policy should be applied. Changing this forces a new resource to be created. + +* `immutability_period_in_days` - (Required) The time interval in days that the data needs to be kept in a non-erasable and non-modifiable state. + +* `locked` - (Optional) Whether to lock this immutability policy. Cannot be set to `false` once the policy has been locked. + +!> **Locking an Immutability Policy** Once an Immutability Policy has been locked, it cannot be unlocked. After locking, it will only be possible to increase the value for `retention_period_in_days` up to 5 times for the lifetime of the policy. No other properties will be updateable. Furthermore, the Storage Container and the Storage Account in which it resides will become protected by the policy. It will no longer be possible to delete the Storage Container or the Storage Account. Please refer to [official documentation](https://learn.microsoft.com/en-us/azure/storage/blobs/immutable-policy-configure-container-scope?tabs=azure-portal#lock-a-time-based-retention-policy) for more information. + +* `protected_append_writes_all_enabled` - (Optional) Whether to allow protected append writes to block and append blobs to the container. Defaults to `false`. Cannot be set with `protected_append_writes_enabled`. + +* `protected_append_writes_enabled` - (Optional) Whether to allow protected append writes to append blobs to the container. Defaults to `false`. Cannot be set with `protected_append_writes_all_enabled`. + +## Attributes Reference + +No additional attributes are exported. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 10 minutes) Used when creating the Storage Container Immutability Policy. +* `update` - (Defaults to 10 minutes) Used when updating the Storage Container Immutability Policy. +* `read` - (Defaults to 5 minutes) Used when retrieving the Storage Container Immutability Policy. +* `delete` - (Defaults to 10 minutes) Used when deleting the Storage Container Immutability Policy. + +## Import + +Storage Container Immutability Policies can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_storage_container_immutability_policy.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/myaccount/blobServices/default/containers/mycontainer/immutabilityPolicies/default +```