Skip to content

Commit 2877174

Browse files
authored
Some minor refactoring of azcore (Azure#20291)
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.
1 parent dae8ab4 commit 2877174

File tree

12 files changed

+398
-322
lines changed

12 files changed

+398
-322
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
//go:build go1.18
2+
// +build go1.18
3+
4+
// Copyright (c) Microsoft Corporation. All rights reserved.
5+
// Licensed under the MIT License.
6+
7+
package resource
8+
9+
import (
10+
"fmt"
11+
"strings"
12+
)
13+
14+
const (
15+
providersKey = "providers"
16+
subscriptionsKey = "subscriptions"
17+
resourceGroupsLowerKey = "resourcegroups"
18+
locationsKey = "locations"
19+
builtInResourceNamespace = "Microsoft.Resources"
20+
)
21+
22+
// RootResourceID defines the tenant as the root parent of all other ResourceID.
23+
var RootResourceID = &ResourceID{
24+
Parent: nil,
25+
ResourceType: TenantResourceType,
26+
Name: "",
27+
}
28+
29+
// ResourceID represents a resource ID such as `/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg`.
30+
// Don't create this type directly, use ParseResourceID instead.
31+
type ResourceID struct {
32+
// Parent is the parent ResourceID of this instance.
33+
// Can be nil if there is no parent.
34+
Parent *ResourceID
35+
36+
// SubscriptionID is the subscription ID in this resource ID.
37+
// The value can be empty if the resource ID does not contain a subscription ID.
38+
SubscriptionID string
39+
40+
// ResourceGroupName is the resource group name in this resource ID.
41+
// The value can be empty if the resource ID does not contain a resource group name.
42+
ResourceGroupName string
43+
44+
// Provider represents the provider name in this resource ID.
45+
// This is only valid when the resource ID represents a resource provider.
46+
// Example: `/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Insights`
47+
Provider string
48+
49+
// Location is the location in this resource ID.
50+
// The value can be empty if the resource ID does not contain a location name.
51+
Location string
52+
53+
// ResourceType represents the type of this resource ID.
54+
ResourceType ResourceType
55+
56+
// Name is the resource name of this resource ID.
57+
Name string
58+
59+
isChild bool
60+
stringValue string
61+
}
62+
63+
// ParseResourceID parses a string to an instance of ResourceID
64+
func ParseResourceID(id string) (*ResourceID, error) {
65+
if len(id) == 0 {
66+
return nil, fmt.Errorf("invalid resource ID: id cannot be empty")
67+
}
68+
69+
if !strings.HasPrefix(id, "/") {
70+
return nil, fmt.Errorf("invalid resource ID: resource id '%s' must start with '/'", id)
71+
}
72+
73+
parts := splitStringAndOmitEmpty(id, "/")
74+
75+
if len(parts) < 2 {
76+
return nil, fmt.Errorf("invalid resource ID: %s", id)
77+
}
78+
79+
if !strings.EqualFold(parts[0], subscriptionsKey) && !strings.EqualFold(parts[0], providersKey) {
80+
return nil, fmt.Errorf("invalid resource ID: %s", id)
81+
}
82+
83+
return appendNext(RootResourceID, parts, id)
84+
}
85+
86+
// String returns the string of the ResourceID
87+
func (id *ResourceID) String() string {
88+
if len(id.stringValue) > 0 {
89+
return id.stringValue
90+
}
91+
92+
if id.Parent == nil {
93+
return ""
94+
}
95+
96+
builder := strings.Builder{}
97+
builder.WriteString(id.Parent.String())
98+
99+
if id.isChild {
100+
builder.WriteString(fmt.Sprintf("/%s", id.ResourceType.lastType()))
101+
if len(id.Name) > 0 {
102+
builder.WriteString(fmt.Sprintf("/%s", id.Name))
103+
}
104+
} else {
105+
builder.WriteString(fmt.Sprintf("/providers/%s/%s/%s", id.ResourceType.Namespace, id.ResourceType.Type, id.Name))
106+
}
107+
108+
id.stringValue = builder.String()
109+
110+
return id.stringValue
111+
}
112+
113+
func newResourceID(parent *ResourceID, resourceTypeName string, resourceName string) *ResourceID {
114+
id := &ResourceID{}
115+
id.init(parent, chooseResourceType(resourceTypeName, parent), resourceName, true)
116+
return id
117+
}
118+
119+
func newResourceIDWithResourceType(parent *ResourceID, resourceType ResourceType, resourceName string) *ResourceID {
120+
id := &ResourceID{}
121+
id.init(parent, resourceType, resourceName, true)
122+
return id
123+
}
124+
125+
func newResourceIDWithProvider(parent *ResourceID, providerNamespace, resourceTypeName, resourceName string) *ResourceID {
126+
id := &ResourceID{}
127+
id.init(parent, NewResourceType(providerNamespace, resourceTypeName), resourceName, false)
128+
return id
129+
}
130+
131+
func chooseResourceType(resourceTypeName string, parent *ResourceID) ResourceType {
132+
if strings.EqualFold(resourceTypeName, resourceGroupsLowerKey) {
133+
return ResourceGroupResourceType
134+
} else if strings.EqualFold(resourceTypeName, subscriptionsKey) && parent != nil && parent.ResourceType.String() == TenantResourceType.String() {
135+
return SubscriptionResourceType
136+
}
137+
138+
return parent.ResourceType.AppendChild(resourceTypeName)
139+
}
140+
141+
func (id *ResourceID) init(parent *ResourceID, resourceType ResourceType, name string, isChild bool) {
142+
if parent != nil {
143+
id.Provider = parent.Provider
144+
id.SubscriptionID = parent.SubscriptionID
145+
id.ResourceGroupName = parent.ResourceGroupName
146+
id.Location = parent.Location
147+
}
148+
149+
if resourceType.String() == SubscriptionResourceType.String() {
150+
id.SubscriptionID = name
151+
}
152+
153+
if resourceType.lastType() == locationsKey {
154+
id.Location = name
155+
}
156+
157+
if resourceType.String() == ResourceGroupResourceType.String() {
158+
id.ResourceGroupName = name
159+
}
160+
161+
if resourceType.String() == ProviderResourceType.String() {
162+
id.Provider = name
163+
}
164+
165+
if parent == nil {
166+
id.Parent = RootResourceID
167+
} else {
168+
id.Parent = parent
169+
}
170+
id.isChild = isChild
171+
id.ResourceType = resourceType
172+
id.Name = name
173+
}
174+
175+
func appendNext(parent *ResourceID, parts []string, id string) (*ResourceID, error) {
176+
if len(parts) == 0 {
177+
return parent, nil
178+
}
179+
180+
if len(parts) == 1 {
181+
// subscriptions and resourceGroups are not valid ids without their names
182+
if strings.EqualFold(parts[0], subscriptionsKey) || strings.EqualFold(parts[0], resourceGroupsLowerKey) {
183+
return nil, fmt.Errorf("invalid resource ID: %s", id)
184+
}
185+
186+
// resourceGroup must contain either child or provider resource type
187+
if parent.ResourceType.String() == ResourceGroupResourceType.String() {
188+
return nil, fmt.Errorf("invalid resource ID: %s", id)
189+
}
190+
191+
return newResourceID(parent, parts[0], ""), nil
192+
}
193+
194+
if strings.EqualFold(parts[0], providersKey) && (len(parts) == 2 || strings.EqualFold(parts[2], providersKey)) {
195+
//provider resource can only be on a tenant or a subscription parent
196+
if parent.ResourceType.String() != SubscriptionResourceType.String() && parent.ResourceType.String() != TenantResourceType.String() {
197+
return nil, fmt.Errorf("invalid resource ID: %s", id)
198+
}
199+
200+
return appendNext(newResourceIDWithResourceType(parent, ProviderResourceType, parts[1]), parts[2:], id)
201+
}
202+
203+
if len(parts) > 3 && strings.EqualFold(parts[0], providersKey) {
204+
return appendNext(newResourceIDWithProvider(parent, parts[1], parts[2], parts[3]), parts[4:], id)
205+
}
206+
207+
if len(parts) > 1 && !strings.EqualFold(parts[0], providersKey) {
208+
return appendNext(newResourceID(parent, parts[0], parts[1]), parts[2:], id)
209+
}
210+
211+
return nil, fmt.Errorf("invalid resource ID: %s", id)
212+
}
213+
214+
func splitStringAndOmitEmpty(v, sep string) []string {
215+
r := make([]string, 0)
216+
for _, s := range strings.Split(v, sep) {
217+
if len(s) == 0 {
218+
continue
219+
}
220+
r = append(r, s)
221+
}
222+
223+
return r
224+
}

sdk/azcore/arm/resource_identifier_test.go renamed to sdk/azcore/arm/internal/resource/resource_identifier_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// Copyright (c) Microsoft Corporation. All rights reserved.
55
// Licensed under the MIT License.
66

7-
package arm
7+
package resource
88

99
import "testing"
1010

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//go:build go1.18
2+
// +build go1.18
3+
4+
// Copyright (c) Microsoft Corporation. All rights reserved.
5+
// Licensed under the MIT License.
6+
7+
package resource
8+
9+
import (
10+
"fmt"
11+
"strings"
12+
)
13+
14+
// SubscriptionResourceType is the ResourceType of a subscription
15+
var SubscriptionResourceType = NewResourceType(builtInResourceNamespace, "subscriptions")
16+
17+
// ResourceGroupResourceType is the ResourceType of a resource group
18+
var ResourceGroupResourceType = NewResourceType(builtInResourceNamespace, "resourceGroups")
19+
20+
// TenantResourceType is the ResourceType of a tenant
21+
var TenantResourceType = NewResourceType(builtInResourceNamespace, "tenants")
22+
23+
// ProviderResourceType is the ResourceType of a provider
24+
var ProviderResourceType = NewResourceType(builtInResourceNamespace, "providers")
25+
26+
// ResourceType represents an Azure resource type, e.g. "Microsoft.Network/virtualNetworks/subnets".
27+
// Don't create this type directly, use ParseResourceType or NewResourceType instead.
28+
type ResourceType struct {
29+
// Namespace is the namespace of the resource type.
30+
// e.g. "Microsoft.Network" in resource type "Microsoft.Network/virtualNetworks/subnets"
31+
Namespace string
32+
33+
// Type is the full type name of the resource type.
34+
// e.g. "virtualNetworks/subnets" in resource type "Microsoft.Network/virtualNetworks/subnets"
35+
Type string
36+
37+
// Types is the slice of all the sub-types of this resource type.
38+
// e.g. ["virtualNetworks", "subnets"] in resource type "Microsoft.Network/virtualNetworks/subnets"
39+
Types []string
40+
41+
stringValue string
42+
}
43+
44+
// String returns the string of the ResourceType
45+
func (t ResourceType) String() string {
46+
return t.stringValue
47+
}
48+
49+
// IsParentOf returns true when the receiver is the parent resource type of the child.
50+
func (t ResourceType) IsParentOf(child ResourceType) bool {
51+
if !strings.EqualFold(t.Namespace, child.Namespace) {
52+
return false
53+
}
54+
if len(t.Types) >= len(child.Types) {
55+
return false
56+
}
57+
for i := range t.Types {
58+
if !strings.EqualFold(t.Types[i], child.Types[i]) {
59+
return false
60+
}
61+
}
62+
63+
return true
64+
}
65+
66+
// AppendChild creates an instance of ResourceType using the receiver as the parent with childType appended to it.
67+
func (t ResourceType) AppendChild(childType string) ResourceType {
68+
return NewResourceType(t.Namespace, fmt.Sprintf("%s/%s", t.Type, childType))
69+
}
70+
71+
// NewResourceType creates an instance of ResourceType using a provider namespace
72+
// such as "Microsoft.Network" and type such as "virtualNetworks/subnets".
73+
func NewResourceType(providerNamespace, typeName string) ResourceType {
74+
return ResourceType{
75+
Namespace: providerNamespace,
76+
Type: typeName,
77+
Types: splitStringAndOmitEmpty(typeName, "/"),
78+
stringValue: fmt.Sprintf("%s/%s", providerNamespace, typeName),
79+
}
80+
}
81+
82+
// ParseResourceType parses the ResourceType from a resource type string (e.g. Microsoft.Network/virtualNetworks/subsets)
83+
// or a resource identifier string.
84+
// e.g. /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/mySubnet)
85+
func ParseResourceType(resourceIDOrType string) (ResourceType, error) {
86+
// split the path into segments
87+
parts := splitStringAndOmitEmpty(resourceIDOrType, "/")
88+
89+
// There must be at least a namespace and type name
90+
if len(parts) < 1 {
91+
return ResourceType{}, fmt.Errorf("invalid resource ID or type: %s", resourceIDOrType)
92+
}
93+
94+
// if the type is just subscriptions, it is a built-in type in the Microsoft.Resources namespace
95+
if len(parts) == 1 {
96+
// Simple resource type
97+
return NewResourceType(builtInResourceNamespace, parts[0]), nil
98+
} else if strings.Contains(parts[0], ".") {
99+
// Handle resource types (Microsoft.Compute/virtualMachines, Microsoft.Network/virtualNetworks/subnets)
100+
// it is a full type name
101+
return NewResourceType(parts[0], strings.Join(parts[1:], "/")), nil
102+
} else {
103+
// Check if ResourceID
104+
id, err := ParseResourceID(resourceIDOrType)
105+
if err != nil {
106+
return ResourceType{}, err
107+
}
108+
return NewResourceType(id.ResourceType.Namespace, id.ResourceType.Type), nil
109+
}
110+
}
111+
112+
func (t ResourceType) lastType() string {
113+
return t.Types[len(t.Types)-1]
114+
}

sdk/azcore/arm/resource_type_test.go renamed to sdk/azcore/arm/internal/resource/resource_type_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// Copyright (c) Microsoft Corporation. All rights reserved.
55
// Licensed under the MIT License.
66

7-
package arm
7+
package resource
88

99
import (
1010
"testing"

0 commit comments

Comments
 (0)