v2/pkg/genruntime/resource_reference.go (231 lines of code) (raw):

/* * Copyright (c) Microsoft Corporation. * Licensed under the MIT license. */ // +kubebuilder:validation:Optional package genruntime import ( "fmt" "reflect" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/rotisserie/eris" "k8s.io/apimachinery/pkg/runtime/schema" kerrors "k8s.io/apimachinery/pkg/util/errors" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/Azure/azure-service-operator/v2/internal/set" ) // KnownResourceReference is a resource reference to a known type. // +kubebuilder:object:generate=true type KnownResourceReference struct { // TODO: In practice this type is used only for Owner fields and so might more appropriately have been called OwnerReference // TODO: but changing to that would be a breaking change so avoiding it. // This is the name of the Kubernetes resource to reference. Name string `json:"name,omitempty"` // References across namespaces are not supported. // Note that ownership across namespaces in Kubernetes is not allowed, but technically resource // references are. There are RBAC considerations here though so probably easier to just start by // disallowing cross-namespace references for now // +kubebuilder:validation:Pattern="(?i)(^(/subscriptions/([^/]+)(/resourcegroups/([^/]+))?)?/providers/([^/]+)/([^/]+/[^/]+)(/([^/]+/[^/]+))*$|^/subscriptions/([^/]+)(/resourcegroups/([^/]+))?$)" ARMID string `json:"armId,omitempty"` } // AsResourceReference transforms this KnownResourceReference into a ResourceReference func (ref *KnownResourceReference) AsResourceReference(group string, kind string) *ResourceReference { if ref.Name == "" { group = "" kind = "" } return &ResourceReference{ Group: group, Kind: kind, Name: ref.Name, ARMID: ref.ARMID, } } // KubernetesOwnerReference is a resource reference to a known type in Kuberentes. Most types support // ARM references as well but some (such as SQL users) do not. // +kubebuilder:object:generate=true type KubernetesOwnerReference struct { // +kubebuilder:validation:Required // This is the name of the Kubernetes resource to reference. Name string `json:"name,omitempty"` } // AsResourceReference transforms this KnownResourceReference into a ResourceReference func (ref *KubernetesOwnerReference) AsResourceReference(group string, kind string) *ResourceReference { return &ResourceReference{ Group: group, Kind: kind, Name: ref.Name, } } // TODO: This type and ResourceReference are almost exactly the same now... // ArbitraryOwnerReference is an owner reference to an unknown type. // +kubebuilder:object:generate=true type ArbitraryOwnerReference struct { // This is the name of the Kubernetes resource to reference. Name string `json:"name,omitempty"` // Group is the Kubernetes group of the resource. Group string `json:"group,omitempty"` // Kind is the Kubernetes kind of the resource. Kind string `json:"kind,omitempty"` // Ownership across namespaces is not supported. // +kubebuilder:validation:Pattern="(?i)(^(/subscriptions/([^/]+)(/resourcegroups/([^/]+))?)?/providers/([^/]+)/([^/]+/[^/]+)(/([^/]+/[^/]+))*$|^/subscriptions/([^/]+)(/resourcegroups/([^/]+))?$)" ARMID string `json:"armId,omitempty"` } // AsResourceReference transforms this ArbitraryOwnerReference into a ResourceReference func (ref *ArbitraryOwnerReference) AsResourceReference() *ResourceReference { return &ResourceReference{ Group: ref.Group, Kind: ref.Kind, Name: ref.Name, ARMID: ref.ARMID, } } var _ fmt.Stringer = ResourceReference{} // ResourceReference represents a resource reference, either to a Kubernetes resource or directly to an Azure resource via ARMID // +kubebuilder:object:generate=true // //nolint:recvcheck type ResourceReference struct { // Group is the Kubernetes group of the resource. Group string `json:"group,omitempty"` // Kind is the Kubernetes kind of the resource. Kind string `json:"kind,omitempty"` // Name is the Kubernetes name of the resource. Name string `json:"name,omitempty"` // Note: Version is not required here because references are all about linking one Kubernetes // resource to another, and Kubernetes resources are uniquely identified by group, kind, (optionally namespace) and // name - the versions are just giving a different view on the same resource // Here are some test patterns for it: https://regex101.com/r/K7l3sv/1 // +kubebuilder:validation:Pattern="(?i)(^(/subscriptions/([^/]+)(/resourcegroups/([^/]+))?)?/providers/([^/]+)/([^/]+/[^/]+)(/([^/]+/[^/]+))*$|^/subscriptions/([^/]+)(/resourcegroups/([^/]+))?$)" // ARMID is a string of the form /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}. // The /resourcegroups/{resourceGroupName} bit is optional as some resources are scoped at the subscription level // ARMID is mutually exclusive with Group, Kind, Namespace and Name. ARMID string `json:"armId,omitempty"` } // CreateResourceReferenceFromARMID creates a new ResourceReference from a string representing an ARM ID func CreateResourceReferenceFromARMID(armID string) ResourceReference { return ResourceReference{ ARMID: armID, } } // IsDirectARMReference returns true if this ResourceReference is referring to an ARMID directly. func (ref *ResourceReference) IsDirectARMReference() bool { return ref.ARMID != "" && ref.Name == "" && ref.Group == "" && ref.Kind == "" } // IsKubernetesReference returns true if this ResourceReference is referring to a Kubernetes resource. func (ref *ResourceReference) IsKubernetesReference() bool { return ref.ARMID == "" && ref.Name != "" && ref.Group != "" && ref.Kind != "" } func (ref ResourceReference) String() string { if ref.IsDirectARMReference() { return ref.ARMID } if ref.IsKubernetesReference() { return fmt.Sprintf("%s, Group/Kind: %s/%s", ref.Name, ref.Group, ref.Kind) } // Printing all the fields here just in case something weird happens and we have an ARMID and also Kubernetes reference stuff return fmt.Sprintf("Group: %q, Kind: %q, Name: %q, ARMID: %q", ref.Group, ref.Kind, ref.Name, ref.ARMID) } // TODO: We wouldn't need this if controller-gen supported DUs or OneOf better, see: https://github.com/kubernetes-sigs/controller-tools/issues/461 // Validate validates the ResourceReference to ensure that it is structurally valid. func (ref *ResourceReference) Validate() (admission.Warnings, error) { if ref.ARMID == "" && ref.Name == "" && ref.Group == "" && ref.Kind == "" { return nil, eris.Errorf("at least one of ['ARMID'] or ['Group', 'Kind', 'Namespace', 'Name'] must be set for ResourceReference") } if ref.ARMID != "" && !ref.IsDirectARMReference() { return nil, eris.Errorf("the 'ARMID' field is mutually exclusive with 'Group', 'Kind', 'Namespace', and 'Name' for ResourceReference: %s", ref.String()) } if ref.ARMID == "" && !ref.IsKubernetesReference() { return nil, eris.Errorf("when referencing a Kubernetes resource, 'Group', 'Kind', 'Namespace', and 'Name' must all be specified for ResourceReference: %s", ref.String()) } return nil, nil } // AsNamespacedRef creates a NamespacedResourceReference from this reference. func (ref *ResourceReference) AsNamespacedRef(namespace string) NamespacedResourceReference { // If this is a direct ARM reference, don't append a namespace as it reads weird if ref.IsDirectARMReference() { return NamespacedResourceReference{ ResourceReference: *ref, } } return NamespacedResourceReference{ ResourceReference: *ref, Namespace: namespace, } } // AsArbitraryOwnerReference creates an ArbitraryOwnerReference from this reference. func (ref *ResourceReference) AsArbitraryOwnerReference() ArbitraryOwnerReference { // If this is a direct ARM reference, return just the ARM ID if ref.IsDirectARMReference() { return ArbitraryOwnerReference{ ARMID: ref.ARMID, } } // Otherwise return GVK return ArbitraryOwnerReference{ Group: ref.Group, Kind: ref.Kind, Name: ref.Name, } } // AsKnownResourceReference creates a KnownResourceReference from this reference. func (ref *ResourceReference) AsKnownResourceReference() KnownResourceReference { // If this is a direct ARM reference, return just the ARM ID if ref.IsDirectARMReference() { return KnownResourceReference{ ARMID: ref.ARMID, } } // Otherwise return just the name return KnownResourceReference{ Name: ref.Name, } } // GroupKind returns the GroupKind of the resource reference func (ref *ResourceReference) GroupKind() schema.GroupKind { return schema.GroupKind{ Group: ref.Group, Kind: ref.Kind, } } // LookupOwnerGroupKind looks up an owners group and kind annotations using reflection. // This is primarily used to convert from a KnownResourceReference to the more general // ResourceReference func LookupOwnerGroupKind(v interface{}) (string, string) { t := reflect.TypeOf(v) field, _ := t.FieldByName("Owner") group, ok := field.Tag.Lookup("group") if !ok { panic("Couldn't find owner group tag") } kind, ok := field.Tag.Lookup("kind") if !ok { panic("Couldn't find %s owner kind tag") } return group, kind } // Copy makes an independent copy of the KnownResourceReference func (ref *KnownResourceReference) Copy() KnownResourceReference { return *ref } // Copy makes an independent copy of the ArbitraryOwnerReference func (ref *ArbitraryOwnerReference) Copy() ArbitraryOwnerReference { return *ref } // Copy makes an independent copy of the ResourceReference func (ref ResourceReference) Copy() ResourceReference { return ref } // Copy makes an independent copy of the KubernetesOwnerReference func (ref *KubernetesOwnerReference) Copy() KubernetesOwnerReference { return *ref } // ValidateResourceReferences calls Validate on each ResourceReference func ValidateResourceReferences(refs set.Set[ResourceReference]) (admission.Warnings, error) { errs := make([]error, 0, len(refs)) var warnings admission.Warnings for ref := range refs { warning, err := ref.Validate() if warning != nil { warnings = append(warnings, warning...) } if err != nil { errs = append(errs, err) } } return warnings, kerrors.NewAggregate(errs) } func VerifyResourceOwnerARMID(resource ARMMetaObject) error { owner := resource.Owner() if owner == nil { return nil } if !owner.IsDirectARMReference() { return nil } armID, err := arm.ParseResourceID(owner.ARMID) if err != nil { return err } provider, rootResourceTypes, err := GetResourceTypeAndProvider(resource) if err != nil { return err } expectedResourceTypesIncludedInARMID := rootResourceTypes[:len(rootResourceTypes)-1] // Ensure that the ARM ID actually has a suffix containing the resource types we expect if len(expectedResourceTypesIncludedInARMID) > 0 { if !strings.EqualFold(armID.ResourceType.Namespace, provider) { return eris.Errorf( "expected owner ARM ID to be from provider %q, but was %q", provider, armID.ResourceType.Namespace) } expectedARMIDType := strings.Join(expectedResourceTypesIncludedInARMID, "/") if !strings.EqualFold(armID.ResourceType.Type, expectedARMIDType) { return eris.Errorf( "expected owner ARM ID to be of type %q, but was %q", fmt.Sprintf("%s/%s", provider, expectedARMIDType), armID.ResourceType.String()) } } else if len(expectedResourceTypesIncludedInARMID) == 0 { scope := resource.GetResourceScope() if scope == ResourceScopeResourceGroup && armID.ResourceType.String() != "Microsoft.Resources/resourceGroups" { return eris.Errorf( "expected owner ARM ID to be for a resource group, but was %q", armID.ResourceType.String()) } } return nil } // ValidateOwner calls Validate on the resource Owner func ValidateOwner(obj ARMMetaObject) (admission.Warnings, error) { owner := obj.Owner() if owner == nil { return nil, nil } var warningsResult admission.Warnings warnings, err := owner.Validate() warningsResult = append(warningsResult, warnings...) if err != nil { return warningsResult, err } err = VerifyResourceOwnerARMID(obj) if err != nil { return warningsResult, err } return warningsResult, nil } // NamespacedResourceReference is a resource reference with namespace information included type NamespacedResourceReference struct { ResourceReference Namespace string }