diff --git a/README.md b/README.md index 9fc8371..49a56d5 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ If you need to create DNS records for your machines, you'll therefore be require We've currently only implemented one strategy for identifying the hostname of a machine, since it's the one we (Deutsche Telekom) are using. In case you have other requirements, we're open to accept contributions for new strategies. Please open an issue if you're interested. -Our strategy uses the name of the CAPI `Machine` as the hostname. To determine the Machine name the provider follows the owner chain from the `IPAddressClaim` via the infrastructure provider resources to the `Machine`. Our implementation is currently provider specific and only supports the VSphere and metal3 providers. +Our strategy uses the name of the CAPI `Machine` as the hostname. To determine the Machine name the provider follows the owner chain from the `IPAddressClaim` via the infrastructure provider resources to the `Machine`. This is used by searching through the owner references up to a depth of five. To enable setting DNS entries, set the `spec.dnsZone` parameter on the `InfobloxIPPool` to your desired zone. The resulting DNS entries will then be `.`. The DNS view will be set to `default.`. diff --git a/internal/controllers/ipaddressclaim.go b/internal/controllers/ipaddressclaim.go index 47910c3..8745d85 100644 --- a/internal/controllers/ipaddressclaim.go +++ b/internal/controllers/ipaddressclaim.go @@ -278,26 +278,9 @@ func (h *InfobloxClaimHandler) getHostname(ctx context.Context) (string, error) } func getHostnameResolver(cl client.Client, claim *ipamv1.IPAddressClaim) (hostname.Resolver, error) { - switch claim.Kind { - case "Metal3Data": - return &hostname.OwnerChainResolver{ - Client: cl, - Chain: []metav1.GroupKind{ - {Group: "infrastructure.cluster.x-k8s.io", Kind: "Metal3Data"}, - {Group: "infrastructure.cluster.x-k8s.io", Kind: "Metal3Machine"}, - {Group: "cluster.x-k8s.io", Kind: "Machine"}, - }, - }, nil - case "VSphereVM": - return &hostname.OwnerChainResolver{ - Client: cl, - Chain: []metav1.GroupKind{ - {Group: "infrastructure.cluster.x-k8s.io", Kind: "VSphereVM"}, - {Group: "infrastructure.cluster.x-k8s.io", Kind: "VSphereMachine"}, - {Group: "cluster.x-k8s.io", Kind: "Machine"}, - }, - }, nil - default: - return nil, fmt.Errorf("failed to create resolver for kind %s", claim.Kind) - } + return &hostname.SearchOwnerReferenceResolver{ + Client: cl, + SearchFor: metav1.GroupKind{Group: "cluster.x-k8s.io", Kind: "Machine"}, + MaxDepth: 5, + }, nil } diff --git a/internal/hostname/resolver.go b/internal/hostname/resolver.go index 4e8f02a..ae077e4 100644 --- a/internal/hostname/resolver.go +++ b/internal/hostname/resolver.go @@ -4,6 +4,7 @@ package hostname import ( "context" "fmt" + "slices" "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -50,6 +51,75 @@ func (r *OwnerChainResolver) GetHostname(ctx context.Context, claim *ipamv1.IPAd return "", fmt.Errorf("failed to follow owner chain") } +// SearchOwnerReferenceResolver performs a depth search on the owner references until it finds the specified [metav1.GroupKind] +// and uses it's name as the hostname. +type SearchOwnerReferenceResolver struct { + client.Client + MaxDepth int + SearchFor metav1.GroupKind +} + +// GetHostname returns the hostname for the specified claim. +func (r *SearchOwnerReferenceResolver) GetHostname(ctx context.Context, claim *ipamv1.IPAddressClaim) (string, error) { + if r.MaxDepth == 0 { + r.MaxDepth = 5 + } + obj := client.Object(claim) + name, err := r.find(ctx, obj, 1) + if err != nil { + return "", err + } + if name != "" { + return name, nil + } + return "", fmt.Errorf("failed to find owner reference to specified group and kind") +} + +func (r *SearchOwnerReferenceResolver) find(ctx context.Context, obj client.Object, currentDepth int) (string, error) { + nextRefs := []metav1.OwnerReference{} + for _, o := range obj.GetOwnerReferences() { + if o.Kind == r.SearchFor.Kind && apiVersionToGroupVersion(o.APIVersion).Group == r.SearchFor.Group { + return o.Name, nil + } + + nextRefs = append(nextRefs, o) + } + + // We'll try to iterate through promising things first to reduce the amount of api requests. + // The simple heuristic is that anything in the infrastructure.capi.x-k8s.io group or anything that contains Machine in + // it's name comes first. The name is more important than the group. + // We don't care for equality since this is just optimization. + slices.SortFunc(nextRefs, func(a, b metav1.OwnerReference) int { + if strings.Contains(b.Kind, "Machine") { + return 1 + } + if strings.Contains(a.Kind, "Machine") || strings.HasPrefix(b.APIVersion, "infrastructure") { + return -1 + } + if strings.HasPrefix(b.APIVersion, "infrastructure") { + return 1 + } + return 0 + }) + + for _, o := range nextRefs { + if currentDepth >= r.MaxDepth { + continue + } + + obj2 := &unstructured.Unstructured{} + obj2.SetAPIVersion(o.APIVersion) + obj2.SetKind(o.Kind) + if err := r.Client.Get(ctx, types.NamespacedName{Name: o.Name, Namespace: obj2.GetNamespace()}, obj2); err != nil { + return "", err + } + if name, err := r.find(ctx, obj2, currentDepth+1); name != "" || err != nil { + return name, err + } + } + return "", nil +} + // findOwnerReferenceWithGK searches the owner references of an object and returns the first with the specified [metav1.GroupVersion]. func findOwnerReferenceWithGK(obj client.Object, gk metav1.GroupKind) (metav1.OwnerReference, error) { for _, o := range obj.GetOwnerReferences() { diff --git a/internal/hostname/resolver_test.go b/internal/hostname/resolver_test.go index d8a78b0..5ec5bb2 100644 --- a/internal/hostname/resolver_test.go +++ b/internal/hostname/resolver_test.go @@ -27,52 +27,42 @@ var _ = Describe("determining hostnames", func() { Expect(capv1.AddToScheme(testScheme)).To(Succeed()) Context("metal3", func() { - When("the owner chain can be resolved", func() { - var cl client.Client - var claim ipamv1.IPAddressClaim - BeforeEach(func() { - cl = fake.NewClientBuilder(). - WithScheme(testScheme). - WithObjects( - &metal3v1.Metal3Data{ - ObjectMeta: metav1.ObjectMeta{ - Name: "data", - OwnerReferences: []metav1.OwnerReference{ - { - Name: "machine", - Kind: "Metal3Machine", - APIVersion: metal3v1.GroupVersion.String(), - }, + var cl client.Client + var claim ipamv1.IPAddressClaim + BeforeEach(func() { + cl = fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects( + &metal3v1.Metal3Data{ + ObjectMeta: metav1.ObjectMeta{ + Name: "data", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "machine", + Kind: "Metal3Machine", + APIVersion: metal3v1.GroupVersion.String(), }, }, }, - &metal3v1.Metal3Machine{ - ObjectMeta: metav1.ObjectMeta{ - Name: "machine", - OwnerReferences: []metav1.OwnerReference{ - { - Name: "capimachine", - Kind: "Machine", - APIVersion: "cluster.x-k8s.io/v1beta1", - }, + }, + &metal3v1.Metal3Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "machine", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "capimachine", + Kind: "Machine", + APIVersion: "cluster.x-k8s.io/v1beta1", }, }, }, - ). - Build() - claim = ipamv1.IPAddressClaim{ - ObjectMeta: metav1.ObjectMeta{ - OwnerReferences: []metav1.OwnerReference{ - { - Name: "data", - Kind: "Metal3Data", - APIVersion: metal3v1.GroupVersion.String(), - }, - }, }, - } - }) - It("the name of the capi Machine is used as the hostname", func() { + ). + Build() + claim = newClaim("data", "Metal3Data", metal3v1.GroupVersion.String()) + }) + Context("OwnerChainResolver", func() { + It("finds the capi machine's name", func() { r := OwnerChainResolver{Client: cl, Chain: []metav1.GroupKind{ {Group: "infrastructure.cluster.x-k8s.io", Kind: "Metal3Data"}, {Group: "infrastructure.cluster.x-k8s.io", Kind: "Metal3Machine"}, @@ -81,53 +71,49 @@ var _ = Describe("determining hostnames", func() { Expect(r.GetHostname(context.Background(), &claim)).To(Equal("capimachine")) }) }) + Context("SearchOwnerReferenceResolver", func() { + It("finds the capi machine's name", func() { + r := SearchOwnerReferenceResolver{Client: cl, SearchFor: metav1.GroupKind{Group: "cluster.x-k8s.io", Kind: "Machine"}} + Expect(r.GetHostname(context.Background(), &claim)).To(Equal("capimachine")) + }) + }) }) Context("vsphere", func() { - When("the owner chain can be resolved", func() { - var cl client.Client - var claim ipamv1.IPAddressClaim - BeforeEach(func() { - cl = fake.NewClientBuilder(). - WithScheme(testScheme). - WithObjects( - &capv1.VSphereVM{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vm", - OwnerReferences: []metav1.OwnerReference{ - { - Name: "machine", - Kind: "VSphereMachine", - APIVersion: capv1.GroupVersion.String(), - }, + var cl client.Client + var claim ipamv1.IPAddressClaim + BeforeEach(func() { + cl = fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects([]client.Object{ + &capv1.VSphereVM{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vm", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "machine", + Kind: "VSphereMachine", + APIVersion: capv1.GroupVersion.String(), }, }, }, - &capv1.VSphereMachine{ - ObjectMeta: metav1.ObjectMeta{ - Name: "machine", - OwnerReferences: []metav1.OwnerReference{ - { - Name: "capimachine", - Kind: "Machine", - APIVersion: "cluster.x-k8s.io/v1beta1", - }, + }, + &capv1.VSphereMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "machine", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "capimachine", + Kind: "Machine", + APIVersion: "cluster.x-k8s.io/v1beta1", }, }, }, - ). - Build() - claim = ipamv1.IPAddressClaim{ - ObjectMeta: metav1.ObjectMeta{ - OwnerReferences: []metav1.OwnerReference{ - { - Name: "vm", - Kind: "VSphereVM", - APIVersion: capv1.GroupVersion.String(), - }, - }, }, - } - }) + }...). + Build() + claim = newClaim("vm", "VSphereVM", capv1.GroupVersion.String()) + }) + Context("OwnerChainResolver", func() { It("the name of the capi Machine is used as the hostname", func() { r := OwnerChainResolver{Client: cl, Chain: []metav1.GroupKind{ {Group: "infrastructure.cluster.x-k8s.io", Kind: "VSphereVM"}, @@ -137,5 +123,25 @@ var _ = Describe("determining hostnames", func() { Expect(r.GetHostname(context.Background(), &claim)).To(Equal("capimachine")) }) }) + Context("SearchOwnerReferenceResolver", func() { + It("finds the capi machine's name", func() { + r := SearchOwnerReferenceResolver{Client: cl, SearchFor: metav1.GroupKind{Group: "cluster.x-k8s.io", Kind: "Machine"}} + Expect(r.GetHostname(context.Background(), &claim)).To(Equal("capimachine")) + }) + }) }) }) + +func newClaim(name, kind, apiVersion string) ipamv1.IPAddressClaim { + return ipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{ + { + Name: name, + Kind: kind, + APIVersion: apiVersion, + }, + }, + }, + } +}