diff --git a/azurerm/internal/services/containers/container_registry_resource.go b/azurerm/internal/services/containers/container_registry_resource.go index 10956c5c1f63..5cd6c1d11cc5 100644 --- a/azurerm/internal/services/containers/container_registry_resource.go +++ b/azurerm/internal/services/containers/container_registry_resource.go @@ -71,11 +71,13 @@ func resourceContainerRegistry() *schema.Resource { Optional: true, Default: false, }, - + // TODO 3.0 - Remove below property "georeplication_locations": { - Type: schema.TypeSet, - MinItems: 1, - Optional: true, + Type: schema.TypeSet, + Optional: true, + Deprecated: "Deprecated in favour of `georeplications`", + Computed: true, + ConflictsWith: []string{"georeplications"}, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringIsNotEmpty, @@ -83,6 +85,27 @@ func resourceContainerRegistry() *schema.Resource { Set: location.HashCode, }, + "georeplications": { + Type: schema.TypeList, + Optional: true, + Computed: true, // TODO -- remove this when deprecation resolves + ConflictsWith: []string{"georeplication_locations"}, + ConfigMode: schema.SchemaConfigModeAttr, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "location": { + Type: schema.TypeString, + Required: true, + ValidateFunc: location.EnhancedValidate, + StateFunc: location.StateFunc, + DiffSuppressFunc: location.DiffSuppressFunc, + }, + + "tags": tags.Schema(), + }, + }, + }, + "public_network_access_enabled": { Type: schema.TypeBool, Optional: true, @@ -222,8 +245,10 @@ func resourceContainerRegistry() *schema.Resource { CustomizeDiff: func(d *schema.ResourceDiff, v interface{}) error { sku := d.Get("sku").(string) geoReplicationLocations := d.Get("georeplication_locations").(*schema.Set) + 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 geoReplicationLocations != nil && geoReplicationLocations.Len() > 0 && !strings.EqualFold(sku, string(containerregistry.Premium)) { + if hasGeoReplicationsApplied && !strings.EqualFold(sku, string(containerregistry.Premium)) { return fmt.Errorf("ACR geo-replication can only be applied when using the Premium Sku.") } @@ -282,6 +307,7 @@ func resourceContainerRegistryCreate(d *schema.ResourceData, meta interface{}) e adminUserEnabled := d.Get("admin_enabled").(bool) t := d.Get("tags").(map[string]interface{}) geoReplicationLocations := d.Get("georeplication_locations").(*schema.Set) + geoReplications := d.Get("georeplications").([]interface{}) networkRuleSet := expandNetworkRuleSet(d.Get("network_rule_set").([]interface{})) if networkRuleSet != nil && !strings.EqualFold(sku, string(containerregistry.Premium)) { @@ -341,8 +367,17 @@ func resourceContainerRegistryCreate(d *schema.ResourceData, meta interface{}) e // locations have been specified for geo-replication if geoReplicationLocations != nil && geoReplicationLocations.Len() > 0 { // the ACR is being created so no previous geo-replication locations - oldGeoReplicationLocations := []interface{}{} - err = applyGeoReplicationLocations(d, meta, resourceGroup, name, oldGeoReplicationLocations, geoReplicationLocations.List()) + var oldGeoReplicationLocations []*containerregistry.Replication + err = applyGeoReplicationLocations(d, meta, resourceGroup, name, oldGeoReplicationLocations, expandReplicationsFromLocations(geoReplicationLocations.List())) + if err != nil { + return fmt.Errorf("Error applying geo replications for Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + // geo replications have been specified + if len(geoReplications) > 0 { + // the ACR is being created so no previous geo-replication locations + var oldGeoReplicationLocations []*containerregistry.Replication + err = applyGeoReplicationLocations(d, meta, resourceGroup, name, oldGeoReplicationLocations, expandReplications(geoReplications)) if err != nil { return fmt.Errorf("Error applying geo replications for Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err) } @@ -381,10 +416,15 @@ func resourceContainerRegistryUpdate(d *schema.ResourceData, meta interface{}) e t := d.Get("tags").(map[string]interface{}) old, new := d.GetChange("georeplication_locations") - hasGeoReplicationChanges := d.HasChange("georeplication_locations") + hasGeoReplicationLocationsChanges := d.HasChange("georeplication_locations") oldGeoReplicationLocations := old.(*schema.Set) newGeoReplicationLocations := new.(*schema.Set) + oldReplicationsRaw, newReplicationsRaw := d.GetChange("georeplications") + hasGeoReplicationsChanges := d.HasChange("georeplications") + oldReplications := oldReplicationsRaw.([]interface{}) + newReplications := newReplicationsRaw.([]interface{}) + // handle upgrade to Premium SKU first if skuChange && isPremiumSku { if err := applyContainerRegistrySku(d, meta, sku, resourceGroup, name); err != nil { @@ -419,13 +459,18 @@ func resourceContainerRegistryUpdate(d *schema.ResourceData, meta interface{}) e } // geo replication is only supported by Premium Sku - if hasGeoReplicationChanges && newGeoReplicationLocations.Len() > 0 && !strings.EqualFold(sku, string(containerregistry.Premium)) { + 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 the registry had replications and is updated to another Sku than premium - remove old locations - if !strings.EqualFold(sku, string(containerregistry.Premium)) && oldGeoReplicationLocations != nil && oldGeoReplicationLocations.Len() > 0 { - err := applyGeoReplicationLocations(d, meta, resourceGroup, name, oldGeoReplicationLocations.List(), newGeoReplicationLocations.List()) + if hasGeoReplicationsChanges { + 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) + } + } else if hasGeoReplicationLocationsChanges { + err := applyGeoReplicationLocations(d, meta, resourceGroup, name, expandReplicationsFromLocations(oldGeoReplicationLocations.List()), expandReplicationsFromLocations(newGeoReplicationLocations.List())) if err != nil { return fmt.Errorf("Error applying geo replications for Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err) } @@ -440,13 +485,6 @@ func resourceContainerRegistryUpdate(d *schema.ResourceData, meta interface{}) e return fmt.Errorf("Error waiting for update of Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err) } - if strings.EqualFold(sku, string(containerregistry.Premium)) && hasGeoReplicationChanges { - err = applyGeoReplicationLocations(d, meta, resourceGroup, name, oldGeoReplicationLocations.List(), newGeoReplicationLocations.List()) - if err != nil { - return fmt.Errorf("Error applying geo replications for Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err) - } - } - // downgrade to Basic or Standard SKU if skuChange && (isBasicSku || isStandardSku) { if err := applyContainerRegistrySku(d, meta, sku, resourceGroup, name); err != nil { @@ -492,45 +530,37 @@ func applyContainerRegistrySku(d *schema.ResourceData, meta interface{}, sku str return nil } -func applyGeoReplicationLocations(d *schema.ResourceData, meta interface{}, resourceGroup string, name string, oldGeoReplicationLocations []interface{}, newGeoReplicationLocations []interface{}) error { +func applyGeoReplicationLocations(d *schema.ResourceData, meta interface{}, resourceGroup string, name string, oldGeoReplicationLocations []*containerregistry.Replication, newGeoReplicationLocations []*containerregistry.Replication) error { replicationClient := meta.(*clients.Client).Containers.ReplicationsClient ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) defer cancel() log.Printf("[INFO] preparing to apply geo-replications for Container Registry.") - createLocations := make(map[string]bool) + createReplications := make(map[string]*containerregistry.Replication) // loop on the new location values for _, nl := range newGeoReplicationLocations { - newLocation := azure.NormalizeLocation(nl) - createLocations[newLocation] = true // the location needs to be created + if nl == nil { + continue + } + newLocation := azure.NormalizeLocation(*nl.Location) + createReplications[newLocation] = nl // the replication needs to be created } // loop on the old location values for _, ol := range oldGeoReplicationLocations { + if ol == nil { + continue + } // oldLocation was created from a previous deployment - oldLocation := azure.NormalizeLocation(ol) + oldLocation := azure.NormalizeLocation(*ol.Location) - // if the list of locations to create already contains the location - if _, ok := createLocations[oldLocation]; ok { - createLocations[oldLocation] = false // the location do not need to be created, it already exists - } + delete(createReplications, oldLocation) // the location do not need to be created, it already exists } // create new geo-replication locations - for locationToCreate := range createLocations { - // if false, the location does not need to be created, continue - if !createLocations[locationToCreate] { - continue - } - - // create the new replication location - replication := containerregistry.Replication{ - Location: &locationToCreate, - Name: &locationToCreate, - } - - future, err := replicationClient.Create(ctx, resourceGroup, name, locationToCreate, replication) + for locationToCreate, replication := range createReplications { + 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) } @@ -542,9 +572,12 @@ func applyGeoReplicationLocations(d *schema.ResourceData, meta interface{}, reso // loop on the list of previously deployed locations for _, ol := range oldGeoReplicationLocations { - oldLocation := azure.NormalizeLocation(ol) + if ol == nil { + continue + } + oldLocation := azure.NormalizeLocation(*ol.Location) // if the old location is still in the list of locations, then continue - if _, ok := createLocations[oldLocation]; ok { + if _, ok := createReplications[oldLocation]; ok { continue } @@ -640,24 +673,23 @@ func resourceContainerRegistryRead(d *schema.ResourceData, meta interface{}) err return fmt.Errorf("Error making Read request on Azure Container Registry %s for replications: %s", name, err) } - replicationValues := replications.Values() - - // if there is more than one location (the main one and the replicas) - if replicationValues != nil || len(replicationValues) > 1 { - georeplication_locations := &schema.Set{F: schema.HashString} - - for _, value := range replicationValues { - if value.Location != nil { - valueLocation := azure.NormalizeLocation(*value.Location) - if location != nil && valueLocation != azure.NormalizeLocation(*location) { - georeplication_locations.Add(valueLocation) - } + geoReplicationLocations := make([]interface{}, 0) + geoReplications := make([]interface{}, 0) + for _, value := range replications.Values() { + if value.Location != nil { + valueLocation := azure.NormalizeLocation(*value.Location) + if location != nil && valueLocation != azure.NormalizeLocation(*location) { + geoReplicationLocations = append(geoReplicationLocations, *value.Location) + replication := make(map[string]interface{}) + replication["location"] = valueLocation + replication["tags"] = tags.Flatten(value.Tags) + geoReplications = append(geoReplications, replication) } } - - d.Set("georeplication_locations", georeplication_locations) } + d.Set("georeplication_locations", geoReplicationLocations) + d.Set("georeplications", geoReplications) return tags.FlattenAndSet(d, resp.Tags) } @@ -781,6 +813,36 @@ func expandTrustPolicy(p []interface{}) *containerregistry.TrustPolicy { return &trustPolicy } +func expandReplicationsFromLocations(p []interface{}) []*containerregistry.Replication { + replications := make([]*containerregistry.Replication, 0) + for _, value := range p { + location := azure.NormalizeLocation(value) + replications = append(replications, &containerregistry.Replication{ + Location: &location, + Name: &location, + }) + } + return replications +} + +func expandReplications(p []interface{}) []*containerregistry.Replication { + replications := make([]*containerregistry.Replication, 0) + if p == nil { + return replications + } + for _, v := range p { + value := v.(map[string]interface{}) + location := azure.NormalizeLocation(value["location"]) + tags := tags.Expand(value["tags"].(map[string]interface{})) + replications = append(replications, &containerregistry.Replication{ + Location: &location, + Name: &location, + Tags: tags, + }) + } + return replications +} + func flattenNetworkRuleSet(networkRuleSet *containerregistry.NetworkRuleSet) []interface{} { if networkRuleSet == nil { return []interface{}{} diff --git a/azurerm/internal/services/containers/container_registry_resource_test.go b/azurerm/internal/services/containers/container_registry_resource_test.go index d552a8e994b2..b9179dbccab4 100644 --- a/azurerm/internal/services/containers/container_registry_resource_test.go +++ b/azurerm/internal/services/containers/container_registry_resource_test.go @@ -204,7 +204,7 @@ func TestAccContainerRegistry_update(t *testing.T) { }) } -func TestAccContainerRegistry_geoReplication(t *testing.T) { +func TestAccContainerRegistry_geoReplicationLocation(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_container_registry", "test") r := ContainerRegistryResource{} @@ -217,7 +217,7 @@ func TestAccContainerRegistry_geoReplication(t *testing.T) { data.ResourceTest(t, r, []resource.TestStep{ // first config creates an ACR with locations { - Config: r.geoReplication(data, skuPremium, []string{secondaryLocation}), + Config: r.geoReplicationLocation(data, skuPremium, []string{secondaryLocation}), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("sku").HasValue(skuPremium), @@ -227,7 +227,7 @@ func TestAccContainerRegistry_geoReplication(t *testing.T) { }, // second config updates the ACR with updated locations { - Config: r.geoReplication(data, skuPremium, []string{ternaryLocation}), + Config: r.geoReplicationLocation(data, skuPremium, []string{ternaryLocation}), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("sku").HasValue(skuPremium), @@ -235,7 +235,8 @@ func TestAccContainerRegistry_geoReplication(t *testing.T) { check.That(data.ResourceName).Key("georeplication_locations.0").HasValue(ternaryLocation), ), }, - // third config updates the ACR with no location + // For compatibility, downgrade from Premium to Basic should remove all replications first, but it's unnecessary. Once georeplication_locations is deprecated, this can be done in single update. + // third config updates the ACR with no location. { Config: r.geoReplicationUpdateWithNoLocation(data, skuPremium), Check: resource.ComposeTestCheckFunc( @@ -244,22 +245,69 @@ func TestAccContainerRegistry_geoReplication(t *testing.T) { check.That(data.ResourceName).Key("georeplication_locations.#").HasValue("0"), ), }, - // fourth config updates an ACR with replicas + // fourth config updates the SKU to basic. { - Config: r.geoReplication(data, skuPremium, []string{secondaryLocation}), + Config: r.geoReplicationUpdateWithNoLocation_basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("georeplication_locations.#").HasValue("1"), - check.That(data.ResourceName).Key("georeplication_locations.0").HasValue(secondaryLocation), + check.That(data.ResourceName).Key("sku").HasValue(skuBasic), + check.That(data.ResourceName).Key("georeplication_locations.#").HasValue("0"), ), }, - // fifth config updates the SKU to basic and no replicas (should remove the existing replicas if any) + }) +} + +func TestAccContainerRegistry_geoReplication(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_container_registry", "test") + r := ContainerRegistryResource{} + + skuPremium := "Premium" + skuBasic := "Basic" + + secondaryLocation := location.Normalize(data.Locations.Secondary) + ternaryLocation := location.Normalize(data.Locations.Ternary) + + data.ResourceTest(t, r, []resource.TestStep{ + // first config creates an ACR with locations { - Config: r.geoReplicationUpdateWithNoLocation_basic(data), + Config: r.geoReplication(data, skuPremium, secondaryLocation), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("sku").HasValue(skuPremium), + check.That(data.ResourceName).Key("georeplications.#").HasValue("1"), + check.That(data.ResourceName).Key("georeplications.0.location").HasValue(secondaryLocation), + ), + }, + data.ImportStep(), + // second config updates the ACR with updated locations + { + Config: r.geoReplication(data, skuPremium, ternaryLocation), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("sku").HasValue(skuPremium), + check.That(data.ResourceName).Key("georeplications.#").HasValue("1"), + check.That(data.ResourceName).Key("georeplications.0.location").HasValue(ternaryLocation), + ), + }, + data.ImportStep(), + // For compatibility, downgrade from Premium to Basic should remove all replications first, but it's unnecessary. Once georeplication_locations is deprecated, this can be done in single update. + // third config updates the ACR with no location + { + Config: r.geoReplicationUpdateWithNoReplication(data, skuPremium), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("sku").HasValue(skuPremium), + check.That(data.ResourceName).Key("georeplications.#").HasValue("0"), + ), + }, + data.ImportStep(), + // fourth config updates the SKU to basic. + { + Config: r.geoReplicationUpdateWithNoReplication_basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("sku").HasValue(skuBasic), - check.That(data.ResourceName).Key("georeplication_locations.#").HasValue("0"), + check.That(data.ResourceName).Key("georeplications.#").HasValue("0"), ), }, }) @@ -414,7 +462,7 @@ resource "azurerm_container_registry" "test" { sku = "Basic" # make sure network_rule_set is empty for basic SKU - # premiuim SKU will automaticcally populate network_rule_set.default_action to allow + # premiuim SKU will automatically populate network_rule_set.default_action to allow network_rule_set = [] } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) @@ -504,7 +552,7 @@ resource "azurerm_container_registry" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } -func (ContainerRegistryResource) geoReplication(data acceptance.TestData, sku string, replicationRegions []string) string { +func (ContainerRegistryResource) geoReplicationLocation(data acceptance.TestData, sku string, replicationRegions []string) string { regions := make([]string, 0) for _, region := range replicationRegions { // ensure they're quoted @@ -530,7 +578,7 @@ resource "azurerm_container_registry" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, sku, strings.Join(regions, ",")) } -func (ContainerRegistryResource) geoReplicationUpdateWithNoLocation(data acceptance.TestData, sku string) string { +func (ContainerRegistryResource) geoReplication(data acceptance.TestData, sku string, replication string) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -546,10 +594,57 @@ resource "azurerm_container_registry" "test" { resource_group_name = azurerm_resource_group.test.name location = azurerm_resource_group.test.location sku = "%s" + georeplications { + location = %s + tags = { + Environment = "Production" + } + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, sku, fmt.Sprintf("%q", replication)) +} + +func (ContainerRegistryResource) geoReplicationUpdateWithNoLocation(data acceptance.TestData, sku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-aks-%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 = "%s" + georeplication_locations = [] } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, sku) } +func (ContainerRegistryResource) geoReplicationUpdateWithNoReplication(data acceptance.TestData, sku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-aks-%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 = "%s" + georeplications = [] +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, sku) +} func (ContainerRegistryResource) geoReplicationUpdateWithNoLocation_basic(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { @@ -568,12 +663,38 @@ resource "azurerm_container_registry" "test" { sku = "Basic" # make sure network_rule_set is empty for basic SKU - # premiuim SKU will automaticcally populate network_rule_set.default_action to allow + # premiuim SKU will automatically populate network_rule_set.default_action to allow network_rule_set = [] } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } +func (ContainerRegistryResource) geoReplicationUpdateWithNoReplication_basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-aks-%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 = "Basic" + + # make sure network_rule_set is empty for basic SKU + # premiuim SKU will automatically populate network_rule_set.default_action to allow + network_rule_set = [] + + georeplications = [] +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + func (ContainerRegistryResource) networkAccessProfile_ip(data acceptance.TestData, sku string) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/website/docs/r/container_registry.html.markdown b/website/docs/r/container_registry.html.markdown index be67c4f51bb9..ddb2dcf65ca4 100644 --- a/website/docs/r/container_registry.html.markdown +++ b/website/docs/r/container_registry.html.markdown @@ -58,6 +58,14 @@ The following arguments are supported: ~> **NOTE:** The `georeplication_locations` list cannot contain the location where the Container Registry exists. +~> **NOTE:** The `georeplication_locations` is deprecated, use `georeplications` instead. + +* `georeplications` - (Optional) A `georeplications` block as documented below. + +~> **NOTE:** The `georeplications` is only supported on new resources with the `Premium` SKU. + +~> **NOTE:** The `georeplications` list cannot contain the location where the Container Registry exists. + * `network_rule_set` - (Optional) A `network_rule_set` block as documented below. * `public_network_access_enabled` - (Optional) Whether public network access is allowed for the container registry. Defaults to `true`. @@ -68,6 +76,12 @@ The following arguments are supported: ~> **NOTE:** `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. + +* `tags` - (Optional) A mapping of tags to assign to the container registry replication resource. + `network_rule_set` supports the following: * `default_action` - (Optional) The behaviour for requests matching no rules. Either `Allow` or `Deny`. Defaults to `Allow`