From e49db08b28f5080f31a010fbdbebdf5c6eca2155 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 3 Nov 2026 11:20:25 -0600 Subject: [PATCH 1/5] feat(private-dns): copy resolver context labels to its managed zone The managed zone and association now carry the context's labels next to the managed marker, and existing ones are labelled on the next pass, so the creator of a context can say what its zone is for. --- docs/architecture/private-dns/README.md | 5 +- .../controlplane/context_reconciler.go | 58 ++++++++++++++++++- .../controlplane/managed_domain_test.go | 53 +++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/docs/architecture/private-dns/README.md b/docs/architecture/private-dns/README.md index 0bc07f24..1422c9a4 100644 --- a/docs/architecture/private-dns/README.md +++ b/docs/architecture/private-dns/README.md @@ -523,7 +523,10 @@ independent zones with the same apex. Static records have no health lease. The context directly owns its managed zone and association. DNS allocates the requested zone and exposes its reference and suffix in context status. Product -publishers use these outputs. A naming policy +publishers use these outputs. The managed zone and association carry the label +`dns.networking.miloapis.com/managed: "true"` and a copy of the context's own +labels, so whoever creates the context can tag what the zone is for, such as the +network it serves. A naming policy adds custom names without a zone choice on each Compute or Connect resource. ```yaml diff --git a/internal/internaldns/controlplane/context_reconciler.go b/internal/internaldns/controlplane/context_reconciler.go index 85ecd06e..d7b52758 100644 --- a/internal/internaldns/controlplane/context_reconciler.go +++ b/internal/internaldns/controlplane/context_reconciler.go @@ -776,10 +776,58 @@ func (r *Reconciler) withdrawManagedZone(ctx context.Context, c *dnsv1alpha1.DNS return nil } +const managedObjectLabel = "dns.networking.miloapis.com/managed" + +// managedObjectLabels labels the zone and association a context manages with +// the context's own labels, so whoever creates the context can say what the +// zone is for, and marks them as managed. +func managedObjectLabels(c *dnsv1alpha1.DNSResolverContext) map[string]string { + l := make(map[string]string, len(c.Labels)+1) + for k, v := range c.Labels { + l[k] = v + } + l[managedObjectLabel] = "true" + return l +} + +// syncManagedLabels adds or updates the context's labels on an object the +// context controls, for objects created before the context had them. Labels +// the context drops stay on the object. +func (r *Reconciler) syncManagedLabels(ctx context.Context, c *dnsv1alpha1.DNSResolverContext, o client.Object) error { + if !metav1.IsControlledBy(o, c) || !o.GetDeletionTimestamp().IsZero() { + return nil + } + want := managedObjectLabels(c) + current := o.GetLabels() + missing := false + for k, v := range want { + if got, ok := current[k]; !ok || got != v { + missing = true + break + } + } + if !missing { + return nil + } + base := o.DeepCopyObject().(client.Object) + merged := make(map[string]string, len(current)+len(want)) + for k, v := range current { + merged[k] = v + } + for k, v := range want { + merged[k] = v + } + o.SetLabels(merged) + if listed, _ := ctx.Value(listedManagedObjectsKey{}).(*listedManagedObjects); listed != nil { + listed.changed = true + } + return client.IgnoreNotFound(r.Patch(ctx, o, client.MergeFrom(base))) +} + func (r *Reconciler) reconcileManagedZone(ctx context.Context, c *dnsv1alpha1.DNSResolverContext) (bool, error) { suffix := canonicalName(c.Spec.ManagedNamespace.DomainSuffix) zoneName := managedZoneName(c) - zone := &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: c.Namespace, Labels: map[string]string{"dns.networking.miloapis.com/managed": "true"}}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: suffix, DNSZoneClassName: r.Options.PrivateZoneClassName, Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}} + zone := &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: c.Namespace, Labels: managedObjectLabels(c)}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: suffix, DNSZoneClassName: r.Options.PrivateZoneClassName, Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}} if r.Scheme != nil { if err := controllerutil.SetControllerReference(c, zone, r.Scheme, controllerutil.WithBlockOwnerDeletion(false)); err != nil { return false, err @@ -788,7 +836,10 @@ func (r *Reconciler) reconcileManagedZone(ctx context.Context, c *dnsv1alpha1.DN if err := r.getOrCreate(ctx, zone); err != nil { return false, err } - association := &dnsv1alpha1.DNSZoneAssociation{ObjectMeta: metav1.ObjectMeta{Name: zoneName + "-context", Namespace: c.Namespace, Labels: map[string]string{"dns.networking.miloapis.com/managed": "true"}}, Spec: dnsv1alpha1.DNSZoneAssociationSpec{DNSZoneRef: dnsv1alpha1.DNSNameReference{Name: zone.Name}, ResolverContextRef: dnsv1alpha1.DNSNameReference{Name: c.Name}}} + if err := r.syncManagedLabels(ctx, c, zone); err != nil { + return false, err + } + association := &dnsv1alpha1.DNSZoneAssociation{ObjectMeta: metav1.ObjectMeta{Name: zoneName + "-context", Namespace: c.Namespace, Labels: managedObjectLabels(c)}, Spec: dnsv1alpha1.DNSZoneAssociationSpec{DNSZoneRef: dnsv1alpha1.DNSNameReference{Name: zone.Name}, ResolverContextRef: dnsv1alpha1.DNSNameReference{Name: c.Name}}} if r.Scheme != nil { if err := controllerutil.SetControllerReference(c, association, r.Scheme, controllerutil.WithBlockOwnerDeletion(false)); err != nil { return false, err @@ -797,6 +848,9 @@ func (r *Reconciler) reconcileManagedZone(ctx context.Context, c *dnsv1alpha1.DN if err := r.getOrCreate(ctx, association); err != nil { return false, err } + if err := r.syncManagedLabels(ctx, c, association); err != nil { + return false, err + } accepted := apimeta.FindStatusCondition(association.Status.Conditions, dnsValueAccepted) refs, err := r.readReferences(ctx, association) if err != nil { diff --git a/internal/internaldns/controlplane/managed_domain_test.go b/internal/internaldns/controlplane/managed_domain_test.go index 23604f37..fca688c5 100644 --- a/internal/internaldns/controlplane/managed_domain_test.go +++ b/internal/internaldns/controlplane/managed_domain_test.go @@ -345,3 +345,56 @@ func TestAssociationInsideReservedScopesIsNotAccepted(t *testing.T) { }) } } + +func TestManagedZoneCarriesContextLabels(t *testing.T) { + s := runtime.NewScheme() + if err := addReferenceStateToScheme(s); err != nil { + t.Fatal(err) + } + resolver := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "vpc-a", Namespace: "project", UID: "vpc-a-context", Generation: 1}, Spec: dnsv1alpha1.DNSResolverContextSpec{ConsumerID: "project-uid/vpc-a", ManagedNamespace: dnsv1alpha1.DNSResolverContextManagedNamespace{Enabled: true, DomainSuffix: "datum.internal"}}} + c := fake.NewClientBuilder().WithScheme(s).WithObjects(resolver).WithStatusSubresource(&dnsv1alpha1.DNSResolverContext{}, &dnsv1alpha1.DNSZoneAssociation{}).WithInterceptorFuncs(interceptor.Funcs{Create: func(ctx context.Context, cl client.WithWatch, obj client.Object, opts ...client.CreateOption) error { + obj.SetUID(types.UID(obj.GetName() + "-uid")) + obj.SetGeneration(1) + return cl.Create(ctx, obj, opts...) + }}).Build() + r := &Reconciler{Client: c, Scheme: s, Options: ReconcilerOptions{ProjectUID: "project-uid", ManagedDomainSuffixes: []string{"internal"}}} + r.Options.defaults() + ctx := context.Background() + zoneKey := client.ObjectKey{Namespace: "project", Name: model.ManagedZoneName(string(resolver.UID))} + assocKey := client.ObjectKey{Namespace: "project", Name: zoneKey.Name + "-context"} + check := func(want map[string]string) { + t.Helper() + var zone dnsv1alpha1.DNSZone + var assoc dnsv1alpha1.DNSZoneAssociation + if err := c.Get(ctx, zoneKey, &zone); err != nil { + t.Fatal(err) + } + if err := c.Get(ctx, assocKey, &assoc); err != nil { + t.Fatal(err) + } + for _, got := range []map[string]string{zone.Labels, assoc.Labels} { + for k, v := range want { + if got[k] != v { + t.Fatalf("label %s = %q, want %q (labels %v)", k, got[k], v, got) + } + } + } + } + + // A zone created before the context had labels is labelled once it does. + if err := r.reconcileResolverContexts(ctx, "project", time.Now()); err != nil { + t.Fatal(err) + } + check(map[string]string{managedObjectLabel: "true"}) + if err := c.Get(ctx, client.ObjectKeyFromObject(resolver), resolver); err != nil { + t.Fatal(err) + } + resolver.Labels = map[string]string{"example.com/network-name": "vpc-a", managedObjectLabel: "false"} + if err := c.Update(ctx, resolver); err != nil { + t.Fatal(err) + } + if err := r.reconcileResolverContexts(ctx, "project", time.Now()); err != nil { + t.Fatal(err) + } + check(map[string]string{"example.com/network-name": "vpc-a", managedObjectLabel: "true"}) +} From eee6029dd67e4587d9f7eefcddf877804f4a5087 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 3 Nov 2026 11:32:20 -0600 Subject: [PATCH 2/5] fix(private-dns): copy only listed context labels, best effort Copy only the labels a context lists in the dns.networking.miloapis.com/managed-object-labels annotation, so labels that track the context (GitOps) never select its managed zone. A failed label write is logged instead of stopping the pass, and the association is left alone while its zone is being deleted. --- docs/architecture/private-dns/README.md | 8 ++- .../controlplane/context_reconciler.go | 66 +++++++++++-------- .../controlplane/managed_domain_test.go | 11 +++- 3 files changed, 54 insertions(+), 31 deletions(-) diff --git a/docs/architecture/private-dns/README.md b/docs/architecture/private-dns/README.md index 1422c9a4..a6089fc4 100644 --- a/docs/architecture/private-dns/README.md +++ b/docs/architecture/private-dns/README.md @@ -524,9 +524,11 @@ independent zones with the same apex. Static records have no health lease. The context directly owns its managed zone and association. DNS allocates the requested zone and exposes its reference and suffix in context status. Product publishers use these outputs. The managed zone and association carry the label -`dns.networking.miloapis.com/managed: "true"` and a copy of the context's own -labels, so whoever creates the context can tag what the zone is for, such as the -network it serves. A naming policy +`dns.networking.miloapis.com/managed: "true"`. They also get a copy of the +context labels named, comma separated, in the context's +`dns.networking.miloapis.com/managed-object-labels` annotation, so whoever +creates the context can tag what the zone is for, such as the network it +serves. Copied labels are added or corrected, never removed. A naming policy adds custom names without a zone choice on each Compute or Connect resource. ```yaml diff --git a/internal/internaldns/controlplane/context_reconciler.go b/internal/internaldns/controlplane/context_reconciler.go index d7b52758..459e0da5 100644 --- a/internal/internaldns/controlplane/context_reconciler.go +++ b/internal/internaldns/controlplane/context_reconciler.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "net/netip" "reflect" "sort" @@ -776,52 +777,64 @@ func (r *Reconciler) withdrawManagedZone(ctx context.Context, c *dnsv1alpha1.DNS return nil } -const managedObjectLabel = "dns.networking.miloapis.com/managed" +const ( + managedObjectLabel = "dns.networking.miloapis.com/managed" + // managedLabelsAnnotation lists, comma separated, the context labels to copy + // onto the zone and association the context manages. Only listed labels + // are copied, so labels used to track the context, such as GitOps ones, + // never select the managed objects. + managedLabelsAnnotation = "dns.networking.miloapis.com/managed-object-labels" +) // managedObjectLabels labels the zone and association a context manages with -// the context's own labels, so whoever creates the context can say what the -// zone is for, and marks them as managed. +// the context labels it lists in managedLabelsAnnotation, so whoever creates +// the context can say what the zone is for, and marks them as managed. func managedObjectLabels(c *dnsv1alpha1.DNSResolverContext) map[string]string { - l := make(map[string]string, len(c.Labels)+1) - for k, v := range c.Labels { - l[k] = v + l := map[string]string{} + for _, key := range strings.Split(c.Annotations[managedLabelsAnnotation], ",") { + key = strings.TrimSpace(key) + if v, ok := c.Labels[key]; ok && key != "" { + l[key] = v + } } l[managedObjectLabel] = "true" return l } -// syncManagedLabels adds or updates the context's labels on an object the -// context controls, for objects created before the context had them. Labels -// the context drops stay on the object. -func (r *Reconciler) syncManagedLabels(ctx context.Context, c *dnsv1alpha1.DNSResolverContext, o client.Object) error { +// syncManagedLabels adds or corrects the copied labels on a live object the +// context controls, for objects created before the context listed them. Labels +// are only added, never removed. Labels are informational, so a failed write +// is logged and never stops the pass. +func (r *Reconciler) syncManagedLabels(ctx context.Context, c *dnsv1alpha1.DNSResolverContext, o client.Object) { if !metav1.IsControlledBy(o, c) || !o.GetDeletionTimestamp().IsZero() { - return nil + return } want := managedObjectLabels(c) current := o.GetLabels() - missing := false + stale := false for k, v := range want { if got, ok := current[k]; !ok || got != v { - missing = true + stale = true break } } - if !missing { - return nil + if !stale { + return } base := o.DeepCopyObject().(client.Object) - merged := make(map[string]string, len(current)+len(want)) - for k, v := range current { - merged[k] = v - } - for k, v := range want { - merged[k] = v + merged := maps.Clone(current) + if merged == nil { + merged = map[string]string{} } + maps.Copy(merged, want) o.SetLabels(merged) if listed, _ := ctx.Value(listedManagedObjectsKey{}).(*listedManagedObjects); listed != nil { listed.changed = true } - return client.IgnoreNotFound(r.Patch(ctx, o, client.MergeFrom(base))) + if err := r.Patch(ctx, o, client.MergeFrom(base)); err != nil && !apierrors.IsNotFound(err) { + o.SetLabels(current) + ctrl.LoggerFrom(ctx).Error(err, "label managed object", "kind", fmt.Sprintf("%T", o), "name", o.GetName()) + } } func (r *Reconciler) reconcileManagedZone(ctx context.Context, c *dnsv1alpha1.DNSResolverContext) (bool, error) { @@ -836,9 +849,7 @@ func (r *Reconciler) reconcileManagedZone(ctx context.Context, c *dnsv1alpha1.DN if err := r.getOrCreate(ctx, zone); err != nil { return false, err } - if err := r.syncManagedLabels(ctx, c, zone); err != nil { - return false, err - } + r.syncManagedLabels(ctx, c, zone) association := &dnsv1alpha1.DNSZoneAssociation{ObjectMeta: metav1.ObjectMeta{Name: zoneName + "-context", Namespace: c.Namespace, Labels: managedObjectLabels(c)}, Spec: dnsv1alpha1.DNSZoneAssociationSpec{DNSZoneRef: dnsv1alpha1.DNSNameReference{Name: zone.Name}, ResolverContextRef: dnsv1alpha1.DNSNameReference{Name: c.Name}}} if r.Scheme != nil { if err := controllerutil.SetControllerReference(c, association, r.Scheme, controllerutil.WithBlockOwnerDeletion(false)); err != nil { @@ -848,8 +859,9 @@ func (r *Reconciler) reconcileManagedZone(ctx context.Context, c *dnsv1alpha1.DN if err := r.getOrCreate(ctx, association); err != nil { return false, err } - if err := r.syncManagedLabels(ctx, c, association); err != nil { - return false, err + // The association webhook needs a live zone, so leave it until there is one. + if zone.DeletionTimestamp.IsZero() { + r.syncManagedLabels(ctx, c, association) } accepted := apimeta.FindStatusCondition(association.Status.Conditions, dnsValueAccepted) refs, err := r.readReferences(ctx, association) diff --git a/internal/internaldns/controlplane/managed_domain_test.go b/internal/internaldns/controlplane/managed_domain_test.go index fca688c5..6d53de83 100644 --- a/internal/internaldns/controlplane/managed_domain_test.go +++ b/internal/internaldns/controlplane/managed_domain_test.go @@ -389,7 +389,8 @@ func TestManagedZoneCarriesContextLabels(t *testing.T) { if err := c.Get(ctx, client.ObjectKeyFromObject(resolver), resolver); err != nil { t.Fatal(err) } - resolver.Labels = map[string]string{"example.com/network-name": "vpc-a", managedObjectLabel: "false"} + resolver.Labels = map[string]string{"example.com/network-name": "vpc-a", "app.kubernetes.io/instance": "gitops", managedObjectLabel: "false"} + resolver.Annotations = map[string]string{managedLabelsAnnotation: "example.com/network-name, " + managedObjectLabel} if err := c.Update(ctx, resolver); err != nil { t.Fatal(err) } @@ -397,4 +398,12 @@ func TestManagedZoneCarriesContextLabels(t *testing.T) { t.Fatal(err) } check(map[string]string{"example.com/network-name": "vpc-a", managedObjectLabel: "true"}) + // Labels the context does not list are never copied. + var zone dnsv1alpha1.DNSZone + if err := c.Get(ctx, zoneKey, &zone); err != nil { + t.Fatal(err) + } + if _, ok := zone.Labels["app.kubernetes.io/instance"]; ok { + t.Fatalf("unlisted label copied: %v", zone.Labels) + } } From f6284dda020fa697e9d6c4e156502ba3a4d09a62 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 3 Nov 2026 11:38:48 -0600 Subject: [PATCH 3/5] feat(private-dns): mark managed zones with app.kubernetes.io/managed-by Managed zones and associations also carry the Kubernetes well-known managed-by label, valued dns.networking.miloapis.com. --- docs/architecture/private-dns/README.md | 3 ++- internal/internaldns/controlplane/context_reconciler.go | 7 ++++++- internal/internaldns/controlplane/managed_domain_test.go | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/architecture/private-dns/README.md b/docs/architecture/private-dns/README.md index a6089fc4..13d066f3 100644 --- a/docs/architecture/private-dns/README.md +++ b/docs/architecture/private-dns/README.md @@ -524,7 +524,8 @@ independent zones with the same apex. Static records have no health lease. The context directly owns its managed zone and association. DNS allocates the requested zone and exposes its reference and suffix in context status. Product publishers use these outputs. The managed zone and association carry the label -`dns.networking.miloapis.com/managed: "true"`. They also get a copy of the +`dns.networking.miloapis.com/managed: "true"` and the well-known +`app.kubernetes.io/managed-by: dns.networking.miloapis.com`. They also get a copy of the context labels named, comma separated, in the context's `dns.networking.miloapis.com/managed-object-labels` annotation, so whoever creates the context can tag what the zone is for, such as the network it diff --git a/internal/internaldns/controlplane/context_reconciler.go b/internal/internaldns/controlplane/context_reconciler.go index 459e0da5..666d8a82 100644 --- a/internal/internaldns/controlplane/context_reconciler.go +++ b/internal/internaldns/controlplane/context_reconciler.go @@ -779,6 +779,9 @@ func (r *Reconciler) withdrawManagedZone(ctx context.Context, c *dnsv1alpha1.DNS const ( managedObjectLabel = "dns.networking.miloapis.com/managed" + // managedByLabel is the Kubernetes well-known label naming the manager. + managedByLabel = "app.kubernetes.io/managed-by" + managedByValue = "dns.networking.miloapis.com" // managedLabelsAnnotation lists, comma separated, the context labels to copy // onto the zone and association the context manages. Only listed labels // are copied, so labels used to track the context, such as GitOps ones, @@ -788,7 +791,8 @@ const ( // managedObjectLabels labels the zone and association a context manages with // the context labels it lists in managedLabelsAnnotation, so whoever creates -// the context can say what the zone is for, and marks them as managed. +// the context can say what the zone is for, and marks them as managed, with +// the well-known managed-by label too. func managedObjectLabels(c *dnsv1alpha1.DNSResolverContext) map[string]string { l := map[string]string{} for _, key := range strings.Split(c.Annotations[managedLabelsAnnotation], ",") { @@ -798,6 +802,7 @@ func managedObjectLabels(c *dnsv1alpha1.DNSResolverContext) map[string]string { } } l[managedObjectLabel] = "true" + l[managedByLabel] = managedByValue return l } diff --git a/internal/internaldns/controlplane/managed_domain_test.go b/internal/internaldns/controlplane/managed_domain_test.go index 6d53de83..40a71f50 100644 --- a/internal/internaldns/controlplane/managed_domain_test.go +++ b/internal/internaldns/controlplane/managed_domain_test.go @@ -385,7 +385,7 @@ func TestManagedZoneCarriesContextLabels(t *testing.T) { if err := r.reconcileResolverContexts(ctx, "project", time.Now()); err != nil { t.Fatal(err) } - check(map[string]string{managedObjectLabel: "true"}) + check(map[string]string{managedObjectLabel: "true", managedByLabel: managedByValue}) if err := c.Get(ctx, client.ObjectKeyFromObject(resolver), resolver); err != nil { t.Fatal(err) } @@ -397,7 +397,7 @@ func TestManagedZoneCarriesContextLabels(t *testing.T) { if err := r.reconcileResolverContexts(ctx, "project", time.Now()); err != nil { t.Fatal(err) } - check(map[string]string{"example.com/network-name": "vpc-a", managedObjectLabel: "true"}) + check(map[string]string{"example.com/network-name": "vpc-a", managedObjectLabel: "true", managedByLabel: managedByValue}) // Labels the context does not list are never copied. var zone dnsv1alpha1.DNSZone if err := c.Get(ctx, zoneKey, &zone); err != nil { From e01c17c35fb9d84006c0edb97d187ff79dd9d4cc Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 3 Nov 2026 11:44:01 -0600 Subject: [PATCH 4/5] feat(private-dns): copy the context description to its managed zone The context's kubernetes.io/description annotation is copied to the managed zone and association, so clients show what the zone is for. --- docs/architecture/private-dns/README.md | 4 +- .../controlplane/context_reconciler.go | 64 +++++++++++++------ .../controlplane/managed_domain_test.go | 5 +- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/docs/architecture/private-dns/README.md b/docs/architecture/private-dns/README.md index 13d066f3..41ff66fc 100644 --- a/docs/architecture/private-dns/README.md +++ b/docs/architecture/private-dns/README.md @@ -529,7 +529,9 @@ publishers use these outputs. The managed zone and association carry the label context labels named, comma separated, in the context's `dns.networking.miloapis.com/managed-object-labels` annotation, so whoever creates the context can tag what the zone is for, such as the network it -serves. Copied labels are added or corrected, never removed. A naming policy +serves. The context's `kubernetes.io/description` annotation is copied too, so +clients can show what the zone is for. Copied labels and the description are +added or corrected, never removed. A naming policy adds custom names without a zone choice on each Compute or Connect resource. ```yaml diff --git a/internal/internaldns/controlplane/context_reconciler.go b/internal/internaldns/controlplane/context_reconciler.go index 666d8a82..ac3c6778 100644 --- a/internal/internaldns/controlplane/context_reconciler.go +++ b/internal/internaldns/controlplane/context_reconciler.go @@ -806,38 +806,64 @@ func managedObjectLabels(c *dnsv1alpha1.DNSResolverContext) map[string]string { return l } -// syncManagedLabels adds or corrects the copied labels on a live object the -// context controls, for objects created before the context listed them. Labels -// are only added, never removed. Labels are informational, so a failed write -// is logged and never stops the pass. -func (r *Reconciler) syncManagedLabels(ctx context.Context, c *dnsv1alpha1.DNSResolverContext, o client.Object) { +// managedObjectAnnotations carries the context's description, if it has one. +func managedObjectAnnotations(c *dnsv1alpha1.DNSResolverContext) map[string]string { + if description, ok := c.Annotations[descriptionAnnotation]; ok { + return map[string]string{descriptionAnnotation: description} + } + return nil +} + +// descriptionAnnotation is the well-known annotation clients show as an +// object's description. A context's description is copied to its managed zone +// and association, so they say what they are for. +const descriptionAnnotation = "kubernetes.io/description" + +// syncManagedMetadata adds or corrects the copied labels and description on a +// live object the context controls, for objects created before the context +// had them. Nothing is ever removed. The metadata is informational, so a +// failed write is logged and never stops the pass. +func (r *Reconciler) syncManagedMetadata(ctx context.Context, c *dnsv1alpha1.DNSResolverContext, o client.Object) { if !metav1.IsControlledBy(o, c) || !o.GetDeletionTimestamp().IsZero() { return } - want := managedObjectLabels(c) - current := o.GetLabels() + labels, annotations := o.GetLabels(), o.GetAnnotations() + wantLabels := managedObjectLabels(c) stale := false - for k, v := range want { - if got, ok := current[k]; !ok || got != v { + for k, v := range wantLabels { + if got, ok := labels[k]; !ok || got != v { stale = true break } } + description, hasDescription := c.Annotations[descriptionAnnotation] + if hasDescription && annotations[descriptionAnnotation] != description { + stale = true + } if !stale { return } base := o.DeepCopyObject().(client.Object) - merged := maps.Clone(current) - if merged == nil { - merged = map[string]string{} + mergedLabels := maps.Clone(labels) + if mergedLabels == nil { + mergedLabels = map[string]string{} + } + maps.Copy(mergedLabels, wantLabels) + o.SetLabels(mergedLabels) + if hasDescription { + mergedAnnotations := maps.Clone(annotations) + if mergedAnnotations == nil { + mergedAnnotations = map[string]string{} + } + mergedAnnotations[descriptionAnnotation] = description + o.SetAnnotations(mergedAnnotations) } - maps.Copy(merged, want) - o.SetLabels(merged) if listed, _ := ctx.Value(listedManagedObjectsKey{}).(*listedManagedObjects); listed != nil { listed.changed = true } if err := r.Patch(ctx, o, client.MergeFrom(base)); err != nil && !apierrors.IsNotFound(err) { - o.SetLabels(current) + o.SetLabels(labels) + o.SetAnnotations(annotations) ctrl.LoggerFrom(ctx).Error(err, "label managed object", "kind", fmt.Sprintf("%T", o), "name", o.GetName()) } } @@ -845,7 +871,7 @@ func (r *Reconciler) syncManagedLabels(ctx context.Context, c *dnsv1alpha1.DNSRe func (r *Reconciler) reconcileManagedZone(ctx context.Context, c *dnsv1alpha1.DNSResolverContext) (bool, error) { suffix := canonicalName(c.Spec.ManagedNamespace.DomainSuffix) zoneName := managedZoneName(c) - zone := &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: c.Namespace, Labels: managedObjectLabels(c)}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: suffix, DNSZoneClassName: r.Options.PrivateZoneClassName, Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}} + zone := &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: c.Namespace, Labels: managedObjectLabels(c), Annotations: managedObjectAnnotations(c)}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: suffix, DNSZoneClassName: r.Options.PrivateZoneClassName, Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}} if r.Scheme != nil { if err := controllerutil.SetControllerReference(c, zone, r.Scheme, controllerutil.WithBlockOwnerDeletion(false)); err != nil { return false, err @@ -854,8 +880,8 @@ func (r *Reconciler) reconcileManagedZone(ctx context.Context, c *dnsv1alpha1.DN if err := r.getOrCreate(ctx, zone); err != nil { return false, err } - r.syncManagedLabels(ctx, c, zone) - association := &dnsv1alpha1.DNSZoneAssociation{ObjectMeta: metav1.ObjectMeta{Name: zoneName + "-context", Namespace: c.Namespace, Labels: managedObjectLabels(c)}, Spec: dnsv1alpha1.DNSZoneAssociationSpec{DNSZoneRef: dnsv1alpha1.DNSNameReference{Name: zone.Name}, ResolverContextRef: dnsv1alpha1.DNSNameReference{Name: c.Name}}} + r.syncManagedMetadata(ctx, c, zone) + association := &dnsv1alpha1.DNSZoneAssociation{ObjectMeta: metav1.ObjectMeta{Name: zoneName + "-context", Namespace: c.Namespace, Labels: managedObjectLabels(c), Annotations: managedObjectAnnotations(c)}, Spec: dnsv1alpha1.DNSZoneAssociationSpec{DNSZoneRef: dnsv1alpha1.DNSNameReference{Name: zone.Name}, ResolverContextRef: dnsv1alpha1.DNSNameReference{Name: c.Name}}} if r.Scheme != nil { if err := controllerutil.SetControllerReference(c, association, r.Scheme, controllerutil.WithBlockOwnerDeletion(false)); err != nil { return false, err @@ -866,7 +892,7 @@ func (r *Reconciler) reconcileManagedZone(ctx context.Context, c *dnsv1alpha1.DN } // The association webhook needs a live zone, so leave it until there is one. if zone.DeletionTimestamp.IsZero() { - r.syncManagedLabels(ctx, c, association) + r.syncManagedMetadata(ctx, c, association) } accepted := apimeta.FindStatusCondition(association.Status.Conditions, dnsValueAccepted) refs, err := r.readReferences(ctx, association) diff --git a/internal/internaldns/controlplane/managed_domain_test.go b/internal/internaldns/controlplane/managed_domain_test.go index 40a71f50..7998020f 100644 --- a/internal/internaldns/controlplane/managed_domain_test.go +++ b/internal/internaldns/controlplane/managed_domain_test.go @@ -390,7 +390,7 @@ func TestManagedZoneCarriesContextLabels(t *testing.T) { t.Fatal(err) } resolver.Labels = map[string]string{"example.com/network-name": "vpc-a", "app.kubernetes.io/instance": "gitops", managedObjectLabel: "false"} - resolver.Annotations = map[string]string{managedLabelsAnnotation: "example.com/network-name, " + managedObjectLabel} + resolver.Annotations = map[string]string{managedLabelsAnnotation: "example.com/network-name, " + managedObjectLabel, descriptionAnnotation: "Private DNS for vpc-a"} if err := c.Update(ctx, resolver); err != nil { t.Fatal(err) } @@ -406,4 +406,7 @@ func TestManagedZoneCarriesContextLabels(t *testing.T) { if _, ok := zone.Labels["app.kubernetes.io/instance"]; ok { t.Fatalf("unlisted label copied: %v", zone.Labels) } + if got := zone.Annotations[descriptionAnnotation]; got != "Private DNS for vpc-a" { + t.Fatalf("zone description = %q", got) + } } From a8a2541261fd1cc15b7309b31b1b99b2f6c3b4c5 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 3 Nov 2026 12:51:04 -0600 Subject: [PATCH 5/5] chore(private-dns): satisfy lint in managed zone metadata sync --- .../internaldns/controlplane/context_reconciler.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/internaldns/controlplane/context_reconciler.go b/internal/internaldns/controlplane/context_reconciler.go index ac3c6778..466083f1 100644 --- a/internal/internaldns/controlplane/context_reconciler.go +++ b/internal/internaldns/controlplane/context_reconciler.go @@ -779,6 +779,7 @@ func (r *Reconciler) withdrawManagedZone(ctx context.Context, c *dnsv1alpha1.DNS const ( managedObjectLabel = "dns.networking.miloapis.com/managed" + managedObjectValue = "true" // managedByLabel is the Kubernetes well-known label naming the manager. managedByLabel = "app.kubernetes.io/managed-by" managedByValue = "dns.networking.miloapis.com" @@ -801,7 +802,7 @@ func managedObjectLabels(c *dnsv1alpha1.DNSResolverContext) map[string]string { l[key] = v } } - l[managedObjectLabel] = "true" + l[managedObjectLabel] = managedObjectValue l[managedByLabel] = managedByValue return l } @@ -827,11 +828,11 @@ func (r *Reconciler) syncManagedMetadata(ctx context.Context, c *dnsv1alpha1.DNS if !metav1.IsControlledBy(o, c) || !o.GetDeletionTimestamp().IsZero() { return } - labels, annotations := o.GetLabels(), o.GetAnnotations() + objLabels, annotations := o.GetLabels(), o.GetAnnotations() wantLabels := managedObjectLabels(c) stale := false for k, v := range wantLabels { - if got, ok := labels[k]; !ok || got != v { + if got, ok := objLabels[k]; !ok || got != v { stale = true break } @@ -844,7 +845,7 @@ func (r *Reconciler) syncManagedMetadata(ctx context.Context, c *dnsv1alpha1.DNS return } base := o.DeepCopyObject().(client.Object) - mergedLabels := maps.Clone(labels) + mergedLabels := maps.Clone(objLabels) if mergedLabels == nil { mergedLabels = map[string]string{} } @@ -862,7 +863,7 @@ func (r *Reconciler) syncManagedMetadata(ctx context.Context, c *dnsv1alpha1.DNS listed.changed = true } if err := r.Patch(ctx, o, client.MergeFrom(base)); err != nil && !apierrors.IsNotFound(err) { - o.SetLabels(labels) + o.SetLabels(objLabels) o.SetAnnotations(annotations) ctrl.LoggerFrom(ctx).Error(err, "label managed object", "kind", fmt.Sprintf("%T", o), "name", o.GetName()) }