v2/internal/resolver/resolver.go (307 lines of code) (raw):
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/
package resolver
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/rotisserie/eris"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"github.com/Azure/azure-service-operator/v2/internal/reflecthelpers"
"github.com/Azure/azure-service-operator/v2/internal/set"
"github.com/Azure/azure-service-operator/v2/internal/util/kubeclient"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/core"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/registration"
)
type Resolver struct {
client kubeclient.Client
kubeSecretResolver SecretResolver
kubeSecretMapResolver SecretMapResolver
kubeConfigMapResolver ConfigMapResolver
reconciledResourceLookup map[schema.GroupKind]schema.GroupVersionKind
}
func NewResolver(client kubeclient.Client) *Resolver {
return &Resolver{
client: client,
kubeSecretResolver: NewKubeSecretResolver(client),
kubeSecretMapResolver: NewKubeSecretMapResolver(client),
kubeConfigMapResolver: NewKubeConfigMapResolver(client),
reconciledResourceLookup: make(map[schema.GroupKind]schema.GroupVersionKind),
}
}
func (r *Resolver) IndexStorageTypes(scheme *runtime.Scheme, objs []*registration.StorageType) error {
for _, obj := range objs {
gvk, err := apiutil.GVKForObject(obj.Obj, scheme)
if err != nil {
return eris.Wrapf(err, "creating GVK for obj %T", obj)
}
groupKind := schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}
if existing, ok := r.reconciledResourceLookup[groupKind]; ok {
if existing == gvk {
continue
}
return eris.Errorf(
"group: %q, kind: %q already has registered storage version %q, but found %q as well",
gvk.Group,
gvk.Kind,
existing.Version,
gvk.Version)
}
r.reconciledResourceLookup[groupKind] = gvk
}
return nil
}
// ResolveReferenceToARMID gets a references ARM ID. If the reference is just pointing to an ARM resource then the ARMID is returned.
// If the reference is pointing to a Kubernetes resource, that resource is looked up and its ARM ID is computed.
func (r *Resolver) ResolveReferenceToARMID(ctx context.Context, ref genruntime.NamespacedResourceReference) (string, error) {
if ref.IsDirectARMReference() {
return ref.ARMID, nil
}
obj, err := r.ResolveReference(ctx, ref)
if err != nil {
return "", err
}
// There are two ways to get the ARM ID here, we can look it up using GetResourceID, which will only work if the
// resource has actually been successfully deployed to Azure, or we can "compute" it. Currently it's harder to compute
// it given that a resource doesn't know what subscription it's deployed in... but we should probably change that
// and move to computing it here.
id, ok := genruntime.GetResourceID(obj)
if !ok {
// Resource doesn't have a resource ID. This probably means it's not done deploying
return "", eris.Errorf("ref %s doesn't have an assigned ARM ID", ref)
}
return id, nil
}
// ResolveReferencesToARMIDs resolves all provided references to their ARM IDs.
func (r *Resolver) ResolveReferencesToARMIDs(ctx context.Context, refs map[genruntime.NamespacedResourceReference]struct{}) (genruntime.Resolved[genruntime.ResourceReference, string], error) {
result := make(map[genruntime.ResourceReference]string, len(refs))
for ref := range refs {
armID, err := r.ResolveReferenceToARMID(ctx, ref)
if err != nil {
return genruntime.MakeResolved[genruntime.ResourceReference, string](nil), err
}
result[ref.ResourceReference] = armID
}
return genruntime.MakeResolved[genruntime.ResourceReference, string](result), nil
}
// ResolveResourceReferences resolves every reference found on the specified genruntime.ARMMetaObject to its corresponding ARM ID.
func (r *Resolver) ResolveResourceReferences(ctx context.Context, metaObject genruntime.ARMMetaObject) (genruntime.Resolved[genruntime.ResourceReference, string], error) {
refs, err := reflecthelpers.FindResourceReferences(metaObject)
if err != nil {
return genruntime.Resolved[genruntime.ResourceReference, string]{}, eris.Wrapf(err, "finding references on %q", metaObject.GetName())
}
// Include the namespace
namespacedRefs := make(map[genruntime.NamespacedResourceReference]struct{}, len(refs))
for ref := range refs {
namespacedRefs[ref.AsNamespacedRef(metaObject.GetNamespace())] = struct{}{}
}
// resolve them
resolvedRefs, err := r.ResolveReferencesToARMIDs(ctx, namespacedRefs)
if err != nil {
return genruntime.Resolved[genruntime.ResourceReference, string]{}, eris.Wrapf(err, "failed resolving ARM IDs for references")
}
return resolvedRefs, nil
}
// ResolveResourceHierarchy gets the resource hierarchy for a given resource. The result is a slice of
// resources, with the uppermost parent at position 0 and the resource itself at position len(slice)-1.
// Note that there is NO GUARANTEE that this hierarchy is "complete". It may root up to a resource which uses
// the ARMID field of owner.
func (r *Resolver) ResolveResourceHierarchy(ctx context.Context, obj genruntime.ARMMetaObject) (ResourceHierarchy, error) {
owner := obj.Owner()
if owner == nil {
return ResourceHierarchy{obj}, nil
}
ownerDetails, err := r.ResolveOwner(ctx, obj)
if err != nil {
return nil, err
}
if ownerDetails.Result == OwnerFoundARM {
return ResourceHierarchy{obj}, nil
}
owners, err := r.ResolveResourceHierarchy(ctx, ownerDetails.Owner)
if err != nil {
return nil, eris.Wrapf(err, "getting owners for %s", ownerDetails.Owner.GetName())
}
return append(owners, obj), nil
}
// ResolveReference resolves a reference, or returns an error if the reference is not pointing to a KubernetesResource
func (r *Resolver) ResolveReference(ctx context.Context, ref genruntime.NamespacedResourceReference) (genruntime.ARMMetaObject, error) {
refGVK, err := r.findGVK(ref)
if err != nil {
return nil, err
}
refNamespacedName := types.NamespacedName{
Namespace: ref.Namespace,
Name: ref.Name,
}
refObj, err := r.client.GetObject(ctx, refNamespacedName, refGVK)
if err != nil {
if apierrors.IsNotFound(err) {
// Check if the user has mistakenly put the armID in 'name' field
_, err1 := arm.ParseResourceID(ref.Name)
if err1 == nil {
return nil, eris.Errorf("couldn't resolve reference %s. 'name' looks like it might be an ARM ID; did you mean 'armID: %s'?", refNamespacedName.String(), ref.Name)
}
err := core.NewReferenceNotFoundError(refNamespacedName, err)
return nil, eris.Wrapf(
err,
"couldn't resolve reference %s/%s",
ref.Namespace,
ref.Name)
}
return nil, eris.Wrapf(err, "couldn't resolve reference %s", ref.String())
}
metaObj, ok := refObj.(genruntime.ARMMetaObject)
if !ok {
return nil, eris.Errorf("reference %s (%s) was not of type genruntime.ARMMetaObject", refNamespacedName, refGVK)
}
return metaObj, nil
}
type ResolveOwnerResult string
const (
// OwnerFoundKubernetes indicates the owner was found in Kubernetes.
OwnerFoundKubernetes = ResolveOwnerResult("OwnerFoundKubernetes")
// OwnerFoundARM indicates the owner is an ARM ID. The resource the ARM ID points to may or may not exist in Azure currently.
OwnerFoundARM = ResolveOwnerResult("OwnerFoundARM")
// OwnerNotExpected indicates that this resource is not expected to have any owner. (Example: ResourceGroup)
OwnerNotExpected = ResolveOwnerResult("OwnerNotExpected")
)
type OwnerDetails struct {
Result ResolveOwnerResult
Owner genruntime.ARMMetaObject
ARMID string
}
func (det OwnerDetails) FoundKubernetesOwner() bool {
if det.Result == OwnerNotExpected || det.Result == OwnerFoundARM {
// If no owner is expected or the owner is only in ARM, no need to assign ownership in Kubernetes
return false
}
return true
}
func OwnerDetailsFromKubernetes(owner genruntime.ARMMetaObject) OwnerDetails {
return OwnerDetails{
Result: OwnerFoundKubernetes,
Owner: owner,
}
}
func OwnerDetailsFromARM(armID string) OwnerDetails {
return OwnerDetails{
Result: OwnerFoundARM,
ARMID: armID,
}
}
func OwnerDetailsNotExpected() OwnerDetails {
return OwnerDetails{
Result: OwnerNotExpected,
}
}
// ResolveOwner returns an OwnerDetails describing more information about the owner of the provided resource.
// If the resource is supposed to have
// an owner but doesn't, this returns an ReferenceNotFound error. If the resource is not supposed
// to have an owner (for example, ResourceGroup) or the owner points to a raw ARM ID this returns an OwnerDetails
// with the OwnerDetails.Result set appropriately.
func (r *Resolver) ResolveOwner(ctx context.Context, obj genruntime.ARMOwnedMetaObject) (OwnerDetails, error) {
owner := obj.Owner()
if owner == nil {
return OwnerDetailsNotExpected(), nil
}
if owner.IsDirectARMReference() {
return OwnerDetailsFromARM(owner.ARMID), nil
}
namespacedRef := genruntime.NamespacedResourceReference{
ResourceReference: *owner,
Namespace: obj.GetNamespace(),
}
ownerMeta, err := r.ResolveReference(ctx, namespacedRef)
if err != nil {
return OwnerDetails{}, err
}
return OwnerDetailsFromKubernetes(ownerMeta), nil
}
// Scheme returns the current scheme from our client
func (r *Resolver) Scheme() *runtime.Scheme {
return r.client.Scheme()
}
func (r *Resolver) findGVK(ref genruntime.NamespacedResourceReference) (schema.GroupVersionKind, error) {
var ownerGvk schema.GroupVersionKind
if !ref.IsKubernetesReference() {
return ownerGvk, eris.Errorf("reference %s is not pointing to a Kubernetes resource", ref)
}
groupKind := schema.GroupKind{Group: ref.Group, Kind: ref.Kind}
gvk, ok := r.reconciledResourceLookup[groupKind]
if !ok {
return ownerGvk, eris.Errorf("group: %q, kind: %q was not in reconciledResourceLookup", ref.Group, ref.Kind)
}
return gvk, nil
}
// ResolveSecretReferences resolves all provided secret references
func (r *Resolver) ResolveSecretReferences(
ctx context.Context,
refs set.Set[genruntime.NamespacedSecretReference],
) (genruntime.Resolved[genruntime.SecretReference, string], error) {
return r.kubeSecretResolver.ResolveSecretReferences(ctx, refs)
}
// ResolveResourceSecretReferences resolves all of the specified genruntime.MetaObject's secret references.
func (r *Resolver) ResolveResourceSecretReferences(ctx context.Context, metaObject genruntime.MetaObject) (genruntime.Resolved[genruntime.SecretReference, string], error) {
refs, err := reflecthelpers.FindSecretReferences(metaObject)
if err != nil {
return genruntime.Resolved[genruntime.SecretReference, string]{}, eris.Wrapf(err, "finding secrets on %q", metaObject.GetName())
}
// Include the namespace
namespacedSecretRefs := set.Make[genruntime.NamespacedSecretReference]()
for ref := range refs {
namespacedSecretRefs.Add(ref.AsNamespacedRef(metaObject.GetNamespace()))
}
// resolve them
resolvedSecrets, err := r.ResolveSecretReferences(ctx, namespacedSecretRefs)
if err != nil {
return genruntime.Resolved[genruntime.SecretReference, string]{}, eris.Wrapf(err, "failed resolving secret references")
}
return resolvedSecrets, nil
}
// ResolveSecretMapReferences resolves all provided secret map references
func (r *Resolver) ResolveSecretMapReferences(
ctx context.Context,
refs set.Set[genruntime.NamespacedSecretMapReference],
) (genruntime.Resolved[genruntime.SecretMapReference, map[string]string], error) {
return r.kubeSecretMapResolver.ResolveSecretMapReferences(ctx, refs)
}
// ResolveResourceSecretMapReferences resolves all the specified genruntime.MetaObject's secret maps.
func (r *Resolver) ResolveResourceSecretMapReferences(
ctx context.Context,
metaObject genruntime.MetaObject,
) (genruntime.Resolved[genruntime.SecretMapReference, map[string]string], error) {
refs, err := reflecthelpers.FindSecretMaps(metaObject)
if err != nil {
return genruntime.Resolved[genruntime.SecretMapReference, map[string]string]{}, eris.Wrapf(err, "finding secrets on %q", metaObject.GetName())
}
// Include the namespace
namespacedSecretRefs := set.Make[genruntime.NamespacedSecretMapReference]()
for ref := range refs {
namespacedSecretRefs.Add(ref.AsNamespacedRef(metaObject.GetNamespace()))
}
// resolve them
resolvedSecrets, err := r.ResolveSecretMapReferences(ctx, namespacedSecretRefs)
if err != nil {
return genruntime.Resolved[genruntime.SecretMapReference, map[string]string]{}, eris.Wrapf(err, "failed resolving secret references")
}
return resolvedSecrets, nil
}
// ResolveConfigMapReferences resolves all provided secret references
func (r *Resolver) ResolveConfigMapReferences(
ctx context.Context,
refs set.Set[genruntime.NamespacedConfigMapReference],
) (genruntime.Resolved[genruntime.ConfigMapReference, string], error) {
return r.kubeConfigMapResolver.ResolveConfigMapReferences(ctx, refs)
}
// ResolveResourceConfigMapReferences resolves the specified genruntime.MetaObject's configmap references.
func (r *Resolver) ResolveResourceConfigMapReferences(ctx context.Context, metaObject genruntime.MetaObject) (genruntime.Resolved[genruntime.ConfigMapReference, string], error) {
refs, err := reflecthelpers.FindConfigMapReferences(metaObject)
if err != nil {
return genruntime.Resolved[genruntime.ConfigMapReference, string]{}, eris.Wrapf(err, "finding config maps on %q", metaObject.GetName())
}
// Include the namespace
namespacedConfigMapReferences := set.Make[genruntime.NamespacedConfigMapReference]()
for ref := range refs {
namespacedConfigMapReferences.Add(ref.AsNamespacedRef(metaObject.GetNamespace()))
}
// resolve them
resolvedConfigMaps, err := r.ResolveConfigMapReferences(ctx, namespacedConfigMapReferences)
if err != nil {
return genruntime.Resolved[genruntime.ConfigMapReference, string]{}, eris.Wrapf(err, "failed resolving config map references")
}
return resolvedConfigMaps, nil
}
// ResolveAll resolves every reference on the provided genruntime.ARMMetaObject.
// This includes: owner, all resource references, and all secrets.
func (r *Resolver) ResolveAll(ctx context.Context, metaObject genruntime.ARMMetaObject) (ResourceHierarchy, genruntime.ConvertToARMResolvedDetails, error) {
// Resolve the resource hierarchy (owner)
resourceHierarchy, err := r.ResolveResourceHierarchy(ctx, metaObject)
if err != nil {
return nil, genruntime.ConvertToARMResolvedDetails{}, err
}
// Resolve all ARM ID references
resolvedRefs, err := r.ResolveResourceReferences(ctx, metaObject)
if err != nil {
return nil, genruntime.ConvertToARMResolvedDetails{}, err
}
// Resolve all secrets
resolvedSecrets, err := r.ResolveResourceSecretReferences(ctx, metaObject)
if err != nil {
return nil, genruntime.ConvertToARMResolvedDetails{}, err
}
resolvedSecretMaps, err := r.ResolveResourceSecretMapReferences(ctx, metaObject)
if err != nil {
return nil, genruntime.ConvertToARMResolvedDetails{}, err
}
// Resolve all configmaps
resolvedConfigMaps, err := r.ResolveResourceConfigMapReferences(ctx, metaObject)
if err != nil {
return nil, genruntime.ConvertToARMResolvedDetails{}, err
}
resolvedDetails := genruntime.ConvertToARMResolvedDetails{
Name: resourceHierarchy.AzureName(),
ResolvedReferences: resolvedRefs,
ResolvedSecrets: resolvedSecrets,
ResolvedSecretMaps: resolvedSecretMaps,
ResolvedConfigMaps: resolvedConfigMaps,
}
return resourceHierarchy, resolvedDetails, nil
}