Skip to content

Commit

Permalink
Merge pull request #221 from hashicorp/recaser
Browse files Browse the repository at this point in the history
Add support for dynamic resource id recasing
  • Loading branch information
tombuildsstuff authored Mar 27, 2024
2 parents 09aa089 + 26de03f commit 9ac8d61
Show file tree
Hide file tree
Showing 4 changed files with 378 additions and 0 deletions.
72 changes: 72 additions & 0 deletions resourcemanager/commonids/ids.go
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{},
}
}
110 changes: 110 additions & 0 deletions resourcemanager/recaser/recase.go
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
}
165 changes: 165 additions & 0 deletions resourcemanager/recaser/recaser_test.go
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{},
}
}
31 changes: 31 additions & 0 deletions resourcemanager/recaser/registration.go
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()
}

0 comments on commit 9ac8d61

Please sign in to comment.