v2/internal/resolver/resource_hierarchy.go (228 lines of code) (raw):
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/
package resolver
import (
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/rotisserie/eris"
"github.com/Azure/azure-service-operator/v2/internal/genericarmclient"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/core"
)
type ResourceHierarchyRoot string
const (
ResourceHierarchyRootResourceGroup = ResourceHierarchyRoot("ResourceGroup")
ResourceHierarchyRootSubscription = ResourceHierarchyRoot("Subscription")
ResourceHierarchyRootTenant = ResourceHierarchyRoot("Tenant")
ResourceHierarchyRootARMID = ResourceHierarchyRoot("ARMID")
ResourceHierarchyRootOverride = ResourceHierarchyRoot("Override")
)
// If we wanted to type-assert we'd have to solve some circular dependency problems... for now this is ok.
const (
ResourceGroupKind = "ResourceGroup"
ResourceGroupGroup = "resources.azure.com"
)
type ResourceHierarchy []genruntime.ARMMetaObject
// ResourceGroup returns the resource group that the hierarchy is in, or an error if the hierarchy is not rooted
// in a resource group.
func (h ResourceHierarchy) ResourceGroup() (string, error) {
rootKind := h.rootKind(h)
if rootKind == ResourceHierarchyRootARMID {
armIDStr := h[0].Owner().ARMID
armID, err := arm.ParseResourceID(armIDStr)
if err != nil {
return "", err
}
if armID.ResourceGroupName == "" {
return "", eris.Errorf("not rooted by a resource group: %s", armIDStr)
}
return armID.ResourceGroupName, nil
}
if rootKind != ResourceHierarchyRootResourceGroup {
return "", eris.Errorf("not rooted by a resource group: %s", rootKind)
}
resourceGroup := h[0]
return resourceGroup.GetName(), nil
}
// Location returns the location root of the hierarchy, or an error
// if the root is not a subscription.
func (h ResourceHierarchy) Location() (string, error) {
rootKind := h.rootKind(h)
// We don't support ARM ID rooted for this method because it doesn't really make sense.
// If there's a need for it in the future we can add it
if rootKind != ResourceHierarchyRootSubscription {
return "", eris.Errorf("not rooted in a subscription: %s", rootKind)
}
// There's an assumption here that the root resource has a location
locatable, ok := h[0].(genruntime.LocatableResource)
if !ok {
return "", eris.Errorf("root does not implement LocatableResource: %T", h[0])
}
return locatable.Location(), nil
}
// AzureName returns the Azure name for use in creating a resource.
func (h ResourceHierarchy) AzureName() string {
azureNames := h.getAzureNames()
if len(azureNames) == 0 {
return ""
}
return azureNames[len(azureNames)-1]
}
// TODO: It's a bit awkward that this takes a subscriptionID parameter but does nothing with it in the tenant scope case
// FullyQualifiedARMID returns the fully qualified ARM ID of the resource
func (h ResourceHierarchy) FullyQualifiedARMID(subscriptionID string) (string, error) {
return h.fullyQualifiedARMIDImpl(subscriptionID, h)
}
func (h ResourceHierarchy) fullyQualifiedARMIDImpl(subscriptionID string, originalHierarchy ResourceHierarchy) (string, error) {
lastResource := h[len(h)-1]
lastResourceScope := lastResource.GetResourceScope()
// Under normal circumstances, this can't happen - but if there's a problem with the
// webhooks it is possible. We can't create a valid ARM ID without an AzureName, so we fail here.
if lastResource.AzureName() == "" {
return "", eris.Errorf("resource has empty AzureName, cannot create fully qualified ARM ID")
}
if lastResourceScope == genruntime.ResourceScopeExtension {
var parentARMID string
if lastResource.Owner().IsDirectARMReference() {
parentARMID = lastResource.Owner().ARMID
} else {
hierarchy := h[:len(h)-1]
var err error
parentARMID, err = hierarchy.fullyQualifiedARMIDImpl(subscriptionID, h)
if err != nil {
return "", err
}
}
provider, types, err := genruntime.GetResourceTypeAndProvider(lastResource)
if err != nil {
return "", err
}
if len(types) != 1 {
return "", eris.Errorf("extension resource cannot have more than one resource type, but had type: %s", lastResource.GetType())
}
return fmt.Sprintf("%s/providers/%s/%s/%s", parentARMID, provider, types[0], lastResource.AzureName()), nil
}
azureNames := h.getAzureNames()
rootKind := h.rootKind(originalHierarchy)
switch rootKind {
case ResourceHierarchyRootSubscription:
// TODO: This is currently a special case as the only resource like this is ResourceGroup and ResourceGroup itself
// TODO: is a bit funky because it doesn't have a /providers like everything else does...
return genericarmclient.MakeResourceGroupID(subscriptionID, azureNames[0]), nil
case ResourceHierarchyRootResourceGroup:
rgName := azureNames[0]
remainingNames := azureNames[1:]
// The only resource we actually care about for figuring out resource types is the
// most derived resource
res := h[len(h)-1]
provider, resourceTypes, err := genruntime.GetResourceTypeAndProvider(res)
if err != nil {
return "", err
}
root := h[0]
err = genruntime.VerifyResourceOwnerARMID(root)
if err != nil {
return "", err
}
// Safe to do it this way, Claimer makes sure the owner exists and is Ready and will always have an armId annotation before we reach here.
ownerARMID, err := genruntime.GetAndParseResourceID(root)
if err != nil {
return "", err
}
// Confirm that the subscription ID the user specified matches the subscription ID we're using from our credential
if ok := genruntime.CheckARMIDMatchesSubscription(subscriptionID, ownerARMID); !ok {
return "", core.NewSubscriptionMismatchError(ownerARMID.SubscriptionID, subscriptionID)
}
// Ensure that we have the same number of names and types
if len(remainingNames) != len(resourceTypes) {
return "", eris.Errorf(
"could not create fully qualified ARM ID, had %d azureNames and %d resourceTypes. azureNames: %+q resourceTypes: %+q",
len(remainingNames),
len(resourceTypes),
remainingNames,
resourceTypes)
}
// Join them together
interleaved := genruntime.InterleaveStrSlice(resourceTypes, remainingNames)
return genericarmclient.MakeResourceGroupScopeARMID(subscriptionID, rgName, provider, interleaved...)
case ResourceHierarchyRootTenant:
// The only resource we actually care about for figuring out resource types is the
// most derived resource
res := h[len(h)-1]
provider, resourceTypes, err := genruntime.GetResourceTypeAndProvider(res)
if err != nil {
return "", err
}
// Ensure that we have the same number of names and types
if len(azureNames) != len(resourceTypes) {
return "", eris.Errorf(
"could not create fully qualified ARM ID, had %d azureNames and %d resourceTypes. azureNames: %+q resourceTypes: %+q",
len(azureNames),
len(resourceTypes),
azureNames,
resourceTypes)
}
// Join them together
interleaved := genruntime.InterleaveStrSlice(resourceTypes, azureNames)
return genericarmclient.MakeTenantScopeARMID(provider, interleaved...)
case ResourceHierarchyRootARMID:
// TODO: Possibly refactor this huge method into sub-functions?
// The only resource we actually care about for figuring out resource types is the
// most derived resource
res := h[len(h)-1]
provider, resourceTypes, err := genruntime.GetResourceTypeAndProvider(res)
if err != nil {
return "", err
}
// We also need the ARMID from the root resource
root := h[0]
armIDStr := root.Owner().ARMID // Safe to do this without nil-guards because we already checked elsewhere
// Trim the trailing slash of the ARM ID if it's there (we'll add it back later)
armIDStr = strings.TrimRight(armIDStr, "/")
err = genruntime.VerifyResourceOwnerARMID(root)
if err != nil {
return "", err
}
// We used to have a check here ensuring that the owner ARM ID matched the referenced credential/secret
// ARM ID. That check was removed as with ARM ID-based owners the subscription the resource is being
// deployed to is pretty obvious (it's directly in the ID) so we felt the chances of mistakenly deploying to
// an incorrect subscription with kube-based ownership.
// Rooting to an ARM ID means that some of the resourceTypes may not actually be included explicitly in our
// hierarchy (because they're instead in the ARM ID itself). We filter these out of resourceTypes by
// removing types that aren't included in the hierarchy.
_, rootResourceTypes, err := genruntime.GetResourceTypeAndProvider(root)
if err != nil {
return "", err
}
resourceTypesIncludedInARMID := rootResourceTypes[:len(rootResourceTypes)-1]
resourceTypes = resourceTypes[len(resourceTypesIncludedInARMID):]
// Ensure that we have the same number of names and types
if len(azureNames) != len(resourceTypes) {
return "", eris.Errorf("could not create fully qualified ARM ID, had %d azureNames and %d resourceTypes. azureNames: %+q resourceTypes: %+q",
len(azureNames),
len(resourceTypes),
azureNames,
resourceTypes)
}
interleaved := genruntime.InterleaveStrSlice(resourceTypes, azureNames)
suffix := strings.Join(interleaved, "/")
// If the root ARM ID already contains the provider ID, we can just append the pairs.
// If it doesn't, we need to build a full ARM ID by appending the provider as well.
if strings.Contains(strings.ToLower(armIDStr), strings.ToLower(provider)) {
return fmt.Sprintf("%s/%s", armIDStr, suffix), nil
} else {
return fmt.Sprintf("%s/providers/%s/%s", armIDStr, provider, suffix), nil
}
case ResourceHierarchyRootOverride:
// Find the resource that has the override and start building the ID from there:
idFragment, idx := h.getChildResourceIDOverride()
if idx == -1 {
return "", eris.Errorf("resource had root kind %q, but had no child resource ID override", rootKind)
}
if idx != 0 {
return "", eris.Errorf("resource had root kind %q, but child resource override was not at index 0. Instead at index %d", rootKind, idx)
}
return idFragment, nil
// TODO: if we actually need to support this for hierarchies, we could do something like the below
// Get the type of the resource idx+1
// Get the type of the resource len(h)-1
// diff the two
// compute the remaining names and types
// append them together and tack them on to the idFragment
default:
return "", eris.Errorf("unknown root kind %q", rootKind)
}
}
// rootKind returns the ResourceHierarchyRoot type of the hierarchy.
// There are 6 cases here:
// 1. The hierarchy is comprised solely of a resource group. This is subscription rooted.
// 2. The hierarchy has multiple entries and roots up to a resource group. This is Resource Group rooted.
// 3. The hierarchy has multiple entries and doesn't root up to a resource group. This is subscription rooted.
// 4. The hierarchy roots up to a tenant scope resource. This is tenant rooted.
// 5. The hierarchy roots up to a resource whose Owner() is an ARMID. This is ARMID rooted.
// 6. The hierarchy contains a resource that sets genruntime.ChildResourceIDOverrideAnnotation. This is
// "Override" rooted.
func (h ResourceHierarchy) rootKind(originalHierarchy ResourceHierarchy) ResourceHierarchyRoot {
if len(h) == 0 {
panic("resource hierarchy cannot be len 0")
}
// This is a special kind of root for if genruntime.ChildResourceIDOverrideAnnotation is used
_, idx := originalHierarchy.getChildResourceIDOverride()
// Child ID override doesn't apply if it's set on the most derived resource
if idx != -1 && idx != len(originalHierarchy)-1 {
return ResourceHierarchyRootOverride
}
root := h[0]
// Check if the root resource is owned by an ARM ID
rootOwner := root.Owner()
if rootOwner != nil && rootOwner.IsDirectARMReference() {
return ResourceHierarchyRootARMID
}
scope := root.GetResourceScope()
if scope == genruntime.ResourceScopeTenant {
return ResourceHierarchyRootTenant
}
if scope == genruntime.ResourceScopeLocation {
if len(h) == 1 { // Just the location scope resource
return ResourceHierarchyRootSubscription
}
return ResourceHierarchyRootResourceGroup
}
return ResourceHierarchyRootSubscription
}
func (h ResourceHierarchy) getAzureNames() []string {
azureNames := make([]string, 0, len(h))
for _, res := range h {
azureNames = append(azureNames, res.AzureName())
}
return azureNames
}
// getChildResourceIDOverride returns the child resource ID override and the index at which the override was specified, or
// -1 if there was no childResourceIDOverride
func (h ResourceHierarchy) getChildResourceIDOverride() (string, int) {
for i, res := range h {
idFragment, ok := genruntime.GetChildResourceIDOverride(res)
if ok {
return idFragment, i
}
}
return "", -1
}