Skip to content

Commit

Permalink
Some minor refactoring of azcore (#20291)
Browse files Browse the repository at this point in the history
Moved ARM resource ID types and helpers to an internal package so they
can be leveraged elsewhere.  The public versions are type-aliased to the
matching internal implementations.
policyFunc has moved to the shared package so it can be used elsewhere.
Sanitizing of URLs has been split into its own function.
Moved a func-local constant to the shared package.
  • Loading branch information
jhendrixMSFT authored Feb 27, 2023
1 parent dae8ab4 commit 2877174
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 322 deletions.
224 changes: 224 additions & 0 deletions sdk/azcore/arm/internal/resource/resource_identifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
//go:build go1.18
// +build go1.18

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package resource

import (
"fmt"
"strings"
)

const (
providersKey = "providers"
subscriptionsKey = "subscriptions"
resourceGroupsLowerKey = "resourcegroups"
locationsKey = "locations"
builtInResourceNamespace = "Microsoft.Resources"
)

// RootResourceID defines the tenant as the root parent of all other ResourceID.
var RootResourceID = &ResourceID{
Parent: nil,
ResourceType: TenantResourceType,
Name: "",
}

// ResourceID represents a resource ID such as `/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg`.
// Don't create this type directly, use ParseResourceID instead.
type ResourceID struct {
// Parent is the parent ResourceID of this instance.
// Can be nil if there is no parent.
Parent *ResourceID

// SubscriptionID is the subscription ID in this resource ID.
// The value can be empty if the resource ID does not contain a subscription ID.
SubscriptionID string

// ResourceGroupName is the resource group name in this resource ID.
// The value can be empty if the resource ID does not contain a resource group name.
ResourceGroupName string

// Provider represents the provider name in this resource ID.
// This is only valid when the resource ID represents a resource provider.
// Example: `/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Insights`
Provider string

// Location is the location in this resource ID.
// The value can be empty if the resource ID does not contain a location name.
Location string

// ResourceType represents the type of this resource ID.
ResourceType ResourceType

// Name is the resource name of this resource ID.
Name string

isChild bool
stringValue string
}

// ParseResourceID parses a string to an instance of ResourceID
func ParseResourceID(id string) (*ResourceID, error) {
if len(id) == 0 {
return nil, fmt.Errorf("invalid resource ID: id cannot be empty")
}

if !strings.HasPrefix(id, "/") {
return nil, fmt.Errorf("invalid resource ID: resource id '%s' must start with '/'", id)
}

parts := splitStringAndOmitEmpty(id, "/")

if len(parts) < 2 {
return nil, fmt.Errorf("invalid resource ID: %s", id)
}

if !strings.EqualFold(parts[0], subscriptionsKey) && !strings.EqualFold(parts[0], providersKey) {
return nil, fmt.Errorf("invalid resource ID: %s", id)
}

return appendNext(RootResourceID, parts, id)
}

// String returns the string of the ResourceID
func (id *ResourceID) String() string {
if len(id.stringValue) > 0 {
return id.stringValue
}

if id.Parent == nil {
return ""
}

builder := strings.Builder{}
builder.WriteString(id.Parent.String())

if id.isChild {
builder.WriteString(fmt.Sprintf("/%s", id.ResourceType.lastType()))
if len(id.Name) > 0 {
builder.WriteString(fmt.Sprintf("/%s", id.Name))
}
} else {
builder.WriteString(fmt.Sprintf("/providers/%s/%s/%s", id.ResourceType.Namespace, id.ResourceType.Type, id.Name))
}

id.stringValue = builder.String()

return id.stringValue
}

func newResourceID(parent *ResourceID, resourceTypeName string, resourceName string) *ResourceID {
id := &ResourceID{}
id.init(parent, chooseResourceType(resourceTypeName, parent), resourceName, true)
return id
}

func newResourceIDWithResourceType(parent *ResourceID, resourceType ResourceType, resourceName string) *ResourceID {
id := &ResourceID{}
id.init(parent, resourceType, resourceName, true)
return id
}

func newResourceIDWithProvider(parent *ResourceID, providerNamespace, resourceTypeName, resourceName string) *ResourceID {
id := &ResourceID{}
id.init(parent, NewResourceType(providerNamespace, resourceTypeName), resourceName, false)
return id
}

func chooseResourceType(resourceTypeName string, parent *ResourceID) ResourceType {
if strings.EqualFold(resourceTypeName, resourceGroupsLowerKey) {
return ResourceGroupResourceType
} else if strings.EqualFold(resourceTypeName, subscriptionsKey) && parent != nil && parent.ResourceType.String() == TenantResourceType.String() {
return SubscriptionResourceType
}

return parent.ResourceType.AppendChild(resourceTypeName)
}

func (id *ResourceID) init(parent *ResourceID, resourceType ResourceType, name string, isChild bool) {
if parent != nil {
id.Provider = parent.Provider
id.SubscriptionID = parent.SubscriptionID
id.ResourceGroupName = parent.ResourceGroupName
id.Location = parent.Location
}

if resourceType.String() == SubscriptionResourceType.String() {
id.SubscriptionID = name
}

if resourceType.lastType() == locationsKey {
id.Location = name
}

if resourceType.String() == ResourceGroupResourceType.String() {
id.ResourceGroupName = name
}

if resourceType.String() == ProviderResourceType.String() {
id.Provider = name
}

if parent == nil {
id.Parent = RootResourceID
} else {
id.Parent = parent
}
id.isChild = isChild
id.ResourceType = resourceType
id.Name = name
}

func appendNext(parent *ResourceID, parts []string, id string) (*ResourceID, error) {
if len(parts) == 0 {
return parent, nil
}

if len(parts) == 1 {
// subscriptions and resourceGroups are not valid ids without their names
if strings.EqualFold(parts[0], subscriptionsKey) || strings.EqualFold(parts[0], resourceGroupsLowerKey) {
return nil, fmt.Errorf("invalid resource ID: %s", id)
}

// resourceGroup must contain either child or provider resource type
if parent.ResourceType.String() == ResourceGroupResourceType.String() {
return nil, fmt.Errorf("invalid resource ID: %s", id)
}

return newResourceID(parent, parts[0], ""), nil
}

if strings.EqualFold(parts[0], providersKey) && (len(parts) == 2 || strings.EqualFold(parts[2], providersKey)) {
//provider resource can only be on a tenant or a subscription parent
if parent.ResourceType.String() != SubscriptionResourceType.String() && parent.ResourceType.String() != TenantResourceType.String() {
return nil, fmt.Errorf("invalid resource ID: %s", id)
}

return appendNext(newResourceIDWithResourceType(parent, ProviderResourceType, parts[1]), parts[2:], id)
}

if len(parts) > 3 && strings.EqualFold(parts[0], providersKey) {
return appendNext(newResourceIDWithProvider(parent, parts[1], parts[2], parts[3]), parts[4:], id)
}

if len(parts) > 1 && !strings.EqualFold(parts[0], providersKey) {
return appendNext(newResourceID(parent, parts[0], parts[1]), parts[2:], id)
}

return nil, fmt.Errorf("invalid resource ID: %s", id)
}

func splitStringAndOmitEmpty(v, sep string) []string {
r := make([]string, 0)
for _, s := range strings.Split(v, sep) {
if len(s) == 0 {
continue
}
r = append(r, s)
}

return r
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package arm
package resource

import "testing"

Expand Down
114 changes: 114 additions & 0 deletions sdk/azcore/arm/internal/resource/resource_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//go:build go1.18
// +build go1.18

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package resource

import (
"fmt"
"strings"
)

// SubscriptionResourceType is the ResourceType of a subscription
var SubscriptionResourceType = NewResourceType(builtInResourceNamespace, "subscriptions")

// ResourceGroupResourceType is the ResourceType of a resource group
var ResourceGroupResourceType = NewResourceType(builtInResourceNamespace, "resourceGroups")

// TenantResourceType is the ResourceType of a tenant
var TenantResourceType = NewResourceType(builtInResourceNamespace, "tenants")

// ProviderResourceType is the ResourceType of a provider
var ProviderResourceType = NewResourceType(builtInResourceNamespace, "providers")

// ResourceType represents an Azure resource type, e.g. "Microsoft.Network/virtualNetworks/subnets".
// Don't create this type directly, use ParseResourceType or NewResourceType instead.
type ResourceType struct {
// Namespace is the namespace of the resource type.
// e.g. "Microsoft.Network" in resource type "Microsoft.Network/virtualNetworks/subnets"
Namespace string

// Type is the full type name of the resource type.
// e.g. "virtualNetworks/subnets" in resource type "Microsoft.Network/virtualNetworks/subnets"
Type string

// Types is the slice of all the sub-types of this resource type.
// e.g. ["virtualNetworks", "subnets"] in resource type "Microsoft.Network/virtualNetworks/subnets"
Types []string

stringValue string
}

// String returns the string of the ResourceType
func (t ResourceType) String() string {
return t.stringValue
}

// IsParentOf returns true when the receiver is the parent resource type of the child.
func (t ResourceType) IsParentOf(child ResourceType) bool {
if !strings.EqualFold(t.Namespace, child.Namespace) {
return false
}
if len(t.Types) >= len(child.Types) {
return false
}
for i := range t.Types {
if !strings.EqualFold(t.Types[i], child.Types[i]) {
return false
}
}

return true
}

// AppendChild creates an instance of ResourceType using the receiver as the parent with childType appended to it.
func (t ResourceType) AppendChild(childType string) ResourceType {
return NewResourceType(t.Namespace, fmt.Sprintf("%s/%s", t.Type, childType))
}

// NewResourceType creates an instance of ResourceType using a provider namespace
// such as "Microsoft.Network" and type such as "virtualNetworks/subnets".
func NewResourceType(providerNamespace, typeName string) ResourceType {
return ResourceType{
Namespace: providerNamespace,
Type: typeName,
Types: splitStringAndOmitEmpty(typeName, "/"),
stringValue: fmt.Sprintf("%s/%s", providerNamespace, typeName),
}
}

// ParseResourceType parses the ResourceType from a resource type string (e.g. Microsoft.Network/virtualNetworks/subsets)
// or a resource identifier string.
// e.g. /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/mySubnet)
func ParseResourceType(resourceIDOrType string) (ResourceType, error) {
// split the path into segments
parts := splitStringAndOmitEmpty(resourceIDOrType, "/")

// There must be at least a namespace and type name
if len(parts) < 1 {
return ResourceType{}, fmt.Errorf("invalid resource ID or type: %s", resourceIDOrType)
}

// if the type is just subscriptions, it is a built-in type in the Microsoft.Resources namespace
if len(parts) == 1 {
// Simple resource type
return NewResourceType(builtInResourceNamespace, parts[0]), nil
} else if strings.Contains(parts[0], ".") {
// Handle resource types (Microsoft.Compute/virtualMachines, Microsoft.Network/virtualNetworks/subnets)
// it is a full type name
return NewResourceType(parts[0], strings.Join(parts[1:], "/")), nil
} else {
// Check if ResourceID
id, err := ParseResourceID(resourceIDOrType)
if err != nil {
return ResourceType{}, err
}
return NewResourceType(id.ResourceType.Namespace, id.ResourceType.Type), nil
}
}

func (t ResourceType) lastType() string {
return t.Types[len(t.Types)-1]
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package arm
package resource

import (
"testing"
Expand Down
Loading

0 comments on commit 2877174

Please sign in to comment.