From 82a9520f9c8f8809f437116c22380b5315d70da4 Mon Sep 17 00:00:00 2001 From: Tao <104055472+teowa@users.noreply.github.com> Date: Tue, 11 Jul 2023 14:54:40 +0800 Subject: [PATCH 1/3] app configuration support replica --- .../appconfiguration/app_configuration.go | 36 +++ .../app_configuration_data_source.go | 37 +++ .../app_configuration_data_source_test.go | 4 + .../app_configuration_resource.go | 240 +++++++++++++++++- .../app_configuration_resource_test.go | 140 +++++++++- .../appconfiguration/client/client.go | 78 +----- .../configuration_store_replica_name.go | 15 ++ .../configuration_store_replica_name_test.go | 60 +++++ .../2023-03-01/replicas/README.md | 82 ++++++ .../2023-03-01/replicas/client.go | 26 ++ .../2023-03-01/replicas/constants.go | 60 +++++ .../replicas/id_configurationstore.go | 127 +++++++++ .../2023-03-01/replicas/id_replica.go | 140 ++++++++++ .../2023-03-01/replicas/method_create.go | 74 ++++++ .../2023-03-01/replicas/method_delete.go | 71 ++++++ .../2023-03-01/replicas/method_get.go | 51 ++++ .../method_listbyconfigurationstore.go | 89 +++++++ .../2023-03-01/replicas/model_replica.go | 17 ++ .../replicas/model_replicaproperties.go | 9 + .../2023-03-01/replicas/predicates.go | 32 +++ .../2023-03-01/replicas/version.go | 12 + vendor/modules.txt | 1 + .../docs/d/app_configuration.html.markdown | 15 ++ .../docs/r/app_configuration.html.markdown | 29 ++- 24 files changed, 1364 insertions(+), 81 deletions(-) create mode 100644 internal/services/appconfiguration/validate/configuration_store_replica_name.go create mode 100644 internal/services/appconfiguration/validate/configuration_store_replica_name_test.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/README.md create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/client.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/constants.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/id_configurationstore.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/id_replica.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_create.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_delete.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_get.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_listbyconfigurationstore.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/model_replica.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/model_replicaproperties.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/predicates.go create mode 100644 vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/version.go diff --git a/internal/services/appconfiguration/app_configuration.go b/internal/services/appconfiguration/app_configuration.go index 527dcd2186ff..31b317173b01 100644 --- a/internal/services/appconfiguration/app_configuration.go +++ b/internal/services/appconfiguration/app_configuration.go @@ -4,11 +4,15 @@ package appconfiguration import ( + "bytes" "context" + "fmt" "github.com/Azure/go-autorest/autorest" + "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/configurationstores" + "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/tombuildsstuff/kermit/sdk/appconfiguration/1.0/appconfiguration" ) @@ -25,6 +29,38 @@ func flattenAppConfigurationEncryption(input *configurationstores.EncryptionProp }, } } + +func flattenAppConfigurationReplicas(input []replicas.Replica) ([]interface{}, error) { + results := make([]interface{}, 0) + for _, v := range input { + if v.Properties == nil { + return results, fmt.Errorf("retrieving Replica %s Properties is nil", *v.Id) + } + + replicaId, err := replicas.ParseReplicaIDInsensitively(pointer.From(v.Id)) + if err != nil { + return results, err + } + + result := map[string]interface{}{ + "name": pointer.From(v.Name), + "location": pointer.From(v.Location), + "endpoint": pointer.From(v.Properties.Endpoint), + "id": replicaId.ID(), + } + results = append(results, result) + } + return results, nil +} + +func resourceConfigurationStoreReplicaHash(input interface{}) int { + var buf bytes.Buffer + if rawData, ok := input.(map[string]interface{}); ok { + buf.WriteString(rawData["name"].(string)) + } + return pluginsdk.HashString(buf.String()) +} + func appConfigurationGetKeyRefreshFunc(ctx context.Context, client *appconfiguration.BaseClient, key, label string) pluginsdk.StateRefreshFunc { return func() (interface{}, string, error) { res, err := client.GetKeyValue(ctx, key, label, "", "", "", []appconfiguration.KeyValueFields{}) diff --git a/internal/services/appconfiguration/app_configuration_data_source.go b/internal/services/appconfiguration/app_configuration_data_source.go index 6672cf34e69c..9ed8d064442c 100644 --- a/internal/services/appconfiguration/app_configuration_data_source.go +++ b/internal/services/appconfiguration/app_configuration_data_source.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-helpers/resourcemanager/tags" "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/configurationstores" + "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/services/appconfiguration/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" @@ -93,6 +94,29 @@ func dataSourceAppConfiguration() *pluginsdk.Resource { Computed: true, }, + "replica": { + Type: pluginsdk.TypeSet, + Computed: true, + Set: resourceConfigurationStoreReplicaHash, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "location": commonschema.LocationComputed(), + "endpoint": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + }, + "primary_read_key": { Type: pluginsdk.TypeList, Computed: true, @@ -257,6 +281,19 @@ func dataSourceAppConfigurationRead(d *pluginsdk.ResourceData, meta interface{}) return fmt.Errorf("setting `identity`: %+v", err) } + replicasClient := meta.(*clients.Client).AppConfiguration.ReplicasClient + resp, err := replicasClient.ListByConfigurationStoreComplete(ctx, replicas.NewConfigurationStoreID(id.SubscriptionId, id.ResourceGroupName, id.ConfigurationStoreName)) + if err != nil { + return fmt.Errorf("retrieving replicas for %s: %+v", id, err) + } + + replica, err := flattenAppConfigurationReplicas(resp.Items) + if err != nil { + return fmt.Errorf("flattening replicas for %s: %+v", id, err) + } + + d.Set("replica", replica) + return tags.FlattenAndSet(d, model.Tags) } diff --git a/internal/services/appconfiguration/app_configuration_data_source_test.go b/internal/services/appconfiguration/app_configuration_data_source_test.go index 0cdaf2c96d26..2bbbd31e0b55 100644 --- a/internal/services/appconfiguration/app_configuration_data_source_test.go +++ b/internal/services/appconfiguration/app_configuration_data_source_test.go @@ -49,6 +49,10 @@ func TestAccAppConfigurationDataSource_basic(t *testing.T) { check.That(data.ResourceName).Key("secondary_write_key.0.secret").Exists(), check.That(data.ResourceName).Key("sku").Exists(), check.That(data.ResourceName).Key("soft_delete_retention_days").Exists(), + check.That(data.ResourceName).Key("replica.#").HasValue("2"), + check.That(data.ResourceName).Key("replica.0.name").Exists(), + check.That(data.ResourceName).Key("replica.1.id").Exists(), + check.That(data.ResourceName).Key("replica.1.endpoint").Exists(), ), }, }) diff --git a/internal/services/appconfiguration/app_configuration_resource.go b/internal/services/appconfiguration/app_configuration_resource.go index ef9a52ed8462..9e5351704111 100644 --- a/internal/services/appconfiguration/app_configuration_resource.go +++ b/internal/services/appconfiguration/app_configuration_resource.go @@ -21,9 +21,10 @@ import ( "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/configurationstores" "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/deletedconfigurationstores" "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/operations" + "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas" "github.com/hashicorp/go-azure-sdk/sdk/client" "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" - "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/services/appconfiguration/validate" @@ -41,10 +42,10 @@ func resourceAppConfiguration() *pluginsdk.Resource { Delete: resourceAppConfigurationDelete, Timeouts: &pluginsdk.ResourceTimeout{ - Create: pluginsdk.DefaultTimeout(30 * time.Minute), + Create: pluginsdk.DefaultTimeout(60 * time.Minute), Read: pluginsdk.DefaultTimeout(5 * time.Minute), - Update: pluginsdk.DefaultTimeout(30 * time.Minute), - Delete: pluginsdk.DefaultTimeout(30 * time.Minute), + Update: pluginsdk.DefaultTimeout(60 * time.Minute), + Delete: pluginsdk.DefaultTimeout(60 * time.Minute), }, Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { @@ -132,6 +133,37 @@ func resourceAppConfiguration() *pluginsdk.Resource { ValidateFunc: validation.StringInSlice(configurationstores.PossibleValuesForPublicNetworkAccess(), true), }, + "replica": { + Type: pluginsdk.TypeSet, + Optional: true, + MinItems: 1, + Set: resourceConfigurationStoreReplicaHash, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validate.ConfigurationStoreReplicaName, + }, + "location": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: location.EnhancedValidate, + StateFunc: location.StateFunc, + DiffSuppressFunc: location.DiffSuppressFunc, + }, + "endpoint": { + Type: pluginsdk.TypeString, + Computed: true, + }, + "id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + }, + }, + }, + "primary_read_key": { Type: pluginsdk.TypeList, Computed: true, @@ -256,7 +288,7 @@ func resourceAppConfigurationCreate(d *pluginsdk.ResourceData, meta interface{}) return tf.ImportAsExistsError("azurerm_app_configuration", resourceId.ID()) } - location := azure.NormalizeLocation(d.Get("location").(string)) + location := location.Normalize(d.Get("location").(string)) recoverSoftDeleted := false if meta.(*clients.Client).Features.AppConfiguration.RecoverSoftDeleted { @@ -325,6 +357,28 @@ func resourceAppConfigurationCreate(d *pluginsdk.ResourceData, meta interface{}) } meta.(*clients.Client).AppConfiguration.AddToCache(resourceId, *resp.Model.Properties.Endpoint) + expandedReplicas, err := expandAppConfigurationReplicas(d.Get("replica").(*schema.Set).List(), name, location) + if err != nil { + return fmt.Errorf("expanding `replica`: %+v", err) + } + + replicaClient := meta.(*clients.Client).AppConfiguration.ReplicasClient + for _, replica := range expandedReplicas { + replicaId := replicas.NewReplicaID(subscriptionId, resourceGroup, name, *replica.Name) + + existingReplica, err := replicaClient.Get(ctx, replicaId) + if err != nil { + if !response.WasNotFound(existingReplica.HttpResponse) { + return fmt.Errorf("retrieving %s: %+v", replicaId, err) + } + } + + if err := replicaClient.CreateThenPoll(ctx, replicaId, replica); err != nil { + return fmt.Errorf("creating %s: %+v", replicaId, err) + } + + } + return resourceAppConfigurationRead(d, meta) } @@ -434,6 +488,61 @@ func resourceAppConfigurationUpdate(d *pluginsdk.ResourceData, meta interface{}) return fmt.Errorf("updating %s: %+v", *id, err) } + if d.HasChange("replica") { + replicaClient := meta.(*clients.Client).AppConfiguration.ReplicasClient + operationsClient := meta.(*clients.Client).AppConfiguration.OperationsClient + + // check if a replica has been removed from config and if so, delete it + deleteReplicaIds := make([]replicas.ReplicaId, 0) + oldReplicas, newReplicas := d.GetChange("replica") + for _, oldReplica := range oldReplicas.(*pluginsdk.Set).List() { + isRemoved := true + oldReplicaMap := oldReplica.(map[string]interface{}) + + for _, newReplica := range newReplicas.(*pluginsdk.Set).List() { + newReplicaMap := newReplica.(map[string]interface{}) + + if strings.EqualFold(oldReplicaMap["name"].(string), newReplicaMap["name"].(string)) && strings.EqualFold(oldReplicaMap["location"].(string), newReplicaMap["location"].(string)) { + isRemoved = false + break + } + } + + if isRemoved { + deleteReplicaIds = append(deleteReplicaIds, replicas.NewReplicaID(id.SubscriptionId, id.ResourceGroupName, id.ConfigurationStoreName, oldReplicaMap["name"].(string))) + } + } + + if err := deleteReplicas(ctx, replicaClient, operationsClient, deleteReplicaIds); err != nil { + return err + } + + expandedReplicas, err := expandAppConfigurationReplicas(d.Get("replica").(*schema.Set).List(), id.ConfigurationStoreName, location.Normalize(existing.Model.Location)) + if err != nil { + return fmt.Errorf("expanding `replica`: %+v", err) + } + + // check if a replica has been added or an existing one changed its location, (re)create it + for _, replica := range expandedReplicas { + replicaId := replicas.NewReplicaID(id.SubscriptionId, id.ResourceGroupName, id.ConfigurationStoreName, *replica.Name) + + existingReplica, err := replicaClient.Get(ctx, replicaId) + if err != nil { + if !response.WasNotFound(existingReplica.HttpResponse) { + return fmt.Errorf("retrieving %s: %+v", replicaId, err) + } + } + + if !response.WasNotFound(existingReplica.HttpResponse) { + continue + } + + if err = replicaClient.CreateThenPoll(ctx, replicaId, replica); err != nil { + return fmt.Errorf("creating %s: %+v", replicaId, err) + } + } + } + return resourceAppConfigurationRead(d, meta) } @@ -508,6 +617,18 @@ func resourceAppConfigurationRead(d *pluginsdk.ResourceData, meta interface{}) e return fmt.Errorf("setting `identity`: %+v", err) } + replicasClient := meta.(*clients.Client).AppConfiguration.ReplicasClient + resp, err := replicasClient.ListByConfigurationStoreComplete(ctx, replicas.NewConfigurationStoreID(id.SubscriptionId, id.ResourceGroupName, id.ConfigurationStoreName)) + if err != nil { + return fmt.Errorf("retrieving replicas for %s: %+v", *id, err) + } + + replica, err := flattenAppConfigurationReplicas(resp.Items) + if err != nil { + return fmt.Errorf("flattening replicas for %s: %+v", *id, err) + } + d.Set("replica", replica) + return tags.FlattenAndSet(d, model.Tags) } @@ -665,6 +786,41 @@ func expandAppConfigurationEncryption(input []interface{}) *configurationstores. return result } +func expandAppConfigurationReplicas(input []interface{}, configurationStoreName, configurationStoreLocation string) ([]replicas.Replica, error) { + result := make([]replicas.Replica, 0) + + locationSet := map[string]interface{}{ + configurationStoreLocation: nil, + } + replicaNameSet := map[string]interface{}{} + + for _, v := range input { + replica := v.(map[string]interface{}) + replicaName := replica["name"].(string) + replicaLocation := location.Normalize(replica["location"].(string)) + + if _, ok := locationSet[replicaLocation]; ok { + return result, fmt.Errorf("replica location %q is duplicated in configuration store %q location", replicaLocation, configurationStoreName) + } + locationSet[replicaLocation] = nil + + if _, ok := replicaNameSet[replicaName]; ok { + return result, fmt.Errorf("replica name %q is duplicated", replicaName) + } + + if len(replicaName)+len(configurationStoreName) > 60 { + return result, fmt.Errorf("replica name %q is too long, the total length of replica name and configuration store name should be greater than 60", replicaName) + } + + result = append(result, replicas.Replica{ + Name: pointer.To(replicaName), + Location: pointer.To(replicaLocation), + }) + } + + return result, nil +} + func flattenAppConfigurationAccessKeys(values []configurationstores.ApiKey) flattenedAccessKeys { result := flattenedAccessKeys{ primaryReadKey: make([]interface{}, 0), @@ -766,9 +922,8 @@ func resourceConfigurationStoreWaitForNameAvailable(ctx context.Context, client Timeout: time.Until(deadline), } - _, err := state.WaitForStateContext(ctx) - if err != nil { - return fmt.Errorf("waiting for the Name from %s to become available: %+v", configurationStoreId, err) + if _, err := state.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for the name from %s to become available: %+v", configurationStoreId, err) } return nil @@ -805,3 +960,72 @@ func resourceConfigurationStoreNameAvailabilityRefreshFunc(ctx context.Context, return resp, "Available", nil } } + +func deleteReplicas(ctx context.Context, replicaClient *replicas.ReplicasClient, operationClient *operations.OperationsClient, configurationStoreReplicaIds []replicas.ReplicaId) error { + for _, configurationStoreReplicaId := range configurationStoreReplicaIds { + log.Printf("[DEBUG] Deleting Replica %q", configurationStoreReplicaId) + if err := replicaClient.DeleteThenPoll(ctx, configurationStoreReplicaId); err != nil { + return fmt.Errorf("deleting replica %q: %+v", configurationStoreReplicaId, err) + } + } + + for _, configurationStoreReplicaId := range configurationStoreReplicaIds { + if err := resourceConfigurationStoreReplicaWaitForNameAvailable(ctx, operationClient, configurationStoreReplicaId); err != nil { + return fmt.Errorf("waiting for replica %q name to be released: %+v", configurationStoreReplicaId, err) + } + } + + return nil +} + +func resourceConfigurationStoreReplicaWaitForNameAvailable(ctx context.Context, client *operations.OperationsClient, configurationStoreReplicaId replicas.ReplicaId) error { + deadline, ok := ctx.Deadline() + if !ok { + return fmt.Errorf("internal error: context had no deadline") + } + state := &pluginsdk.StateChangeConf{ + MinTimeout: 10 * time.Second, + ContinuousTargetOccurence: 2, + Pending: []string{"Unavailable"}, + Target: []string{"Available"}, + Refresh: resourceConfigurationStoreReplicaNameAvailabilityRefreshFunc(ctx, client, configurationStoreReplicaId), + Timeout: time.Until(deadline), + } + + if _, err := state.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for the name from %s to become available: %+v", configurationStoreReplicaId, err) + } + + return nil +} + +func resourceConfigurationStoreReplicaNameAvailabilityRefreshFunc(ctx context.Context, client *operations.OperationsClient, configurationStoreReplicaId replicas.ReplicaId) pluginsdk.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("[DEBUG] Checking to see if the name for %s is available ..", configurationStoreReplicaId) + + subscriptionId := commonids.NewSubscriptionID(configurationStoreReplicaId.SubscriptionId) + + parameters := operations.CheckNameAvailabilityParameters{ + Name: fmt.Sprintf("%s-%s", configurationStoreReplicaId.ConfigurationStoreName, configurationStoreReplicaId.ReplicaName), + Type: operations.ConfigurationResourceTypeMicrosoftPointAppConfigurationConfigurationStores, + } + + resp, err := client.CheckNameAvailability(ctx, subscriptionId, parameters) + if err != nil { + return resp, "Error", fmt.Errorf("retrieving Deployment: %+v", err) + } + + if resp.Model == nil { + return resp, "Error", fmt.Errorf("unexpected null model of %s", configurationStoreReplicaId) + } + + if resp.Model.NameAvailable == nil { + return resp, "Error", fmt.Errorf("unexpected null NameAvailable property of %s", configurationStoreReplicaId) + } + + if !*resp.Model.NameAvailable { + return resp, "Unavailable", nil + } + return resp, "Available", nil + } +} diff --git a/internal/services/appconfiguration/app_configuration_resource_test.go b/internal/services/appconfiguration/app_configuration_resource_test.go index 76c01eaf8707..ba7b21f2462d 100644 --- a/internal/services/appconfiguration/app_configuration_resource_test.go +++ b/internal/services/appconfiguration/app_configuration_resource_test.go @@ -185,6 +185,79 @@ func TestAccAppConfiguration_encryption(t *testing.T) { }) } +func TestAccAppConfiguration_replica(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_configuration", "test") + r := AppConfigurationResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.replica(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccAppConfiguration_replicaRemove(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_configuration", "test") + r := AppConfigurationResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.replica(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.standard(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccAppConfiguration_replicaUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_configuration", "test") + r := AppConfigurationResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.standard(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.replica(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.replicaUpdated(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccAppConfiguration_encryptionUpdated(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_app_configuration", "test") r := AppConfigurationResource{} @@ -419,6 +492,61 @@ resource "azurerm_app_configuration" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } +func (AppConfigurationResource) replica(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-appconfig-%d" + location = "%s" +} + +resource "azurerm_app_configuration" "test" { + name = "testaccappconf%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku = "standard" + + replica { + name = "replica1" + location = "%s" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.Locations.Ternary) +} + +func (AppConfigurationResource) replicaUpdated(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-appconfig-%d" + location = "%s" +} + +resource "azurerm_app_configuration" "test" { + name = "testaccappconf%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku = "standard" + + replica { + name = "replica1" + location = "%s" + } + + replica { + name = "replica2" + location = "%s" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.Locations.Ternary, data.Locations.Secondary) +} + func (r AppConfigurationResource) requiresImport(data acceptance.TestData) string { template := r.standard(data) return fmt.Sprintf(` @@ -527,6 +655,16 @@ resource "azurerm_app_configuration" "test" { identity_client_id = azurerm_user_assigned_identity.test.client_id } + replica { + name = "replica1" + location = "%[3]s" + } + + replica { + name = "replica2" + location = "%[4]s" + } + tags = { environment = "development" } @@ -536,7 +674,7 @@ resource "azurerm_app_configuration" "test" { azurerm_key_vault_access_policy.server, ] } -`, data.RandomInteger, data.Locations.Primary) +`, data.RandomInteger, data.Locations.Primary, data.Locations.Secondary, data.Locations.Ternary) } func (AppConfigurationResource) identity(data acceptance.TestData) string { diff --git a/internal/services/appconfiguration/client/client.go b/internal/services/appconfiguration/client/client.go index 2053af90c79c..9cb616eb0438 100644 --- a/internal/services/appconfiguration/client/client.go +++ b/internal/services/appconfiguration/client/client.go @@ -4,14 +4,13 @@ package client import ( - "context" "fmt" "github.com/Azure/go-autorest/autorest" - "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/configurationstores" "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/deletedconfigurationstores" "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/operations" + "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas" authWrapper "github.com/hashicorp/go-azure-sdk/sdk/auth/autorest" "github.com/hashicorp/go-azure-sdk/sdk/environments" "github.com/hashicorp/terraform-provider-azurerm/internal/common" @@ -23,6 +22,7 @@ type Client struct { ConfigurationStoresClient *configurationstores.ConfigurationStoresClient DeletedConfigurationStoresClient *deletedconfigurationstores.DeletedConfigurationStoresClient OperationsClient *operations.OperationsClient + ReplicasClient *replicas.ReplicasClient authorizerFunc common.ApiAuthorizerFunc configureClientFunc func(c *autorest.Client, authorizer autorest.Authorizer) } @@ -54,73 +54,6 @@ func (c Client) LinkWorkaroundDataPlaneClientWithEndpoint(configurationStoreEndp return &workaroundClient, nil } -func (c Client) DataPlaneClient(ctx context.Context, configurationStoreId string) (*appconfiguration.BaseClient, error) { - appConfigId, err := configurationstores.ParseConfigurationStoreID(configurationStoreId) - if err != nil { - return nil, err - } - - // TODO: caching all of this - appConfig, err := c.ConfigurationStoresClient.Get(ctx, *appConfigId) - if err != nil { - if response.WasNotFound(appConfig.HttpResponse) { - return nil, nil - } - - return nil, err - } - - if appConfig.Model == nil || appConfig.Model.Properties == nil || appConfig.Model.Properties.Endpoint == nil { - return nil, fmt.Errorf("endpoint was nil") - } - - endpoint := *appConfig.Model.Properties.Endpoint - - api := environments.NewApiEndpoint("AppConfiguration", endpoint, nil) - appConfigAuth, err := c.authorizerFunc(api) - if err != nil { - return nil, fmt.Errorf("obtaining auth token for %q: %+v", endpoint, err) - } - - client := appconfiguration.NewWithoutDefaults("", endpoint) - c.configureClientFunc(&client.Client, authWrapper.AutorestAuthorizer(appConfigAuth)) - - return &client, nil -} - -func (c Client) LinkWorkaroundDataPlaneClient(ctx context.Context, configurationStoreId string) (*azuresdkhacks.DataPlaneClient, error) { - appConfigId, err := configurationstores.ParseConfigurationStoreID(configurationStoreId) - if err != nil { - return nil, err - } - - // TODO: caching all of this - appConfig, err := c.ConfigurationStoresClient.Get(ctx, *appConfigId) - if err != nil { - if response.WasNotFound(appConfig.HttpResponse) { - return nil, nil - } - - return nil, err - } - - if appConfig.Model == nil || appConfig.Model.Properties == nil || appConfig.Model.Properties.Endpoint == nil { - return nil, fmt.Errorf("endpoint was nil") - } - - api := environments.NewApiEndpoint("AppConfiguration", *appConfig.Model.Properties.Endpoint, nil) - appConfigAuth, err := c.authorizerFunc(api) - if err != nil { - return nil, fmt.Errorf("obtaining auth token for %q: %+v", *appConfig.Model.Properties.Endpoint, err) - } - - client := appconfiguration.NewWithoutDefaults("", *appConfig.Model.Properties.Endpoint) - c.configureClientFunc(&client.Client, authWrapper.AutorestAuthorizer(appConfigAuth)) - workaroundClient := azuresdkhacks.NewDataPlaneClient(client) - - return &workaroundClient, nil -} - func NewClient(o *common.ClientOptions) (*Client, error) { configurationStores, err := configurationstores.NewConfigurationStoresClientWithBaseURI(o.Environment.ResourceManager) if err != nil { @@ -140,10 +73,17 @@ func NewClient(o *common.ClientOptions) (*Client, error) { } o.Configure(operationsClient.Client, o.Authorizers.ResourceManager) + replicasClient, err := replicas.NewReplicasClientWithBaseURI(o.Environment.ResourceManager) + if err != nil { + return nil, fmt.Errorf("building DeletedConfigurationStores client: %+v", err) + } + o.Configure(replicasClient.Client, o.Authorizers.ResourceManager) + return &Client{ ConfigurationStoresClient: configurationStores, DeletedConfigurationStoresClient: deletedConfigurationStores, OperationsClient: operationsClient, + ReplicasClient: replicasClient, authorizerFunc: o.Authorizers.AuthorizerFunc, configureClientFunc: o.ConfigureClient, }, nil diff --git a/internal/services/appconfiguration/validate/configuration_store_replica_name.go b/internal/services/appconfiguration/validate/configuration_store_replica_name.go new file mode 100644 index 000000000000..9e7f513f7684 --- /dev/null +++ b/internal/services/appconfiguration/validate/configuration_store_replica_name.go @@ -0,0 +1,15 @@ +package validate + +import ( + "fmt" + "regexp" +) + +func ConfigurationStoreReplicaName(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + if matched := regexp.MustCompile(`^[a-zA-Z0-9]{1,50}$`).Match([]byte(value)); !matched { + errors = append(errors, fmt.Errorf("%q Replica name may only contain alphanumeric characters and must be between 1-50 chars", k)) + } + + return warnings, errors +} diff --git a/internal/services/appconfiguration/validate/configuration_store_replica_name_test.go b/internal/services/appconfiguration/validate/configuration_store_replica_name_test.go new file mode 100644 index 000000000000..f0db179e91bd --- /dev/null +++ b/internal/services/appconfiguration/validate/configuration_store_replica_name_test.go @@ -0,0 +1,60 @@ +package validate + +import ( + "testing" +) + +func TestAppConfigurationReplicaName(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "four", + ErrCount: 0, + }, + { + Value: "5five", + ErrCount: 0, + }, + { + Value: "hello-world", + ErrCount: 1, + }, + { + Value: "hello_world", + ErrCount: 1, + }, + { + Value: "helloWorld", + ErrCount: 0, + }, + { + Value: "helloworld12", + ErrCount: 0, + }, + { + Value: "hello@world", + ErrCount: 1, + }, + { + Value: "qfvbdsbvipqdbwsbddbdcwqffewsqwcdw21ddwqwd3324120", + ErrCount: 0, + }, + { + Value: "qfvbdsbvipqdbwsbddbdcwqffewsqwcdw21ddwqwd332412020", + ErrCount: 0, + }, + { + Value: "qfvbdsbvipqdbwsbddbdcwqfjjfewsqwcdw21ddwqwd33241201", + ErrCount: 1, + }, + } + + for _, tc := range cases { + _, errors := ConfigurationStoreReplicaName(tc.Value, "azurerm_app_configuration") + if len(errors) != tc.ErrCount { + t.Fatalf("Expected the Azure App Configuration Replica Name to trigger a validation error: %v", tc) + } + } +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/README.md b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/README.md new file mode 100644 index 000000000000..2e4d4d60eb24 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/README.md @@ -0,0 +1,82 @@ + +## `github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas` Documentation + +The `replicas` SDK allows for interaction with the Azure Resource Manager Service `appconfiguration` (API Version `2023-03-01`). + +This readme covers example usages, but further information on [using this SDK can be found in the project root](https://github.com/hashicorp/go-azure-sdk/tree/main/docs). + +### Import Path + +```go +import "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas" +``` + + +### Client Initialization + +```go +client := replicas.NewReplicasClientWithBaseURI("https://management.azure.com") +client.Client.Authorizer = authorizer +``` + + +### Example Usage: `ReplicasClient.Create` + +```go +ctx := context.TODO() +id := replicas.NewReplicaID("12345678-1234-9876-4563-123456789012", "example-resource-group", "configurationStoreValue", "replicaValue") + +payload := replicas.Replica{ + // ... +} + + +if err := client.CreateThenPoll(ctx, id, payload); err != nil { + // handle the error +} +``` + + +### Example Usage: `ReplicasClient.Delete` + +```go +ctx := context.TODO() +id := replicas.NewReplicaID("12345678-1234-9876-4563-123456789012", "example-resource-group", "configurationStoreValue", "replicaValue") + +if err := client.DeleteThenPoll(ctx, id); err != nil { + // handle the error +} +``` + + +### Example Usage: `ReplicasClient.Get` + +```go +ctx := context.TODO() +id := replicas.NewReplicaID("12345678-1234-9876-4563-123456789012", "example-resource-group", "configurationStoreValue", "replicaValue") + +read, err := client.Get(ctx, id) +if err != nil { + // handle the error +} +if model := read.Model; model != nil { + // do something with the model/response object +} +``` + + +### Example Usage: `ReplicasClient.ListByConfigurationStore` + +```go +ctx := context.TODO() +id := replicas.NewConfigurationStoreID("12345678-1234-9876-4563-123456789012", "example-resource-group", "configurationStoreValue") + +// alternatively `client.ListByConfigurationStore(ctx, id)` can be used to do batched pagination +items, err := client.ListByConfigurationStoreComplete(ctx, id) +if err != nil { + // handle the error +} +for _, item := range items { + // do something +} +``` diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/client.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/client.go new file mode 100644 index 000000000000..084e9f434364 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/client.go @@ -0,0 +1,26 @@ +package replicas + +import ( + "fmt" + + "github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager" + "github.com/hashicorp/go-azure-sdk/sdk/environments" +) + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +type ReplicasClient struct { + Client *resourcemanager.Client +} + +func NewReplicasClientWithBaseURI(api environments.Api) (*ReplicasClient, error) { + client, err := resourcemanager.NewResourceManagerClient(api, "replicas", defaultApiVersion) + if err != nil { + return nil, fmt.Errorf("instantiating ReplicasClient: %+v", err) + } + + return &ReplicasClient{ + Client: client, + }, nil +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/constants.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/constants.go new file mode 100644 index 000000000000..2a2698617064 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/constants.go @@ -0,0 +1,60 @@ +package replicas + +import ( + "encoding/json" + "fmt" + "strings" +) + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +type ReplicaProvisioningState string + +const ( + ReplicaProvisioningStateCanceled ReplicaProvisioningState = "Canceled" + ReplicaProvisioningStateCreating ReplicaProvisioningState = "Creating" + ReplicaProvisioningStateDeleting ReplicaProvisioningState = "Deleting" + ReplicaProvisioningStateFailed ReplicaProvisioningState = "Failed" + ReplicaProvisioningStateSucceeded ReplicaProvisioningState = "Succeeded" +) + +func PossibleValuesForReplicaProvisioningState() []string { + return []string{ + string(ReplicaProvisioningStateCanceled), + string(ReplicaProvisioningStateCreating), + string(ReplicaProvisioningStateDeleting), + string(ReplicaProvisioningStateFailed), + string(ReplicaProvisioningStateSucceeded), + } +} + +func (s *ReplicaProvisioningState) UnmarshalJSON(bytes []byte) error { + var decoded string + if err := json.Unmarshal(bytes, &decoded); err != nil { + return fmt.Errorf("unmarshaling: %+v", err) + } + out, err := parseReplicaProvisioningState(decoded) + if err != nil { + return fmt.Errorf("parsing %q: %+v", decoded, err) + } + *s = *out + return nil +} + +func parseReplicaProvisioningState(input string) (*ReplicaProvisioningState, error) { + vals := map[string]ReplicaProvisioningState{ + "canceled": ReplicaProvisioningStateCanceled, + "creating": ReplicaProvisioningStateCreating, + "deleting": ReplicaProvisioningStateDeleting, + "failed": ReplicaProvisioningStateFailed, + "succeeded": ReplicaProvisioningStateSucceeded, + } + if v, ok := vals[strings.ToLower(input)]; ok { + return &v, nil + } + + // otherwise presume it's an undefined value and best-effort it + out := ReplicaProvisioningState(input) + return &out, nil +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/id_configurationstore.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/id_configurationstore.go new file mode 100644 index 000000000000..64c7ad6a2440 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/id_configurationstore.go @@ -0,0 +1,127 @@ +package replicas + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +var _ resourceids.ResourceId = ConfigurationStoreId{} + +// ConfigurationStoreId is a struct representing the Resource ID for a Configuration Store +type ConfigurationStoreId struct { + SubscriptionId string + ResourceGroupName string + ConfigurationStoreName string +} + +// NewConfigurationStoreID returns a new ConfigurationStoreId struct +func NewConfigurationStoreID(subscriptionId string, resourceGroupName string, configurationStoreName string) ConfigurationStoreId { + return ConfigurationStoreId{ + SubscriptionId: subscriptionId, + ResourceGroupName: resourceGroupName, + ConfigurationStoreName: configurationStoreName, + } +} + +// ParseConfigurationStoreID parses 'input' into a ConfigurationStoreId +func ParseConfigurationStoreID(input string) (*ConfigurationStoreId, error) { + parser := resourceids.NewParserFromResourceIdType(ConfigurationStoreId{}) + parsed, err := parser.Parse(input, false) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + var ok bool + id := ConfigurationStoreId{} + + if id.SubscriptionId, ok = parsed.Parsed["subscriptionId"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "subscriptionId", *parsed) + } + + if id.ResourceGroupName, ok = parsed.Parsed["resourceGroupName"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "resourceGroupName", *parsed) + } + + if id.ConfigurationStoreName, ok = parsed.Parsed["configurationStoreName"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "configurationStoreName", *parsed) + } + + return &id, nil +} + +// ParseConfigurationStoreIDInsensitively parses 'input' case-insensitively into a ConfigurationStoreId +// note: this method should only be used for API response data and not user input +func ParseConfigurationStoreIDInsensitively(input string) (*ConfigurationStoreId, error) { + parser := resourceids.NewParserFromResourceIdType(ConfigurationStoreId{}) + parsed, err := parser.Parse(input, true) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + var ok bool + id := ConfigurationStoreId{} + + if id.SubscriptionId, ok = parsed.Parsed["subscriptionId"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "subscriptionId", *parsed) + } + + if id.ResourceGroupName, ok = parsed.Parsed["resourceGroupName"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "resourceGroupName", *parsed) + } + + if id.ConfigurationStoreName, ok = parsed.Parsed["configurationStoreName"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "configurationStoreName", *parsed) + } + + return &id, nil +} + +// ValidateConfigurationStoreID checks that 'input' can be parsed as a Configuration Store ID +func ValidateConfigurationStoreID(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 := ParseConfigurationStoreID(v); err != nil { + errors = append(errors, err) + } + + return +} + +// ID returns the formatted Configuration Store ID +func (id ConfigurationStoreId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.AppConfiguration/configurationStores/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroupName, id.ConfigurationStoreName) +} + +// Segments returns a slice of Resource ID Segments which comprise this Configuration Store ID +func (id ConfigurationStoreId) Segments() []resourceids.Segment { + return []resourceids.Segment{ + resourceids.StaticSegment("staticSubscriptions", "subscriptions", "subscriptions"), + resourceids.SubscriptionIdSegment("subscriptionId", "12345678-1234-9876-4563-123456789012"), + resourceids.StaticSegment("staticResourceGroups", "resourceGroups", "resourceGroups"), + resourceids.ResourceGroupSegment("resourceGroupName", "example-resource-group"), + resourceids.StaticSegment("staticProviders", "providers", "providers"), + resourceids.ResourceProviderSegment("staticMicrosoftAppConfiguration", "Microsoft.AppConfiguration", "Microsoft.AppConfiguration"), + resourceids.StaticSegment("staticConfigurationStores", "configurationStores", "configurationStores"), + resourceids.UserSpecifiedSegment("configurationStoreName", "configurationStoreValue"), + } +} + +// String returns a human-readable description of this Configuration Store ID +func (id ConfigurationStoreId) String() string { + components := []string{ + fmt.Sprintf("Subscription: %q", id.SubscriptionId), + fmt.Sprintf("Resource Group Name: %q", id.ResourceGroupName), + fmt.Sprintf("Configuration Store Name: %q", id.ConfigurationStoreName), + } + return fmt.Sprintf("Configuration Store (%s)", strings.Join(components, "\n")) +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/id_replica.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/id_replica.go new file mode 100644 index 000000000000..464e8bae7e91 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/id_replica.go @@ -0,0 +1,140 @@ +package replicas + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +var _ resourceids.ResourceId = ReplicaId{} + +// ReplicaId is a struct representing the Resource ID for a Replica +type ReplicaId struct { + SubscriptionId string + ResourceGroupName string + ConfigurationStoreName string + ReplicaName string +} + +// NewReplicaID returns a new ReplicaId struct +func NewReplicaID(subscriptionId string, resourceGroupName string, configurationStoreName string, replicaName string) ReplicaId { + return ReplicaId{ + SubscriptionId: subscriptionId, + ResourceGroupName: resourceGroupName, + ConfigurationStoreName: configurationStoreName, + ReplicaName: replicaName, + } +} + +// ParseReplicaID parses 'input' into a ReplicaId +func ParseReplicaID(input string) (*ReplicaId, error) { + parser := resourceids.NewParserFromResourceIdType(ReplicaId{}) + parsed, err := parser.Parse(input, false) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + var ok bool + id := ReplicaId{} + + if id.SubscriptionId, ok = parsed.Parsed["subscriptionId"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "subscriptionId", *parsed) + } + + if id.ResourceGroupName, ok = parsed.Parsed["resourceGroupName"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "resourceGroupName", *parsed) + } + + if id.ConfigurationStoreName, ok = parsed.Parsed["configurationStoreName"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "configurationStoreName", *parsed) + } + + if id.ReplicaName, ok = parsed.Parsed["replicaName"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "replicaName", *parsed) + } + + return &id, nil +} + +// ParseReplicaIDInsensitively parses 'input' case-insensitively into a ReplicaId +// note: this method should only be used for API response data and not user input +func ParseReplicaIDInsensitively(input string) (*ReplicaId, error) { + parser := resourceids.NewParserFromResourceIdType(ReplicaId{}) + parsed, err := parser.Parse(input, true) + if err != nil { + return nil, fmt.Errorf("parsing %q: %+v", input, err) + } + + var ok bool + id := ReplicaId{} + + if id.SubscriptionId, ok = parsed.Parsed["subscriptionId"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "subscriptionId", *parsed) + } + + if id.ResourceGroupName, ok = parsed.Parsed["resourceGroupName"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "resourceGroupName", *parsed) + } + + if id.ConfigurationStoreName, ok = parsed.Parsed["configurationStoreName"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "configurationStoreName", *parsed) + } + + if id.ReplicaName, ok = parsed.Parsed["replicaName"]; !ok { + return nil, resourceids.NewSegmentNotSpecifiedError(id, "replicaName", *parsed) + } + + return &id, nil +} + +// ValidateReplicaID checks that 'input' can be parsed as a Replica ID +func ValidateReplicaID(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 := ParseReplicaID(v); err != nil { + errors = append(errors, err) + } + + return +} + +// ID returns the formatted Replica ID +func (id ReplicaId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.AppConfiguration/configurationStores/%s/replicas/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroupName, id.ConfigurationStoreName, id.ReplicaName) +} + +// Segments returns a slice of Resource ID Segments which comprise this Replica ID +func (id ReplicaId) Segments() []resourceids.Segment { + return []resourceids.Segment{ + resourceids.StaticSegment("staticSubscriptions", "subscriptions", "subscriptions"), + resourceids.SubscriptionIdSegment("subscriptionId", "12345678-1234-9876-4563-123456789012"), + resourceids.StaticSegment("staticResourceGroups", "resourceGroups", "resourceGroups"), + resourceids.ResourceGroupSegment("resourceGroupName", "example-resource-group"), + resourceids.StaticSegment("staticProviders", "providers", "providers"), + resourceids.ResourceProviderSegment("staticMicrosoftAppConfiguration", "Microsoft.AppConfiguration", "Microsoft.AppConfiguration"), + resourceids.StaticSegment("staticConfigurationStores", "configurationStores", "configurationStores"), + resourceids.UserSpecifiedSegment("configurationStoreName", "configurationStoreValue"), + resourceids.StaticSegment("staticReplicas", "replicas", "replicas"), + resourceids.UserSpecifiedSegment("replicaName", "replicaValue"), + } +} + +// String returns a human-readable description of this Replica ID +func (id ReplicaId) String() string { + components := []string{ + fmt.Sprintf("Subscription: %q", id.SubscriptionId), + fmt.Sprintf("Resource Group Name: %q", id.ResourceGroupName), + fmt.Sprintf("Configuration Store Name: %q", id.ConfigurationStoreName), + fmt.Sprintf("Replica Name: %q", id.ReplicaName), + } + return fmt.Sprintf("Replica (%s)", strings.Join(components, "\n")) +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_create.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_create.go new file mode 100644 index 000000000000..d9774267ac91 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_create.go @@ -0,0 +1,74 @@ +package replicas + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/go-azure-sdk/sdk/client" + "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" + "github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager" + "github.com/hashicorp/go-azure-sdk/sdk/odata" +) + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +type CreateOperationResponse struct { + Poller pollers.Poller + HttpResponse *http.Response + OData *odata.OData +} + +// Create ... +func (c ReplicasClient) Create(ctx context.Context, id ReplicaId, input Replica) (result CreateOperationResponse, err error) { + opts := client.RequestOptions{ + ContentType: "application/json", + ExpectedStatusCodes: []int{ + http.StatusCreated, + http.StatusOK, + }, + HttpMethod: http.MethodPut, + Path: id.ID(), + } + + req, err := c.Client.NewRequest(ctx, opts) + if err != nil { + return + } + + if err = req.Marshal(input); err != nil { + return + } + + var resp *client.Response + resp, err = req.Execute(ctx) + if resp != nil { + result.OData = resp.OData + result.HttpResponse = resp.Response + } + if err != nil { + return + } + + result.Poller, err = resourcemanager.PollerFromResponse(resp, c.Client) + if err != nil { + return + } + + return +} + +// CreateThenPoll performs Create then polls until it's completed +func (c ReplicasClient) CreateThenPoll(ctx context.Context, id ReplicaId, input Replica) error { + result, err := c.Create(ctx, id, input) + if err != nil { + return fmt.Errorf("performing Create: %+v", err) + } + + if err := result.Poller.PollUntilDone(ctx); err != nil { + return fmt.Errorf("polling after Create: %+v", err) + } + + return nil +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_delete.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_delete.go new file mode 100644 index 000000000000..68fe181f8c39 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_delete.go @@ -0,0 +1,71 @@ +package replicas + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/go-azure-sdk/sdk/client" + "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" + "github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager" + "github.com/hashicorp/go-azure-sdk/sdk/odata" +) + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +type DeleteOperationResponse struct { + Poller pollers.Poller + HttpResponse *http.Response + OData *odata.OData +} + +// Delete ... +func (c ReplicasClient) Delete(ctx context.Context, id ReplicaId) (result DeleteOperationResponse, err error) { + opts := client.RequestOptions{ + ContentType: "application/json", + ExpectedStatusCodes: []int{ + http.StatusAccepted, + http.StatusNoContent, + http.StatusOK, + }, + HttpMethod: http.MethodDelete, + Path: id.ID(), + } + + req, err := c.Client.NewRequest(ctx, opts) + if err != nil { + return + } + + var resp *client.Response + resp, err = req.Execute(ctx) + if resp != nil { + result.OData = resp.OData + result.HttpResponse = resp.Response + } + if err != nil { + return + } + + result.Poller, err = resourcemanager.PollerFromResponse(resp, c.Client) + if err != nil { + return + } + + return +} + +// DeleteThenPoll performs Delete then polls until it's completed +func (c ReplicasClient) DeleteThenPoll(ctx context.Context, id ReplicaId) error { + result, err := c.Delete(ctx, id) + if err != nil { + return fmt.Errorf("performing Delete: %+v", err) + } + + if err := result.Poller.PollUntilDone(ctx); err != nil { + return fmt.Errorf("polling after Delete: %+v", err) + } + + return nil +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_get.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_get.go new file mode 100644 index 000000000000..86f7742e95dd --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_get.go @@ -0,0 +1,51 @@ +package replicas + +import ( + "context" + "net/http" + + "github.com/hashicorp/go-azure-sdk/sdk/client" + "github.com/hashicorp/go-azure-sdk/sdk/odata" +) + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +type GetOperationResponse struct { + HttpResponse *http.Response + OData *odata.OData + Model *Replica +} + +// Get ... +func (c ReplicasClient) Get(ctx context.Context, id ReplicaId) (result GetOperationResponse, err error) { + opts := client.RequestOptions{ + ContentType: "application/json", + ExpectedStatusCodes: []int{ + http.StatusOK, + }, + HttpMethod: http.MethodGet, + Path: id.ID(), + } + + req, err := c.Client.NewRequest(ctx, opts) + if err != nil { + return + } + + var resp *client.Response + resp, err = req.Execute(ctx) + if resp != nil { + result.OData = resp.OData + result.HttpResponse = resp.Response + } + if err != nil { + return + } + + if err = resp.Unmarshal(&result.Model); err != nil { + return + } + + return +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_listbyconfigurationstore.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_listbyconfigurationstore.go new file mode 100644 index 000000000000..eafe2251a0a3 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_listbyconfigurationstore.go @@ -0,0 +1,89 @@ +package replicas + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/go-azure-sdk/sdk/client" + "github.com/hashicorp/go-azure-sdk/sdk/odata" +) + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +type ListByConfigurationStoreOperationResponse struct { + HttpResponse *http.Response + OData *odata.OData + Model *[]Replica +} + +type ListByConfigurationStoreCompleteResult struct { + Items []Replica +} + +// ListByConfigurationStore ... +func (c ReplicasClient) ListByConfigurationStore(ctx context.Context, id ConfigurationStoreId) (result ListByConfigurationStoreOperationResponse, err error) { + opts := client.RequestOptions{ + ContentType: "application/json", + ExpectedStatusCodes: []int{ + http.StatusOK, + }, + HttpMethod: http.MethodGet, + Path: fmt.Sprintf("%s/replicas", id.ID()), + } + + req, err := c.Client.NewRequest(ctx, opts) + if err != nil { + return + } + + var resp *client.Response + resp, err = req.ExecutePaged(ctx) + if resp != nil { + result.OData = resp.OData + result.HttpResponse = resp.Response + } + if err != nil { + return + } + + var values struct { + Values *[]Replica `json:"value"` + } + if err = resp.Unmarshal(&values); err != nil { + return + } + + result.Model = values.Values + + return +} + +// ListByConfigurationStoreComplete retrieves all the results into a single object +func (c ReplicasClient) ListByConfigurationStoreComplete(ctx context.Context, id ConfigurationStoreId) (ListByConfigurationStoreCompleteResult, error) { + return c.ListByConfigurationStoreCompleteMatchingPredicate(ctx, id, ReplicaOperationPredicate{}) +} + +// ListByConfigurationStoreCompleteMatchingPredicate retrieves all the results and then applies the predicate +func (c ReplicasClient) ListByConfigurationStoreCompleteMatchingPredicate(ctx context.Context, id ConfigurationStoreId, predicate ReplicaOperationPredicate) (result ListByConfigurationStoreCompleteResult, err error) { + items := make([]Replica, 0) + + resp, err := c.ListByConfigurationStore(ctx, id) + if err != nil { + err = fmt.Errorf("loading results: %+v", err) + return + } + if resp.Model != nil { + for _, v := range *resp.Model { + if predicate.Matches(v) { + items = append(items, v) + } + } + } + + result = ListByConfigurationStoreCompleteResult{ + Items: items, + } + return +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/model_replica.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/model_replica.go new file mode 100644 index 000000000000..95deda8039b9 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/model_replica.go @@ -0,0 +1,17 @@ +package replicas + +import ( + "github.com/hashicorp/go-azure-helpers/resourcemanager/systemdata" +) + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +type Replica struct { + Id *string `json:"id,omitempty"` + Location *string `json:"location,omitempty"` + Name *string `json:"name,omitempty"` + Properties *ReplicaProperties `json:"properties,omitempty"` + SystemData *systemdata.SystemData `json:"systemData,omitempty"` + Type *string `json:"type,omitempty"` +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/model_replicaproperties.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/model_replicaproperties.go new file mode 100644 index 000000000000..361a71b2a0d2 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/model_replicaproperties.go @@ -0,0 +1,9 @@ +package replicas + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +type ReplicaProperties struct { + Endpoint *string `json:"endpoint,omitempty"` + ProvisioningState *ReplicaProvisioningState `json:"provisioningState,omitempty"` +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/predicates.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/predicates.go new file mode 100644 index 000000000000..9dd3706f57c5 --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/predicates.go @@ -0,0 +1,32 @@ +package replicas + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +type ReplicaOperationPredicate struct { + Id *string + Location *string + Name *string + Type *string +} + +func (p ReplicaOperationPredicate) Matches(input Replica) bool { + + if p.Id != nil && (input.Id == nil && *p.Id != *input.Id) { + return false + } + + if p.Location != nil && (input.Location == nil && *p.Location != *input.Location) { + return false + } + + if p.Name != nil && (input.Name == nil && *p.Name != *input.Name) { + return false + } + + if p.Type != nil && (input.Type == nil && *p.Type != *input.Type) { + return false + } + + return true +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/version.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/version.go new file mode 100644 index 000000000000..198b0d4d213b --- /dev/null +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/version.go @@ -0,0 +1,12 @@ +package replicas + +import "fmt" + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +const defaultApiVersion = "2023-03-01" + +func userAgent() string { + return fmt.Sprintf("hashicorp/go-azure-sdk/replicas/%s", defaultApiVersion) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 354c5ccb2256..7c1fd83c9770 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -216,6 +216,7 @@ github.com/hashicorp/go-azure-sdk/resource-manager/apimanagement/2021-08-01/user github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/configurationstores github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/deletedconfigurationstores github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/operations +github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas github.com/hashicorp/go-azure-sdk/resource-manager/applicationinsights/2020-11-20/workbooktemplatesapis github.com/hashicorp/go-azure-sdk/resource-manager/applicationinsights/2022-04-01/workbooksapis github.com/hashicorp/go-azure-sdk/resource-manager/applicationinsights/2022-06-15/webtestsapis diff --git a/website/docs/d/app_configuration.html.markdown b/website/docs/d/app_configuration.html.markdown index a61a8dd59528..bca3b2a054fc 100644 --- a/website/docs/d/app_configuration.html.markdown +++ b/website/docs/d/app_configuration.html.markdown @@ -53,6 +53,8 @@ In addition to the Arguments listed above - the following Attributes are exporte * `purge_protection_enabled` - Whether Purge Protection is enabled. +* `replica` - One or more `replica` blocks as defined below. + * `secondary_read_key` - A `secondary_read_key` block as defined below containing the secondary read access key. * `secondary_write_key` - A `secondary_write_key` block as defined below containing the secondary write access key. @@ -83,6 +85,19 @@ A `primary_write_key` block exports the following: * `secret` - The Secret of the Access Key. +--- + +A `replica` block exports the following: + +* `id` - The replica ID. + +* `endpoint` - The URL of the replica. + +* `location` - The supported Azure location where the replica exists. + +* `name` - (Optional) The name of the replica. + + --- A `secondary_read_key` block exports the following: diff --git a/website/docs/r/app_configuration.html.markdown b/website/docs/r/app_configuration.html.markdown index 5921934f6c4b..2b7e41c4b38e 100644 --- a/website/docs/r/app_configuration.html.markdown +++ b/website/docs/r/app_configuration.html.markdown @@ -127,6 +127,11 @@ resource "azurerm_app_configuration" "example" { identity_client_id = azurerm_user_assigned_identity.example.client_id } + replica { + name = "replica1" + location = "West US" + } + tags = { environment = "development" } @@ -164,6 +169,8 @@ The following arguments are supported: !> **Note:** Once Purge Protection has been enabled it's not possible to disable it. Deleting the App Configuration with Purge Protection enabled will schedule the App Configuration to be deleted (which will happen by Azure in the configured number of days). +* `replica` - (Optional) One or more `replica` blocks as defined below. + * `sku` - (Optional) The SKU name of the App Configuration. Possible values are `free` and `standard`. Defaults to `free`. * `soft_delete_retention_days` - (Optional) The number of days that items should be retained for once soft-deleted. This field only works for `standard` sku. This value can be between `1` and `7` days. Defaults to `7`. Changing this forces a new resource to be created. @@ -192,6 +199,14 @@ An `identity` block supports the following: --- +A `replica` block supports the following: + +* `location` - (Required) Specifies the supported Azure location where the replica exists. + +* `name` - (Optional) Specifies the name of the replica. Changing this forces a new replica to be created. + +--- + ## Attributes Reference In addition to the Arguments listed above - the following Attributes are exported: @@ -220,6 +235,14 @@ An `identity` block exports the following: --- +A `replica` block exports the following: + +* `id` - The replica ID. + +* `endpoint` - The URL of the replica. + +--- + A `primary_read_key` block exports the following: * `connection_string` - The Connection String for this Access Key - comprising of the Endpoint, ID and Secret. @@ -262,10 +285,10 @@ A `secondary_write_key` block exports the following: The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: -* `create` - (Defaults to 30 minutes) Used when creating the App Configuration. -* `update` - (Defaults to 30 minutes) Used when updating the App Configuration. +* `create` - (Defaults to 60 minutes) Used when creating the App Configuration. +* `update` - (Defaults to 60 minutes) Used when updating the App Configuration. * `read` - (Defaults to 5 minutes) Used when retrieving the App Configuration. -* `delete` - (Defaults to 30 minutes) Used when deleting the App Configuration. +* `delete` - (Defaults to 60 minutes) Used when deleting the App Configuration. ## Import From 26030d71d03f94543ec544460a3968bf85cd7896 Mon Sep 17 00:00:00 2001 From: teowa <104055472+teowa@users.noreply.github.com> Date: Fri, 15 Sep 2023 09:33:07 +0000 Subject: [PATCH 2/3] fix --- .../appconfiguration/app_configuration.go | 1 + .../app_configuration_data_source.go | 3 +- .../app_configuration_resource.go | 65 +++++++++---------- .../app_configuration_resource_test.go | 37 +++++++++++ .../2023-03-01/replicas/client.go | 6 +- .../2023-03-01/replicas/method_create.go | 2 +- .../2023-03-01/replicas/method_delete.go | 2 +- .../2023-03-01/replicas/method_get.go | 2 +- .../method_listbyconfigurationstore.go | 2 +- .../2023-03-01/replicas/predicates.go | 8 +-- .../docs/d/app_configuration.html.markdown | 2 +- .../docs/r/app_configuration.html.markdown | 8 +-- 12 files changed, 86 insertions(+), 52 deletions(-) diff --git a/internal/services/appconfiguration/app_configuration.go b/internal/services/appconfiguration/app_configuration.go index 31b317173b01..b54c2d691f1e 100644 --- a/internal/services/appconfiguration/app_configuration.go +++ b/internal/services/appconfiguration/app_configuration.go @@ -57,6 +57,7 @@ func resourceConfigurationStoreReplicaHash(input interface{}) int { var buf bytes.Buffer if rawData, ok := input.(map[string]interface{}); ok { buf.WriteString(rawData["name"].(string)) + buf.WriteString(rawData["location"].(string)) } return pluginsdk.HashString(buf.String()) } diff --git a/internal/services/appconfiguration/app_configuration_data_source.go b/internal/services/appconfiguration/app_configuration_data_source.go index 9ed8d064442c..f7f57558c422 100644 --- a/internal/services/appconfiguration/app_configuration_data_source.go +++ b/internal/services/appconfiguration/app_configuration_data_source.go @@ -95,9 +95,8 @@ func dataSourceAppConfiguration() *pluginsdk.Resource { }, "replica": { - Type: pluginsdk.TypeSet, + Type: pluginsdk.TypeList, Computed: true, - Set: resourceConfigurationStoreReplicaHash, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ "name": { diff --git a/internal/services/appconfiguration/app_configuration_resource.go b/internal/services/appconfiguration/app_configuration_resource.go index 9e5351704111..11528402395e 100644 --- a/internal/services/appconfiguration/app_configuration_resource.go +++ b/internal/services/appconfiguration/app_configuration_resource.go @@ -24,7 +24,6 @@ import ( "github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas" "github.com/hashicorp/go-azure-sdk/sdk/client" "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/services/appconfiguration/validate" @@ -145,13 +144,7 @@ func resourceAppConfiguration() *pluginsdk.Resource { Required: true, ValidateFunc: validate.ConfigurationStoreReplicaName, }, - "location": { - Type: pluginsdk.TypeString, - Required: true, - ValidateFunc: location.EnhancedValidate, - StateFunc: location.StateFunc, - DiffSuppressFunc: location.DiffSuppressFunc, - }, + "location": commonschema.LocationWithoutForceNew(), "endpoint": { Type: pluginsdk.TypeString, Computed: true, @@ -357,21 +350,14 @@ func resourceAppConfigurationCreate(d *pluginsdk.ResourceData, meta interface{}) } meta.(*clients.Client).AppConfiguration.AddToCache(resourceId, *resp.Model.Properties.Endpoint) - expandedReplicas, err := expandAppConfigurationReplicas(d.Get("replica").(*schema.Set).List(), name, location) + expandedReplicas, err := expandAppConfigurationReplicas(d.Get("replica").(*pluginsdk.Set).List(), name, location) if err != nil { return fmt.Errorf("expanding `replica`: %+v", err) } replicaClient := meta.(*clients.Client).AppConfiguration.ReplicasClient - for _, replica := range expandedReplicas { - replicaId := replicas.NewReplicaID(subscriptionId, resourceGroup, name, *replica.Name) - - existingReplica, err := replicaClient.Get(ctx, replicaId) - if err != nil { - if !response.WasNotFound(existingReplica.HttpResponse) { - return fmt.Errorf("retrieving %s: %+v", replicaId, err) - } - } + for _, replica := range *expandedReplicas { + replicaId := replicas.NewReplicaID(resourceId.SubscriptionId, resourceId.ResourceGroupName, resourceId.ConfigurationStoreName, *replica.Name) if err := replicaClient.CreateThenPoll(ctx, replicaId, replica); err != nil { return fmt.Errorf("creating %s: %+v", replicaId, err) @@ -494,6 +480,7 @@ func resourceAppConfigurationUpdate(d *pluginsdk.ResourceData, meta interface{}) // check if a replica has been removed from config and if so, delete it deleteReplicaIds := make([]replicas.ReplicaId, 0) + unchangedReplicaNames := make(map[string]struct{}, 0) oldReplicas, newReplicas := d.GetChange("replica") for _, oldReplica := range oldReplicas.(*pluginsdk.Set).List() { isRemoved := true @@ -502,7 +489,8 @@ func resourceAppConfigurationUpdate(d *pluginsdk.ResourceData, meta interface{}) for _, newReplica := range newReplicas.(*pluginsdk.Set).List() { newReplicaMap := newReplica.(map[string]interface{}) - if strings.EqualFold(oldReplicaMap["name"].(string), newReplicaMap["name"].(string)) && strings.EqualFold(oldReplicaMap["location"].(string), newReplicaMap["location"].(string)) { + if strings.EqualFold(oldReplicaMap["name"].(string), newReplicaMap["name"].(string)) && strings.EqualFold(location.Normalize(oldReplicaMap["location"].(string)), location.Normalize(newReplicaMap["location"].(string))) { + unchangedReplicaNames[oldReplicaMap["name"].(string)] = struct{}{} isRemoved = false break } @@ -517,13 +505,17 @@ func resourceAppConfigurationUpdate(d *pluginsdk.ResourceData, meta interface{}) return err } - expandedReplicas, err := expandAppConfigurationReplicas(d.Get("replica").(*schema.Set).List(), id.ConfigurationStoreName, location.Normalize(existing.Model.Location)) + expandedReplicas, err := expandAppConfigurationReplicas(d.Get("replica").(*pluginsdk.Set).List(), id.ConfigurationStoreName, location.Normalize(existing.Model.Location)) if err != nil { return fmt.Errorf("expanding `replica`: %+v", err) } // check if a replica has been added or an existing one changed its location, (re)create it - for _, replica := range expandedReplicas { + for _, replica := range *expandedReplicas { + if _, isUnchanged := unchangedReplicaNames[*replica.Name]; isUnchanged { + continue + } + replicaId := replicas.NewReplicaID(id.SubscriptionId, id.ResourceGroupName, id.ConfigurationStoreName, *replica.Name) existingReplica, err := replicaClient.Get(ctx, replicaId) @@ -534,7 +526,7 @@ func resourceAppConfigurationUpdate(d *pluginsdk.ResourceData, meta interface{}) } if !response.WasNotFound(existingReplica.HttpResponse) { - continue + return fmt.Errorf("updating %s: replica %s already exists", *id, replicaId) } if err = replicaClient.CreateThenPoll(ctx, replicaId, replica); err != nil { @@ -786,30 +778,35 @@ func expandAppConfigurationEncryption(input []interface{}) *configurationstores. return result } -func expandAppConfigurationReplicas(input []interface{}, configurationStoreName, configurationStoreLocation string) ([]replicas.Replica, error) { +func expandAppConfigurationReplicas(input []interface{}, configurationStoreName, configurationStoreLocation string) (*[]replicas.Replica, error) { result := make([]replicas.Replica, 0) - locationSet := map[string]interface{}{ - configurationStoreLocation: nil, - } - replicaNameSet := map[string]interface{}{} + // check if there are duplicated replica names or locations + // location cannot be same as original configuration store and other replicas + locationSet := make(map[string]string, 0) + replicaNameSet := make(map[string]struct{}, 0) for _, v := range input { replica := v.(map[string]interface{}) replicaName := replica["name"].(string) replicaLocation := location.Normalize(replica["location"].(string)) + if strings.EqualFold(replicaLocation, configurationStoreLocation) { + return nil, fmt.Errorf("location (%q) of replica %q is duplicated with original configuration store %q", replicaName, replicaLocation, configurationStoreName) + } - if _, ok := locationSet[replicaLocation]; ok { - return result, fmt.Errorf("replica location %q is duplicated in configuration store %q location", replicaLocation, configurationStoreName) + if name, ok := locationSet[replicaLocation]; ok { + return nil, fmt.Errorf("location (%q) of replica %q is duplicated with replica %q", replicaName, replicaLocation, name) } - locationSet[replicaLocation] = nil + locationSet[replicaLocation] = replicaName - if _, ok := replicaNameSet[replicaName]; ok { - return result, fmt.Errorf("replica name %q is duplicated", replicaName) + normalizedReplicaName := strings.ToLower(replicaName) + if _, ok := replicaNameSet[normalizedReplicaName]; ok { + return nil, fmt.Errorf("replica name %q is duplicated", replicaName) } + replicaNameSet[normalizedReplicaName] = struct{}{} if len(replicaName)+len(configurationStoreName) > 60 { - return result, fmt.Errorf("replica name %q is too long, the total length of replica name and configuration store name should be greater than 60", replicaName) + return nil, fmt.Errorf("replica name %q is too long, the total length of replica name and configuration store name should be greater than 60", replicaName) } result = append(result, replicas.Replica{ @@ -818,7 +815,7 @@ func expandAppConfigurationReplicas(input []interface{}, configurationStoreName, }) } - return result, nil + return &result, nil } func flattenAppConfigurationAccessKeys(values []configurationstores.ApiKey) flattenedAccessKeys { diff --git a/internal/services/appconfiguration/app_configuration_resource_test.go b/internal/services/appconfiguration/app_configuration_resource_test.go index ba7b21f2462d..7605b102cad3 100644 --- a/internal/services/appconfiguration/app_configuration_resource_test.go +++ b/internal/services/appconfiguration/app_configuration_resource_test.go @@ -248,6 +248,13 @@ func TestAccAppConfiguration_replicaUpdate(t *testing.T) { ), }, data.ImportStep(), + { + Config: r.replicaUpdatedPartial(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), { Config: r.complete(data), Check: acceptance.ComposeTestCheckFunc( @@ -547,6 +554,36 @@ resource "azurerm_app_configuration" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.Locations.Ternary, data.Locations.Secondary) } +func (AppConfigurationResource) replicaUpdatedPartial(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-appconfig-%d" + location = "%s" +} + +resource "azurerm_app_configuration" "test" { + name = "testaccappconf%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku = "standard" + + replica { + name = "replica3" + location = "%s" + } + + replica { + name = "replica2" + location = "%s" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.Locations.Ternary, data.Locations.Secondary) +} + func (r AppConfigurationResource) requiresImport(data acceptance.TestData) string { template := r.standard(data) return fmt.Sprintf(` diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/client.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/client.go index 084e9f434364..d971742c1bdc 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/client.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/client.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager" - "github.com/hashicorp/go-azure-sdk/sdk/environments" + sdkEnv "github.com/hashicorp/go-azure-sdk/sdk/environments" ) // Copyright (c) Microsoft Corporation. All rights reserved. @@ -14,8 +14,8 @@ type ReplicasClient struct { Client *resourcemanager.Client } -func NewReplicasClientWithBaseURI(api environments.Api) (*ReplicasClient, error) { - client, err := resourcemanager.NewResourceManagerClient(api, "replicas", defaultApiVersion) +func NewReplicasClientWithBaseURI(sdkApi sdkEnv.Api) (*ReplicasClient, error) { + client, err := resourcemanager.NewResourceManagerClient(sdkApi, "replicas", defaultApiVersion) if err != nil { return nil, fmt.Errorf("instantiating ReplicasClient: %+v", err) } diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_create.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_create.go index d9774267ac91..58b36592090f 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_create.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_create.go @@ -23,7 +23,7 @@ type CreateOperationResponse struct { // Create ... func (c ReplicasClient) Create(ctx context.Context, id ReplicaId, input Replica) (result CreateOperationResponse, err error) { opts := client.RequestOptions{ - ContentType: "application/json", + ContentType: "application/json; charset=utf-8", ExpectedStatusCodes: []int{ http.StatusCreated, http.StatusOK, diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_delete.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_delete.go index 68fe181f8c39..a245c7836583 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_delete.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_delete.go @@ -23,7 +23,7 @@ type DeleteOperationResponse struct { // Delete ... func (c ReplicasClient) Delete(ctx context.Context, id ReplicaId) (result DeleteOperationResponse, err error) { opts := client.RequestOptions{ - ContentType: "application/json", + ContentType: "application/json; charset=utf-8", ExpectedStatusCodes: []int{ http.StatusAccepted, http.StatusNoContent, diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_get.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_get.go index 86f7742e95dd..45e517f34af7 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_get.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_get.go @@ -20,7 +20,7 @@ type GetOperationResponse struct { // Get ... func (c ReplicasClient) Get(ctx context.Context, id ReplicaId) (result GetOperationResponse, err error) { opts := client.RequestOptions{ - ContentType: "application/json", + ContentType: "application/json; charset=utf-8", ExpectedStatusCodes: []int{ http.StatusOK, }, diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_listbyconfigurationstore.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_listbyconfigurationstore.go index eafe2251a0a3..01a7747d8076 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_listbyconfigurationstore.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/method_listbyconfigurationstore.go @@ -25,7 +25,7 @@ type ListByConfigurationStoreCompleteResult struct { // ListByConfigurationStore ... func (c ReplicasClient) ListByConfigurationStore(ctx context.Context, id ConfigurationStoreId) (result ListByConfigurationStoreOperationResponse, err error) { opts := client.RequestOptions{ - ContentType: "application/json", + ContentType: "application/json; charset=utf-8", ExpectedStatusCodes: []int{ http.StatusOK, }, diff --git a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/predicates.go b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/predicates.go index 9dd3706f57c5..436f7639b383 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/predicates.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/replicas/predicates.go @@ -12,19 +12,19 @@ type ReplicaOperationPredicate struct { func (p ReplicaOperationPredicate) Matches(input Replica) bool { - if p.Id != nil && (input.Id == nil && *p.Id != *input.Id) { + if p.Id != nil && (input.Id == nil || *p.Id != *input.Id) { return false } - if p.Location != nil && (input.Location == nil && *p.Location != *input.Location) { + if p.Location != nil && (input.Location == nil || *p.Location != *input.Location) { return false } - if p.Name != nil && (input.Name == nil && *p.Name != *input.Name) { + if p.Name != nil && (input.Name == nil || *p.Name != *input.Name) { return false } - if p.Type != nil && (input.Type == nil && *p.Type != *input.Type) { + if p.Type != nil && (input.Type == nil || *p.Type != *input.Type) { return false } diff --git a/website/docs/d/app_configuration.html.markdown b/website/docs/d/app_configuration.html.markdown index bca3b2a054fc..1ab49ecf9f7e 100644 --- a/website/docs/d/app_configuration.html.markdown +++ b/website/docs/d/app_configuration.html.markdown @@ -95,7 +95,7 @@ A `replica` block exports the following: * `location` - The supported Azure location where the replica exists. -* `name` - (Optional) The name of the replica. +* `name` - The name of the replica. --- diff --git a/website/docs/r/app_configuration.html.markdown b/website/docs/r/app_configuration.html.markdown index 2b7e41c4b38e..115b6f4cfab6 100644 --- a/website/docs/r/app_configuration.html.markdown +++ b/website/docs/r/app_configuration.html.markdown @@ -201,9 +201,9 @@ An `identity` block supports the following: A `replica` block supports the following: -* `location` - (Required) Specifies the supported Azure location where the replica exists. +* `location` - (Required) Specifies the supported Azure location where the replica exists. Changing this forces a new replica to be created. -* `name` - (Optional) Specifies the name of the replica. Changing this forces a new replica to be created. +* `name` - (Required) Specifies the name of the replica. Changing this forces a new replica to be created. --- @@ -237,9 +237,9 @@ An `identity` block exports the following: A `replica` block exports the following: -* `id` - The replica ID. +* `id` - The ID of the App Configuration Replica. -* `endpoint` - The URL of the replica. +* `endpoint` - The URL of the App Configuration Replica. --- From b387ca83cb717c9d085e29982be4f7ae573821ee Mon Sep 17 00:00:00 2001 From: Tao <104055472+teowa@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:19:42 +0800 Subject: [PATCH 3/3] fix datasource doc --- website/docs/d/app_configuration.html.markdown | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/d/app_configuration.html.markdown b/website/docs/d/app_configuration.html.markdown index 1ab49ecf9f7e..8a042fd13f86 100644 --- a/website/docs/d/app_configuration.html.markdown +++ b/website/docs/d/app_configuration.html.markdown @@ -89,13 +89,13 @@ A `primary_write_key` block exports the following: A `replica` block exports the following: -* `id` - The replica ID. +* `id` - The ID of the App Configuration Replica. -* `endpoint` - The URL of the replica. +* `endpoint` - The URL of the App Configuration Replica. -* `location` - The supported Azure location where the replica exists. +* `location` - The supported Azure location where the App Configuration Replica exists. -* `name` - The name of the replica. +* `name` - The name of the App Configuration Replica. ---