Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/architecture/private-dns/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,15 @@ 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 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
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
Expand Down
102 changes: 100 additions & 2 deletions internal/internaldns/controlplane/context_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"net/netip"
"reflect"
"sort"
Expand Down Expand Up @@ -776,10 +777,102 @@ func (r *Reconciler) withdrawManagedZone(ctx context.Context, c *dnsv1alpha1.DNS
return nil
}

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"
// 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 labels it lists in managedLabelsAnnotation, so whoever creates
// 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], ",") {
key = strings.TrimSpace(key)
if v, ok := c.Labels[key]; ok && key != "" {
l[key] = v
}
}
l[managedObjectLabel] = managedObjectValue
l[managedByLabel] = managedByValue
return l
}

// 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
}
objLabels, annotations := o.GetLabels(), o.GetAnnotations()
wantLabels := managedObjectLabels(c)
stale := false
for k, v := range wantLabels {
if got, ok := objLabels[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)
mergedLabels := maps.Clone(objLabels)
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)
}
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(objLabels)
o.SetAnnotations(annotations)
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) {
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), 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
Expand All @@ -788,7 +881,8 @@ 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}}}
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
Expand All @@ -797,6 +891,10 @@ func (r *Reconciler) reconcileManagedZone(ctx context.Context, c *dnsv1alpha1.DN
if err := r.getOrCreate(ctx, 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.syncManagedMetadata(ctx, c, association)
}
accepted := apimeta.FindStatusCondition(association.Status.Conditions, dnsValueAccepted)
refs, err := r.readReferences(ctx, association)
if err != nil {
Expand Down
65 changes: 65 additions & 0 deletions internal/internaldns/controlplane/managed_domain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,68 @@ 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", managedByLabel: managedByValue})
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", "app.kubernetes.io/instance": "gitops", managedObjectLabel: "false"}
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)
}
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", managedByLabel: managedByValue})
// 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)
}
if got := zone.Annotations[descriptionAnnotation]; got != "Private DNS for vpc-a" {
t.Fatalf("zone description = %q", got)
}
}
Loading