Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azurerm_app_configuration_key: Support for slashes in key #13859

Merged
merged 2 commits into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/url"
"time"

"github.com/Azure/go-autorest/autorest"
Expand Down Expand Up @@ -128,7 +129,7 @@ func (k KeyResource) Create() sdk.ResourceFunc {

appCfgKeyResourceID := parse.AppConfigurationKeyId{
ConfigurationStoreId: model.ConfigurationStoreId,
Key: model.Key,
Key: url.QueryEscape(model.Key),
Label: model.Label,
}

Expand Down Expand Up @@ -206,16 +207,21 @@ func (k KeyResource) Read() sdk.ResourceFunc {
return err
}

kv, err := client.GetKeyValue(ctx, resourceID.Key, resourceID.Label, "", "", "", []string{})
decodedKey, err := url.QueryUnescape(resourceID.Key)
if err != nil {
return fmt.Errorf("while decoding key of resource ID: %+v", err)
}

kv, err := client.GetKeyValue(ctx, decodedKey, resourceID.Label, "", "", "", []string{})
if err != nil {
if v, ok := err.(autorest.DetailedError); ok {
if utils.ResponseWasNotFound(autorest.Response{Response: v.Response}) {
return metadata.MarkAsGone(resourceID)
}
} else {
return fmt.Errorf("while checking for key's %q existence: %+v", resourceID.Key, err)
return fmt.Errorf("while checking for key's %q existence: %+v", decodedKey, err)
}
return fmt.Errorf("while checking for key's %q existence: %+v", resourceID.Key, err)
return fmt.Errorf("while checking for key's %q existence: %+v", decodedKey, err)
}

model := KeyResourceModel{
Expand Down Expand Up @@ -325,13 +331,18 @@ func (k KeyResource) Delete() sdk.ResourceFunc {
return err
}

if _, err = client.DeleteLock(ctx, resourceID.Key, resourceID.Label, "", ""); err != nil {
return fmt.Errorf("while unlocking key/label pair %s/%s: %+v", resourceID.Key, resourceID.Label, err)
decodedKey, err := url.QueryUnescape(resourceID.Key)
if err != nil {
return fmt.Errorf("while decoding key of resource ID: %+v", err)
}

if _, err = client.DeleteLock(ctx, decodedKey, resourceID.Label, "", ""); err != nil {
return fmt.Errorf("while unlocking key/label pair %s/%s: %+v", decodedKey, resourceID.Label, err)
}

_, err = client.DeleteKeyValue(ctx, resourceID.Key, resourceID.Label, "")
_, err = client.DeleteKeyValue(ctx, decodedKey, resourceID.Label, "")
if err != nil {
return fmt.Errorf("while removing key %q from App Configuration Store %q: %+v", resourceID.Key, resourceID.ConfigurationStoreId, err)
return fmt.Errorf("while removing key %q from App Configuration Store %q: %+v", decodedKey, resourceID.ConfigurationStoreId, err)
}

return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ func TestAccAppConfigurationKey_KVToVault(t *testing.T) {
})
}

func TestAccAppConfigurationKey_slash(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_app_configuration_key", "test")
r := AppConfigurationKeyResource{}
data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.slash(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
})
}

func TestAccAppConfigurationKey_requiresImport(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_app_configuration_key", "test")
r := AppConfigurationKeyResource{}
Expand Down Expand Up @@ -176,6 +190,34 @@ resource "azurerm_app_configuration_key" "test" {
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger)
}

func (t AppConfigurationKeyResource) slash(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 = "testacc-appconf%d"
resource_group_name = azurerm_resource_group.test.name
location = azurerm_resource_group.test.location
sku = "standard"
}

resource "azurerm_app_configuration_key" "test" {
configuration_store_id = azurerm_app_configuration.test.id
key = "/acctest/-ackey/-%d"
content_type = "test"
label = "acctest-ackeylabel-%d"
value = "a test"
}
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger)
}

func (t AppConfigurationKeyResource) basicNoLabel(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
Expand Down
83 changes: 78 additions & 5 deletions internal/services/appconfiguration/parse/app_configuration_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (k AppConfigurationKeyId) ID() string {
}

func KeyId(input string) (*AppConfigurationKeyId, error) {
resourceID, err := azure.ParseAzureResourceID(input)
resourceID, err := parseAzureResourceID(input)
if err != nil {
return nil, fmt.Errorf("while parsing resource ID: %+v", err)
}
Expand All @@ -31,14 +31,87 @@ func KeyId(input string) (*AppConfigurationKeyId, error) {
Label: label,
}

// Golang's URL parser will translate %00 to \000 (NUL). This will only happen if we're dealing with an empty
// label, so we set the label to the expected value (empty string) and trim the input string, so we can properly
// extract the configuration store ID out of it.
if label == "\000" {
// Label will have a "%00" placeholder if we're dealing with an empty label,
// so we set the label to the expected value (empty string) and trim the input
// string, so we can properly extract the configuration store ID out of it.
if label == "%00" {
appcfgID.Label = ""
input = strings.TrimSuffix(input, "%00")
}
appcfgID.ConfigurationStoreId = strings.TrimSuffix(input, fmt.Sprintf("/AppConfigurationKey/%s/Label/%s", appcfgID.Key, appcfgID.Label))

return &appcfgID, nil
}

// specific parser to prevent decoding of the ID
func parseAzureResourceID(id string) (*azure.ResourceID, error) {
id = strings.TrimPrefix(id, "/")
id = strings.TrimSuffix(id, "/")

components := strings.Split(id, "/")

// We should have an even number of key-value pairs.
if len(components)%2 != 0 {
return nil, fmt.Errorf("the number of path segments is not divisible by 2 in %q", id)
}

var subscriptionID string
var provider string

// Put the constituent key-value pairs into a map
componentMap := make(map[string]string, len(components)/2)
for current := 0; current < len(components); current += 2 {
key := components[current]
value := components[current+1]

// Check key/value for empty strings.
if key == "" || value == "" {
return nil, fmt.Errorf("Key/Value cannot be empty strings. Key: '%s', Value: '%s'", key, value)
}

switch {
case key == "subscriptions" && subscriptionID == "":
// Catch the subscriptionID before it can be overwritten by another "subscriptions"
// value in the ID which is the case for the Service Bus subscription resource
subscriptionID = value
case key == "providers" && provider == "":
// Catch the provider before it can be overwritten by another "providers"
// value in the ID which can be the case for the Role Assignment resource
provider = value
default:
componentMap[key] = value
}
}

// Build up a TargetResourceID from the map
idObj := &azure.ResourceID{}
idObj.Path = componentMap

if subscriptionID != "" {
idObj.SubscriptionID = subscriptionID
} else {
return nil, fmt.Errorf("no subscription ID found in: %q", id)
}

if resourceGroup, ok := componentMap["resourceGroups"]; ok {
idObj.ResourceGroup = resourceGroup
delete(componentMap, "resourceGroups")
} else if resourceGroup, ok := componentMap["resourcegroups"]; ok {
// Some Azure APIs are weird and provide things in lower case...
// However it's not clear whether the casing of other elements in the URI
// matter, so we explicitly look for that case here.
idObj.ResourceGroup = resourceGroup
delete(componentMap, "resourcegroups")
}

if provider != "" {
idObj.Provider = provider
}

if secondaryProvider := componentMap["providers"]; secondaryProvider != "" {
idObj.SecondaryProvider = secondaryProvider
delete(componentMap, "providers")
}

return idObj, nil
}