diff --git a/azurerm/internal/services/containers/container_registry_resource.go b/azurerm/internal/services/containers/container_registry_resource.go index 4f23bb8b22a8..564c40bac4b4 100644 --- a/azurerm/internal/services/containers/container_registry_resource.go +++ b/azurerm/internal/services/containers/container_registry_resource.go @@ -95,7 +95,9 @@ func resourceContainerRegistry() *pluginsdk.Resource { }, "georeplications": { - Type: pluginsdk.TypeSet, + // Don't make this a TypeSet since TypeSet has bugs when there is a nested property using `StateFunc`. + // See: https://github.com/hashicorp/terraform-plugin-sdk/issues/160 + Type: pluginsdk.TypeList, Optional: true, Computed: true, // TODO -- remove this when deprecation resolves ConflictsWith: []string{"georeplication_locations"}, @@ -110,6 +112,12 @@ func resourceContainerRegistry() *pluginsdk.Resource { DiffSuppressFunc: location.DiffSuppressFunc, }, + "zone_redundancy_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + "tags": tags.Schema(), }, }, @@ -314,14 +322,21 @@ func resourceContainerRegistry() *pluginsdk.Resource { }, }, + "zone_redundancy_enabled": { + Type: pluginsdk.TypeBool, + ForceNew: true, + Optional: true, + Default: false, + }, + "tags": tags.Schema(), }, CustomizeDiff: pluginsdk.CustomizeDiffShim(func(ctx context.Context, d *pluginsdk.ResourceDiff, v interface{}) error { sku := d.Get("sku").(string) geoReplicationLocations := d.Get("georeplication_locations").(*pluginsdk.Set) - geoReplications := d.Get("georeplications").(*pluginsdk.Set) - hasGeoReplicationsApplied := geoReplicationLocations.Len() > 0 || geoReplications.Len() > 0 + geoReplications := d.Get("georeplications").([]interface{}) + hasGeoReplicationsApplied := geoReplicationLocations.Len() > 0 || len(geoReplications) > 0 // if locations have been specified for geo-replication then, the SKU has to be Premium if hasGeoReplicationsApplied && !strings.EqualFold(sku, string(containerregistry.Premium)) { return fmt.Errorf("ACR geo-replication can only be applied when using the Premium Sku.") @@ -346,6 +361,20 @@ func resourceContainerRegistry() *pluginsdk.Resource { if ok && encryptionEnabled.(bool) && !strings.EqualFold(sku, string(containerregistry.Premium)) { return fmt.Errorf("ACR encryption can only be applied when using the Premium Sku.") } + + // zone redundancy is only available for Premium Sku. + zoneRedundancyEnabled, ok := d.GetOk("zone_redundancy_enabled") + if ok && zoneRedundancyEnabled.(bool) && !strings.EqualFold(sku, string(containerregistry.Premium)) { + return fmt.Errorf("ACR zone redundancy can only be applied when using the Premium Sku") + } + for _, loc := range geoReplications { + loc := loc.(map[string]interface{}) + zoneRedundancyEnabled, ok := loc["zone_redundancy_enabled"] + if ok && zoneRedundancyEnabled.(bool) && !strings.EqualFold(sku, string(containerregistry.Premium)) { + return fmt.Errorf("ACR zone redundancy can only be applied when using the Premium Sku") + } + } + return nil }), } @@ -391,7 +420,7 @@ func resourceContainerRegistryCreate(d *pluginsdk.ResourceData, meta interface{} adminUserEnabled := d.Get("admin_enabled").(bool) t := d.Get("tags").(map[string]interface{}) geoReplicationLocations := d.Get("georeplication_locations").(*pluginsdk.Set) - geoReplications := d.Get("georeplications").(*pluginsdk.Set) + geoReplications := d.Get("georeplications").([]interface{}) networkRuleSet := expandNetworkRuleSet(d.Get("network_rule_set").([]interface{})) if networkRuleSet != nil && !strings.EqualFold(sku, string(containerregistry.Premium)) { @@ -416,6 +445,12 @@ func resourceContainerRegistryCreate(d *pluginsdk.ResourceData, meta interface{} if !d.Get("public_network_access_enabled").(bool) { publicNetworkAccess = containerregistry.PublicNetworkAccessDisabled } + + zoneRedundancy := containerregistry.ZoneRedundancyDisabled + if d.Get("zone_redundancy_enabled").(bool) { + zoneRedundancy = containerregistry.ZoneRedundancyEnabled + } + parameters := containerregistry.Registry{ Location: &location, Sku: &containerregistry.Sku{ @@ -425,6 +460,7 @@ func resourceContainerRegistryCreate(d *pluginsdk.ResourceData, meta interface{} Identity: identity, RegistryProperties: &containerregistry.RegistryProperties{ AdminUserEnabled: utils.Bool(adminUserEnabled), + Encryption: encryption, NetworkRuleSet: networkRuleSet, Policies: &containerregistry.Policies{ QuarantinePolicy: quarantinePolicy, @@ -432,7 +468,7 @@ func resourceContainerRegistryCreate(d *pluginsdk.ResourceData, meta interface{} TrustPolicy: trustPolicy, }, PublicNetworkAccess: publicNetworkAccess, - Encryption: encryption, + ZoneRedundancy: zoneRedundancy, }, Tags: tags.Expand(t), @@ -460,11 +496,11 @@ func resourceContainerRegistryCreate(d *pluginsdk.ResourceData, meta interface{} } // the ACR is being created so no previous geo-replication locations - var oldGeoReplicationLocations, newGeoReplicationLocations []*containerregistry.Replication + var oldGeoReplicationLocations, newGeoReplicationLocations []containerregistry.Replication if geoReplicationLocations != nil && geoReplicationLocations.Len() > 0 { newGeoReplicationLocations = expandReplicationsFromLocations(geoReplicationLocations.List()) } else { - newGeoReplicationLocations = expandReplications(geoReplications.List()) + newGeoReplicationLocations = expandReplications(geoReplications) } // geo replications have been specified if len(newGeoReplicationLocations) > 0 { @@ -513,8 +549,8 @@ func resourceContainerRegistryUpdate(d *pluginsdk.ResourceData, meta interface{} oldReplicationsRaw, newReplicationsRaw := d.GetChange("georeplications") hasGeoReplicationsChanges := d.HasChange("georeplications") - oldReplications := oldReplicationsRaw.(*pluginsdk.Set) - newReplications := newReplicationsRaw.(*pluginsdk.Set) + oldReplications := oldReplicationsRaw.([]interface{}) + newReplications := newReplicationsRaw.([]interface{}) // handle upgrade to Premium SKU first if skuChange && isPremiumSku { @@ -560,13 +596,13 @@ func resourceContainerRegistryUpdate(d *pluginsdk.ResourceData, meta interface{} } // geo replication is only supported by Premium Sku - hasGeoReplicationsApplied := newGeoReplicationLocations.Len() > 0 || newReplications.Len() > 0 + hasGeoReplicationsApplied := newGeoReplicationLocations.Len() > 0 || len(newReplications) > 0 if hasGeoReplicationsApplied && !strings.EqualFold(sku, string(containerregistry.Premium)) { return fmt.Errorf("ACR geo-replication can only be applied when using the Premium Sku.") } if hasGeoReplicationsChanges { - err := applyGeoReplicationLocations(d, meta, resourceGroup, name, expandReplications(oldReplications.List()), expandReplications(newReplications.List())) + err := applyGeoReplicationLocations(d, meta, resourceGroup, name, expandReplications(oldReplications), expandReplications(newReplications)) if err != nil { return fmt.Errorf("Error applying geo replications for Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err) } @@ -631,7 +667,7 @@ func applyContainerRegistrySku(d *pluginsdk.ResourceData, meta interface{}, sku return nil } -func applyGeoReplicationLocations(d *pluginsdk.ResourceData, meta interface{}, resourceGroup string, name string, oldGeoReplications []*containerregistry.Replication, newGeoReplications []*containerregistry.Replication) error { +func applyGeoReplicationLocations(d *pluginsdk.ResourceData, meta interface{}, resourceGroup string, name string, oldGeoReplications []containerregistry.Replication, newGeoReplications []containerregistry.Replication) error { replicationClient := meta.(*clients.Client).Containers.ReplicationsClient ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -639,7 +675,7 @@ func applyGeoReplicationLocations(d *pluginsdk.ResourceData, meta interface{}, r // delete previously deployed locations for _, replication := range oldGeoReplications { - if replication == nil || len(*replication.Location) == 0 { + if replication.Location == nil { continue } oldLocation := azure.NormalizeLocation(*replication.Location) @@ -655,11 +691,11 @@ func applyGeoReplicationLocations(d *pluginsdk.ResourceData, meta interface{}, r // create new geo-replication locations for _, replication := range newGeoReplications { - if replication == nil || len(*replication.Location) == 0 { + if replication.Location == nil { continue } locationToCreate := azure.NormalizeLocation(*replication.Location) - future, err := replicationClient.Create(ctx, resourceGroup, name, locationToCreate, *replication) + future, err := replicationClient.Create(ctx, resourceGroup, name, locationToCreate, replication) if err != nil { return fmt.Errorf("Error creating Container Registry Replication %q (Resource Group %q, Location %q): %+v", name, resourceGroup, locationToCreate, err) } @@ -730,6 +766,7 @@ func resourceContainerRegistryRead(d *pluginsdk.ResourceData, meta interface{}) if err := d.Set("encryption", flattenEncryption(properties.Encryption)); err != nil { return fmt.Errorf("Error setting `encryption`: %+v", err) } + d.Set("zone_redundancy_enabled", properties.ZoneRedundancy == containerregistry.ZoneRedundancyEnabled) } if sku := resp.Sku; sku != nil { @@ -771,6 +808,7 @@ func resourceContainerRegistryRead(d *pluginsdk.ResourceData, meta interface{}) replication := make(map[string]interface{}) replication["location"] = valueLocation replication["tags"] = tags.Flatten(value.Tags) + replication["zone_redundancy_enabled"] = value.ZoneRedundancy == containerregistry.ZoneRedundancyEnabled geoReplications = append(geoReplications, replication) } } @@ -895,11 +933,11 @@ func expandTrustPolicy(p []interface{}) *containerregistry.TrustPolicy { return &trustPolicy } -func expandReplicationsFromLocations(p []interface{}) []*containerregistry.Replication { - replications := make([]*containerregistry.Replication, 0) +func expandReplicationsFromLocations(p []interface{}) []containerregistry.Replication { + replications := make([]containerregistry.Replication, 0) for _, value := range p { location := azure.NormalizeLocation(value) - replications = append(replications, &containerregistry.Replication{ + replications = append(replications, containerregistry.Replication{ Location: &location, Name: &location, }) @@ -907,8 +945,8 @@ func expandReplicationsFromLocations(p []interface{}) []*containerregistry.Repli return replications } -func expandReplications(p []interface{}) []*containerregistry.Replication { - replications := make([]*containerregistry.Replication, 0) +func expandReplications(p []interface{}) []containerregistry.Replication { + replications := make([]containerregistry.Replication, 0) if p == nil { return replications } @@ -916,10 +954,17 @@ func expandReplications(p []interface{}) []*containerregistry.Replication { value := v.(map[string]interface{}) location := azure.NormalizeLocation(value["location"]) tags := tags.Expand(value["tags"].(map[string]interface{})) - replications = append(replications, &containerregistry.Replication{ + zoneRedundancy := containerregistry.ZoneRedundancyDisabled + if value["zone_redundancy_enabled"].(bool) { + zoneRedundancy = containerregistry.ZoneRedundancyEnabled + } + replications = append(replications, containerregistry.Replication{ Location: &location, Name: &location, Tags: tags, + ReplicationProperties: &containerregistry.ReplicationProperties{ + ZoneRedundancy: zoneRedundancy, + }, }) } return replications diff --git a/azurerm/internal/services/containers/container_registry_resource_test.go b/azurerm/internal/services/containers/container_registry_resource_test.go index 3c6e5fc97371..996221df3d75 100644 --- a/azurerm/internal/services/containers/container_registry_resource_test.go +++ b/azurerm/internal/services/containers/container_registry_resource_test.go @@ -508,6 +508,36 @@ func TestAccContainerRegistry_identity(t *testing.T) { }) } +func TestAccContainerRegistry_zoneRedundancy(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_container_registry", "test") + r := ContainerRegistryResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.zoneRedundancy(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccContainerRegistry_geoReplicationZoneRedundancy(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_container_registry", "test") + r := ContainerRegistryResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.geoReplicationZoneRedundancy(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func (t ContainerRegistryResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := azure.ParseAzureResourceID(state.ID) if err != nil { @@ -1035,3 +1065,44 @@ resource "azurerm_user_assigned_identity" "test" { } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) } + +func (ContainerRegistryResource) zoneRedundancy(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +resource "azurerm_resource_group" "test" { + name = "acctestRG-acr-%d" + location = "%s" +} +resource "azurerm_container_registry" "test" { + name = "testacccr%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku = "Premium" + zone_redundancy_enabled = true +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + +func (ContainerRegistryResource) geoReplicationZoneRedundancy(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +resource "azurerm_resource_group" "test" { + name = "acctestRG-acr-%d" + location = "%s" +} +resource "azurerm_container_registry" "test" { + name = "testacccr%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku = "Premium" + georeplications { + location = "%s" + zone_redundancy_enabled = true + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.Locations.Secondary) +} diff --git a/website/docs/r/container_registry.html.markdown b/website/docs/r/container_registry.html.markdown index f6309fb2612b..377fb4a121a7 100644 --- a/website/docs/r/container_registry.html.markdown +++ b/website/docs/r/container_registry.html.markdown @@ -121,18 +121,26 @@ The following arguments are supported: * `trust_policy` - (Optional) A `trust_policy` block as documented below. +* `zone_redundancy_enabled` - (Optional) Whether zone redundancy is enabled for this Container Registry? Changing this forces a new resource to be created. Defaults to `false`. + + ~> **NOTE:** `quarantine_policy_enabled`, `retention_policy`, `trust_policy` and `zone_redundancy_enabled` are only supported on resources with the `Premium` SKU. + * `identity` - (Optional) An `identity` block as documented below. * `encryption` - (Optional) An `encryption` block as documented below. -~> **NOTE:** `quarantine_policy_enabled`, `retention_policy` and `trust_policy` are only supported on resources with the `Premium` SKU. +--- `georeplications` supports the following: * `location` - (Required) A location where the container registry should be geo-replicated. +* `zone_redundancy_enabled` - (Optional) Whether zone redundancy is enabled for this replication location? Defaults to `false`. + * `tags` - (Optional) A mapping of tags to assign to this replication location. +--- + `network_rule_set` supports the following: * `default_action` - (Optional) The behaviour for requests matching no rules. Either `Allow` or `Deny`. Defaults to `Allow` @@ -145,41 +153,53 @@ The following arguments are supported: ~> **NOTE:** Azure automatically configures Network Rules - to remove these you'll need to specify an `network_rule_set` block with `default_action` set to `Deny`. +--- + `ip_rule` supports the following: * `action` - (Required) The behaviour for requests matching this rule. At this time the only supported value is `Allow` * `ip_range` - (Required) The CIDR block from which requests will match the rule. +--- + `virtual_network` supports the following: * `action` - (Required) The behaviour for requests matching this rule. At this time the only supported value is `Allow` * `subnet_id` - (Required) The subnet id from which requests will match the rule. +--- + `trust_policy` supports the following: * `enabled` - (Optional) Boolean value that indicates whether the policy is enabled. +--- + `retention_policy` supports the following: * `days` - (Optional) The number of days to retain an untagged manifest after which it gets purged. Default is `7`. * `enabled` - (Optional) Boolean value that indicates whether the policy is enabled. +--- + `identity` supports the following: * `type` - (Required) The type of Managed Identity which should be assigned to the Container Registry. Possible values are `SystemAssigned`, `UserAssigned` and `SystemAssigned, UserAssigned`. * `identity_ids` - (Optional) A list of User Managed Identity ID's which should be assigned to the Container Registry. +--- + `encryption` supports the following: * `enabled` - (Optional) Boolean value that indicates whether encryption is enabled. * `key_vault_key_id` - (Required) The ID of the Key Vault Key. -* `identity_client_id` - (Required) The client ID of the managed identity associated with the encryption key. +* `identity_client_id` - (Required) The client ID of the managed identity associated with the encryption key. ~> **NOTE** The managed identity used in `encryption` also needs to be part of the `identity` block under `identity_ids`