-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #221 from hashicorp/recaser
Add support for dynamic resource id recasing
- Loading branch information
Showing
4 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package commonids | ||
|
||
import ( | ||
"github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" | ||
) | ||
|
||
func CommonIds() []resourceids.ResourceId { | ||
|
||
return []resourceids.ResourceId{ | ||
&AppServiceId{}, | ||
&AppServiceEnvironmentId{}, | ||
&AppServicePlanId{}, | ||
&AutomationCompilationJobId{}, | ||
&AvailabilitySetId{}, | ||
&BotServiceId{}, | ||
&BotServiceChannelId{}, | ||
&ChaosStudioCapabilityId{}, | ||
&ChaosStudioTargetId{}, | ||
&CloudServicesIPConfigurationId{}, | ||
&CloudServicesPublicIPAddressId{}, | ||
&DedicatedHostId{}, | ||
&DedicatedHostGroupId{}, | ||
&DevCenterId{}, | ||
&DiskEncryptionSetId{}, | ||
&ExpressRouteCircuitPeeringId{}, | ||
&HDInsightClusterId{}, | ||
&HyperVSiteJobId{}, | ||
&HyperVSiteMachineId{}, | ||
&HyperVSiteRunAsAccountId{}, | ||
&KeyVaultId{}, | ||
&KeyVaultKeyId{}, | ||
&KeyVaultKeyVersionId{}, | ||
&KeyVaultPrivateEndpointConnectionId{}, | ||
&KubernetesClusterId{}, | ||
&KubernetesFleetId{}, | ||
&KustoClusterId{}, | ||
&KustoDatabaseId{}, | ||
&ManagedDiskId{}, | ||
&ManagementGroupId{}, | ||
&NetworkInterfaceId{}, | ||
&NetworkInterfaceIPConfigurationId{}, | ||
&NetworkInterfaceId{}, | ||
&ProvisioningServiceId{}, | ||
&PublicIPAddressId{}, | ||
&ResourceGroupId{}, | ||
&SharedImageGalleryId{}, | ||
&SpringCloudServiceId{}, | ||
&SqlDatabaseId{}, | ||
&SqlElasticPoolId{}, | ||
&SqlManagedInstanceId{}, | ||
&SqlManagedInstanceDatabaseId{}, | ||
&SqlServerId{}, | ||
&StorageAccountId{}, | ||
&StorageContainerId{}, | ||
&SubnetId{}, | ||
&SubscriptionId{}, | ||
&UserAssignedIdentityId{}, | ||
&VirtualHubBGPConnectionId{}, | ||
&VirtualHubIPConfigurationId{}, | ||
&VirtualMachineId{}, | ||
&VirtualMachineScaleSetNetworkInterfaceId{}, | ||
&VirtualMachineScaleSetPublicIPAddressId{}, | ||
&VirtualMachineScaleSetId{}, | ||
&VirtualNetworkId{}, | ||
&VirtualRouterPeeringId{}, | ||
&VirtualWANP2SVPNGatewayId{}, | ||
&VMwareSiteJobId{}, | ||
&VMwareSiteMachineId{}, | ||
&VMwareSiteRunAsAccountId{}, | ||
&VPNConnectionId{}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package recaser | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" | ||
) | ||
|
||
// ReCase tries to determine the type of Resource ID defined in `input` to be able to re-case it from | ||
func ReCase(input string) string { | ||
return reCaseWithIds(input, knownResourceIds) | ||
} | ||
|
||
// reCaseWithIds tries to determine the type of Resource ID defined in `input` to be able to re-case it based on an input list of Resource IDs | ||
func reCaseWithIds(input string, ids map[string]resourceids.ResourceId) string { | ||
output := input | ||
recased := false | ||
|
||
key, ok := buildInputKey(input) | ||
if ok { | ||
id := ids[*key] | ||
if id != nil { | ||
output, recased = parseId(id, input) | ||
} | ||
} | ||
|
||
// if we can't find a matching id recase these known segments | ||
if !recased { | ||
|
||
segmentsToFix := []string{ | ||
"/subscriptions/", | ||
"/resourceGroups/", | ||
"/managementGroups/", | ||
"/tenants/", | ||
} | ||
|
||
for _, segment := range segmentsToFix { | ||
output = fixSegment(output, segment) | ||
} | ||
} | ||
|
||
return output | ||
} | ||
|
||
// parseId uses the specified ResourceId to parse the input and returns the id string with correct casing | ||
func parseId(id resourceids.ResourceId, input string) (string, bool) { | ||
|
||
// we need to take a local copy of id to work against else we're mutating the original | ||
localId := id | ||
|
||
parser := resourceids.NewParserFromResourceIdType(localId) | ||
parsed, err := parser.Parse(input, true) | ||
if err != nil { | ||
return input, false | ||
} | ||
|
||
if err = id.FromParseResult(*parsed); err != nil { | ||
return input, false | ||
} | ||
input = id.ID() | ||
|
||
return input, true | ||
} | ||
|
||
// fixSegment searches the input id string for a specified segment case-insensitively | ||
// and returns the input string with the casing corrected on the segment | ||
func fixSegment(input, segment string) string { | ||
if strings.Contains(strings.ToLower(input), strings.ToLower(segment)) { | ||
re := regexp.MustCompile(fmt.Sprintf("(?i)%s", segment)) | ||
input = re.ReplaceAllString(input, segment) | ||
} | ||
return input | ||
} | ||
|
||
// buildInputKey takes an input id string and removes user-specified values from it | ||
// so it can be used as a key to extract the correct id from knownResourceIds | ||
func buildInputKey(input string) (*string, bool) { | ||
|
||
// don't attempt to build a key if this isn't a standard resource id | ||
if !strings.HasPrefix(input, "/") { | ||
return nil, false | ||
} | ||
|
||
output := "" | ||
|
||
segments := strings.Split(input, "/") | ||
// iterate through the segments extracting any that are not user inputs | ||
// and append them together to make a key | ||
// eg "/subscriptions/1111/resourceGroups/group1/providers/Microsoft.BotService/botServices/botServiceValue" will become: | ||
// "/subscriptions//resourceGroups//providers/Microsoft.BotService/botServices/" | ||
if len(segments)%2 != 0 { | ||
for i := 1; len(segments) > i; i++ { | ||
if i%2 != 0 { | ||
key := segments[i] | ||
output = fmt.Sprintf("%s/%s/", output, key) | ||
|
||
// if the current segment is a providers segment, then we should append the next segment to the key | ||
// as this is not a user input segment | ||
if strings.EqualFold(key, "providers") && len(segments) >= i+2 { | ||
value := segments[i+1] | ||
output = fmt.Sprintf("%s%s", output, value) | ||
} | ||
} | ||
} | ||
} | ||
output = strings.ToLower(output) | ||
return &output, true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
package recaser | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
|
||
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" | ||
"github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" | ||
) | ||
|
||
func TestReCaserWithIncorrectCasing(t *testing.T) { | ||
expected := "/subscriptions/11111/resourceGroups/bobby/providers/Microsoft.Compute/availabilitySets/HeYO" | ||
|
||
actual := reCaseWithIds("/Subscriptions/11111/resourcegroups/bobby/Providers/Microsoft.Compute/AvailabilitySets/HeYO", getTestIds()) | ||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithCorrectCasing(t *testing.T) { | ||
expected := "/subscriptions/11111/resourceGroups/bobby/providers/Microsoft.Compute/availabilitySets/HeYO" | ||
actual := reCaseWithIds("/subscriptions/11111/resourceGroups/bobby/providers/Microsoft.Compute/availabilitySets/HeYO", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
|
||
} | ||
|
||
func TestReCaserWithCorrectCasingResourceGroupId(t *testing.T) { | ||
expected := "/subscriptions/11111/resourceGroups/bobby" | ||
actual := reCaseWithIds("/subscriptions/11111/resourceGroups/bobby", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithIncorrectCasingResourceGroupId(t *testing.T) { | ||
expected := "/subscriptions/11111/resourceGroups/bobby" | ||
actual := reCaseWithIds("/Subscriptions/11111/resourcegroups/bobby", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithUnknownId(t *testing.T) { | ||
// should return string without recasing | ||
expected := "/blah/11111/Blah" | ||
actual := reCaseWithIds("/blah/11111/Blah", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithUnkownIdContainingSubscriptions(t *testing.T) { | ||
|
||
expected := "/subscriptions/11111/Blah" | ||
actual := reCaseWithIds("/suBsCrIpTiOnS/11111/Blah", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithUnkownIdContainingSubscriptionsAndResourceGroups(t *testing.T) { | ||
expected := "/subscriptions/11111/resourceGroups/group1/blah/" | ||
actual := reCaseWithIds("/suBscriptions/11111/ReSoUrCeGRoUps/group1/blah/", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithEmptyString(t *testing.T) { | ||
expected := "" | ||
actual := reCaseWithIds("", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithMultipleProviderSegmentsAndCorrectCasing(t *testing.T) { | ||
expected := "/subscriptions/11111/resourceGroups/bobby/providers/Microsoft.Compute/availabilitySets/HeYO/providers/Microsoft.Compute" | ||
actual := reCaseWithIds("/subscriptions/11111/resourceGroups/bobby/providers/Microsoft.Compute/availabilitySets/HeYO/providers/Microsoft.Compute", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithMultipleProviderSegmentsAndIncorrectCasing(t *testing.T) { | ||
expected := "/subscriptions/11111/resourceGroups/bobby/providers/Microsoft.Compute/availabilitySets/HeYO/providers/Microsoft.Compute" | ||
actual := reCaseWithIds("/Subscriptions/11111/resourcegroups/bobby/providers/Microsoft.Compute/availabilitySets/HeYO/providers/Microsoft.Compute", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithIncompleteProviderSegments(t *testing.T) { | ||
expected := "/subscriptions/11111/resourceGroups/bobby/providers/" | ||
actual := reCaseWithIds("/Subscriptions/11111/resourcegroups/bobby/providers/", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithOddNumberOfSegmentsAndCorrectCasing(t *testing.T) { | ||
expected := "/subscriptions/11111/resourceGroups/bobby/providers/Microsoft.Compute/availabilitySets/" | ||
actual := reCaseWithIds("/subscriptions/11111/resourceGroups/bobby/providers/Microsoft.Compute/availabilitySets/", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithOddNumberOfSegmentsAndIncorrectCasing(t *testing.T) { | ||
// expect /subscriptions/ and /resourceGroups/ to be recased but not /AvaiLabilitySets/ | ||
expected := "/subscriptions/11111/resourceGroups/bobby/providers/Microsoft.Compute/AvaiLabilitySets/" | ||
actual := reCaseWithIds("/SubsCriptions/11111/ResourceGroups/bobby/providers/Microsoft.Compute/AvaiLabilitySets/", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithURIAndCorrectCasing(t *testing.T) { | ||
expected := "https://management.azure.com:80/subscriptions/12345" | ||
actual := reCaseWithIds("https://management.azure.com:80/subscriptions/12345", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithURIAndIncorrectCasing(t *testing.T) { | ||
expected := "https://management.azure.com:80/subscriptions/12345" | ||
actual := reCaseWithIds("https://management.azure.com:80/SuBsCriPTions/12345", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func TestReCaserWithDataPlaneURI(t *testing.T) { | ||
expected := "https://example.blob.storage.azure.com/container1" | ||
actual := reCaseWithIds("https://example.blob.storage.azure.com/container1", getTestIds()) | ||
|
||
if actual != expected { | ||
t.Fatalf("Expected %q but got %q", expected, actual) | ||
} | ||
} | ||
|
||
func getTestIds() map[string]resourceids.ResourceId { | ||
return map[string]resourceids.ResourceId{ | ||
strings.ToLower(commonids.AppServiceId{}.ID()): &commonids.AppServiceId{}, | ||
strings.ToLower(commonids.AvailabilitySetId{}.ID()): &commonids.AvailabilitySetId{}, | ||
strings.ToLower(commonids.BotServiceId{}.ID()): &commonids.BotServiceId{}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package recaser | ||
|
||
import ( | ||
"strings" | ||
"sync" | ||
|
||
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" | ||
"github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" | ||
) | ||
|
||
var knownResourceIds = make(map[string]resourceids.ResourceId) | ||
|
||
var resourceIdsWriteLock = &sync.Mutex{} | ||
|
||
func init() { | ||
//register common ids | ||
for _, id := range commonids.CommonIds() { | ||
RegisterResourceId(id) | ||
} | ||
} | ||
|
||
// RegisterResourceId adds ResourceIds to a list of known ids | ||
func RegisterResourceId(id resourceids.ResourceId) { | ||
key := strings.ToLower(id.ID()) | ||
|
||
resourceIdsWriteLock.Lock() | ||
if _, ok := knownResourceIds[key]; !ok { | ||
knownResourceIds[key] = id | ||
} | ||
resourceIdsWriteLock.Unlock() | ||
} |