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 }