Skip to content

Commit

Permalink
Merge pull request #2055 from jcorioland/feat/acr-georeplication
Browse files Browse the repository at this point in the history
Support geo-replication for azurerm_container_registry resource
  • Loading branch information
katbyte authored Nov 27, 2018
2 parents d584767 + 62cefde commit 26572ba
Show file tree
Hide file tree
Showing 10 changed files with 427 additions and 10 deletions.
4 changes: 4 additions & 0 deletions azurerm/common_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ func zonesSchemaComputed() *schema.Schema {
func expandZones(v []interface{}) *[]string {
return azure.ExpandZones(v)
}

func azureRMHashLocation(location interface{}) int {
return azure.HashAzureLocation(location)
}
14 changes: 10 additions & 4 deletions azurerm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,11 @@ type ArmClient struct {
dnsClient dns.RecordSetsClient
zonesClient dns.ZonesClient

containerRegistryClient containerregistry.RegistriesClient
containerServicesClient containerservice.ContainerServicesClient
kubernetesClustersClient containerservice.ManagedClustersClient
containerGroupsClient containerinstance.ContainerGroupsClient
containerRegistryClient containerregistry.RegistriesClient
containerRegistryReplicationsClient containerregistry.ReplicationsClient
containerServicesClient containerservice.ContainerServicesClient
kubernetesClustersClient containerservice.ManagedClustersClient
containerGroupsClient containerinstance.ContainerGroupsClient

eventGridTopicsClient eventgrid.TopicsClient
eventHubClient eventhub.EventHubsClient
Expand Down Expand Up @@ -612,6 +613,11 @@ func (c *ArmClient) registerContainerRegistryClients(endpoint, subscriptionId st
crc := containerregistry.NewRegistriesClientWithBaseURI(endpoint, subscriptionId)
c.configureClient(&crc.Client, auth)
c.containerRegistryClient = crc

// container registry replicalication client
crrc := containerregistry.NewReplicationsClientWithBaseURI(endpoint, subscriptionId)
c.configureClient(&crrc.Client, auth)
c.containerRegistryReplicationsClient = crrc
}

func (c *ArmClient) registerContainerServicesClients(endpoint, subscriptionId string, auth autorest.Authorizer) {
Expand Down
5 changes: 5 additions & 0 deletions azurerm/helpers/azure/location.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package azure
import (
"strings"

"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)

Expand Down Expand Up @@ -46,3 +47,7 @@ func NormalizeLocation(location interface{}) string {
func SuppressLocationDiff(k, old, new string, d *schema.ResourceData) bool {
return NormalizeLocation(old) == NormalizeLocation(new)
}

func HashAzureLocation(location interface{}) int {
return hashcode.String(NormalizeLocation(location.(string)))
}
165 changes: 159 additions & 6 deletions azurerm/resource_arm_container_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ func resourceArmContainerRegistry() *schema.Resource {
Default: false,
},

"georeplication_locations": {
Type: schema.TypeSet,
MinItems: 1,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.NoZeroValues,
},
Set: azureRMHashLocation,
},

"storage_account_id": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -101,6 +112,17 @@ func resourceArmContainerRegistry() *schema.Resource {

"tags": tagsSchema(),
},

CustomizeDiff: func(d *schema.ResourceDiff, v interface{}) error {
sku := d.Get("sku").(string)
geoReplicationLocations := d.Get("georeplication_locations").(*schema.Set)
// 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)) {
return fmt.Errorf("ACR geo-replication can only be applied when using the Premium Sku.")
}

return nil
},
}
}

Expand All @@ -115,6 +137,7 @@ func resourceArmContainerRegistryCreate(d *schema.ResourceData, meta interface{}
sku := d.Get("sku").(string)
adminUserEnabled := d.Get("admin_enabled").(bool)
tags := d.Get("tags").(map[string]interface{})
geoReplicationLocations := d.Get("georeplication_locations").(*schema.Set)

parameters := containerregistry.Registry{
Location: &location,
Expand Down Expand Up @@ -152,6 +175,16 @@ func resourceArmContainerRegistryCreate(d *schema.ResourceData, meta interface{}
return fmt.Errorf("Error waiting for creation of Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err)
}

// 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(meta, resourceGroup, name, oldGeoReplicationLocations, geoReplicationLocations.List())
if err != nil {
return fmt.Errorf("Error applying geo replications for Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err)
}
}

read, err := client.Get(ctx, resourceGroup, name)
if err != nil {
return fmt.Errorf("Error retrieving Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err)
Expand All @@ -178,6 +211,11 @@ func resourceArmContainerRegistryUpdate(d *schema.ResourceData, meta interface{}
adminUserEnabled := d.Get("admin_enabled").(bool)
tags := d.Get("tags").(map[string]interface{})

old, new := d.GetChange("georeplication_locations")
hasGeoReplicationChanges := d.HasChange("georeplication_locations")
oldGeoReplicationLocations := old.(*schema.Set)
newGeoReplicationLocations := new.(*schema.Set)

parameters := containerregistry.RegistryUpdateParameters{
RegistryPropertiesUpdateParameters: &containerregistry.RegistryPropertiesUpdateParameters{
AdminUserEnabled: utils.Bool(adminUserEnabled),
Expand All @@ -190,19 +228,32 @@ func resourceArmContainerRegistryUpdate(d *schema.ResourceData, meta interface{}
}

if v, ok := d.GetOk("storage_account_id"); ok {
if strings.ToLower(sku) != strings.ToLower(string(containerregistry.Classic)) {
if !strings.EqualFold(sku, string(containerregistry.Classic)) {
return fmt.Errorf("`storage_account_id` can only be specified for a Classic (unmanaged) Sku.")
}

parameters.StorageAccount = &containerregistry.StorageAccountProperties{
ID: utils.String(v.(string)),
}
} else {
if strings.ToLower(sku) == strings.ToLower(string(containerregistry.Classic)) {
if strings.EqualFold(sku, string(containerregistry.Classic)) {
return fmt.Errorf("`storage_account_id` must be specified for a Classic (unmanaged) Sku.")
}
}

// geo replication is only supported by Premium Sku
if hasGeoReplicationChanges && newGeoReplicationLocations.Len() > 0 && !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(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)
}
}

future, err := client.Update(ctx, resourceGroup, name, parameters)
if err != nil {
return fmt.Errorf("Error updating Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err)
Expand All @@ -213,6 +264,13 @@ func resourceArmContainerRegistryUpdate(d *schema.ResourceData, meta interface{}
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(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)
}
}

read, err := client.Get(ctx, resourceGroup, name)
if err != nil {
return fmt.Errorf("Error retrieving Container Registry %q (Resource Group %q): %+v", name, resourceGroup, err)
Expand All @@ -227,8 +285,78 @@ func resourceArmContainerRegistryUpdate(d *schema.ResourceData, meta interface{}
return resourceArmContainerRegistryRead(d, meta)
}

func applyGeoReplicationLocations(meta interface{}, resourceGroup string, name string, oldGeoReplicationLocations []interface{}, newGeoReplicationLocations []interface{}) error {
replicationClient := meta.(*ArmClient).containerRegistryReplicationsClient
ctx := meta.(*ArmClient).StopContext
log.Printf("[INFO] preparing to apply geo-replications for AzureRM Container Registry.")

createLocations := make(map[string]bool)

// loop on the new location values
for _, nl := range newGeoReplicationLocations {
newLocation := azureRMNormalizeLocation(nl)
createLocations[newLocation] = true // the location needs to be created
}

// loop on the old location values
for _, ol := range oldGeoReplicationLocations {
// oldLocation was created from a previous deployment
oldLocation := azureRMNormalizeLocation(ol)

// 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
}
}

// 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)
if err != nil {
return fmt.Errorf("Error creating Container Registry Replication %q (Resource Group %q, Location %q): %+v", name, resourceGroup, locationToCreate, err)
}

if err = future.WaitForCompletionRef(ctx, replicationClient.Client); err != nil {
return fmt.Errorf("Error waiting for creation of Container Registry Replication %q (Resource Group %q, Location %q): %+v", name, resourceGroup, locationToCreate, err)
}
}

// loop on the list of previously deployed locations
for _, ol := range oldGeoReplicationLocations {
oldLocation := azureRMNormalizeLocation(ol)
// if the old location is still in the list of locations, then continue
if _, ok := createLocations[oldLocation]; ok {
continue
}

// the old location is not in the list of locations, delete it
future, err := replicationClient.Delete(ctx, resourceGroup, name, oldLocation)
if err != nil {
return fmt.Errorf("Error deleting Container Registry Replication %q (Resource Group %q, Location %q): %+v", name, resourceGroup, oldLocation, err)
}

if err = future.WaitForCompletionRef(ctx, replicationClient.Client); err != nil {
return fmt.Errorf("Error waiting for deletion of Container Registry Replication %q (Resource Group %q, Location %q): %+v", name, resourceGroup, oldLocation, err)
}
}

return nil
}

func resourceArmContainerRegistryRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).containerRegistryClient
replicationClient := meta.(*ArmClient).containerRegistryReplicationsClient
ctx := meta.(*ArmClient).StopContext

id, err := parseAzureResourceID(d.Id())
Expand All @@ -251,7 +379,9 @@ func resourceArmContainerRegistryRead(d *schema.ResourceData, meta interface{})

d.Set("name", resp.Name)
d.Set("resource_group_name", resourceGroup)
if location := resp.Location; location != nil {

location := resp.Location
if location != nil {
d.Set("location", azureRMNormalizeLocation(*location))
}
d.Set("admin_enabled", resp.AdminUserEnabled)
Expand All @@ -266,9 +396,9 @@ func resourceArmContainerRegistryRead(d *schema.ResourceData, meta interface{})
}

if *resp.AdminUserEnabled {
credsResp, err := client.ListCredentials(ctx, resourceGroup, name)
if err != nil {
return fmt.Errorf("Error making Read request on Azure Container Registry %s for Credentials: %s", name, err)
credsResp, errList := client.ListCredentials(ctx, resourceGroup, name)
if errList != nil {
return fmt.Errorf("Error making Read request on Azure Container Registry %s for Credentials: %s", name, errList)
}

d.Set("admin_username", credsResp.Username)
Expand All @@ -283,6 +413,29 @@ func resourceArmContainerRegistryRead(d *schema.ResourceData, meta interface{})

flattenAndSetTags(d, resp.Tags)

replications, err := replicationClient.List(ctx, resourceGroup, name)
if err != nil {
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 := azureRMNormalizeLocation(*value.Location)
if location != nil && valueLocation != azureRMNormalizeLocation(*location) {
georeplication_locations.Add(valueLocation)
}
}
}

d.Set("georeplication_locations", georeplication_locations)
}

return nil
}

Expand Down
Loading

0 comments on commit 26572ba

Please sign in to comment.