From fe0b199961fa3a2671e33d48272e85fa08640221 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 12:09:25 -0500 Subject: [PATCH 1/4] Allocate private DNS domains requested by consumer services --- api/v1alpha1/internaldns_project_types.go | 7 + ...king.miloapis.com_dnsresolvercontexts.yaml | 16 +++ .../internal-dns/control-plane.example.yaml | 1 - config/internal-dns/examples/context.yaml | 2 + .../access_recovery_envtest_test.go | 24 ++++ .../controlplane/context_reconciler.go | 20 ++- .../controlplane/managed_domain_test.go | 134 ++++++++++++++++++ internal/internaldns/controlplane/naming.go | 11 +- .../internaldns/controlplane/naming_test.go | 2 +- .../internaldns/controlplane/reconciler.go | 1 - internal/internaldns/runtime/config.go | 1 - internal/internaldns/runtime/run.go | 4 +- test/internaldns/kubernetes/qualify/main.go | 2 +- test/internaldns/kubernetes/render/main.go | 1 - 14 files changed, 201 insertions(+), 25 deletions(-) create mode 100644 internal/internaldns/controlplane/managed_domain_test.go diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index 04bbd19c..97685c5b 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -44,6 +44,7 @@ type DNSZoneAssociationStatus struct { // DNSResolverContext is the DNS-owned serving and namespace boundary created by // a trusted network integration. consumerID is opaque to DNS and immutable. // +kubebuilder:validation:XValidation:rule="oldSelf.consumerID == self.consumerID",message="consumerID is immutable" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.managedNamespace) || !has(oldSelf.managedNamespace.domainSuffix) || (has(self.managedNamespace) && has(self.managedNamespace.domainSuffix) && self.managedNamespace.domainSuffix == oldSelf.managedNamespace.domainSuffix)",message="managed namespace domainSuffix is immutable" type DNSResolverContextSpec struct { // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=512 @@ -51,8 +52,14 @@ type DNSResolverContextSpec struct { ManagedNamespace DNSResolverContextManagedNamespace `json:"managedNamespace,omitempty"` } +// +kubebuilder:validation:XValidation:rule="!has(self.enabled) || !self.enabled || (has(self.domainSuffix) && self.domainSuffix.size() > 0)",message="domainSuffix is required when the managed namespace is enabled" type DNSResolverContextManagedNamespace struct { Enabled bool `json:"enabled,omitempty"` + // DomainSuffix is the zone apex requested by the trusted consumer integration. + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$` + DomainSuffix string `json:"domainSuffix,omitempty"` } type DNSResolverContextManagedNamespaceStatus struct { diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml index 0adba855..15b51f51 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml @@ -47,15 +47,31 @@ spec: type: string managedNamespace: properties: + domainSuffix: + description: DomainSuffix is the zone apex requested by the trusted + consumer integration. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$ + type: string enabled: type: boolean type: object + x-kubernetes-validations: + - message: domainSuffix is required when the managed namespace is + enabled + rule: '!has(self.enabled) || !self.enabled || (has(self.domainSuffix) + && self.domainSuffix.size() > 0)' required: - consumerID type: object x-kubernetes-validations: - message: consumerID is immutable rule: oldSelf.consumerID == self.consumerID + - message: managed namespace domainSuffix is immutable + rule: '!has(oldSelf.managedNamespace) || !has(oldSelf.managedNamespace.domainSuffix) + || (has(self.managedNamespace) && has(self.managedNamespace.domainSuffix) + && self.managedNamespace.domainSuffix == oldSelf.managedNamespace.domainSuffix)' status: properties: accessWriterEpoch: diff --git a/config/internal-dns/control-plane.example.yaml b/config/internal-dns/control-plane.example.yaml index 75e81ed3..9f01b945 100644 --- a/config/internal-dns/control-plane.example.yaml +++ b/config/internal-dns/control-plane.example.yaml @@ -34,7 +34,6 @@ data: ], "consumerPrefix": "fd70:100::/48", "clusterPrefix": "fd70:200::/48", - "managedDomainSuffix": "internal.example.net", "privateZoneClassName": "private-bind", "ownershipLeaseSeconds": 30, "shards": [ diff --git a/config/internal-dns/examples/context.yaml b/config/internal-dns/examples/context.yaml index 337d38c2..56364b7d 100644 --- a/config/internal-dns/examples/context.yaml +++ b/config/internal-dns/examples/context.yaml @@ -9,3 +9,5 @@ spec: # Products discover this namespace instead of asking users to select a zone. managedNamespace: enabled: true + # Network services selects the default domain for this consumer. + domainSuffix: datum.internal diff --git a/internal/internaldns/controlplane/access_recovery_envtest_test.go b/internal/internaldns/controlplane/access_recovery_envtest_test.go index dff35d3b..3f4c0d3a 100644 --- a/internal/internaldns/controlplane/access_recovery_envtest_test.go +++ b/internal/internaldns/controlplane/access_recovery_envtest_test.go @@ -48,6 +48,30 @@ func TestAccessRecoveryPreservesSourceFencesWithCEL(t *testing.T) { for _, ns := range []string{"project", "platform"} { requireAccessTestSuccess(t, cl.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}})) } + t.Run("requested managed domain admission", func(t *testing.T) { + for name, domain := range map[string]string{"missing": "", "invalid": "bad..internal", "wildcard": "*.internal"} { + resolver := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "project"}, Spec: dnsv1alpha1.DNSResolverContextSpec{ConsumerID: name, ManagedNamespace: dnsv1alpha1.DNSResolverContextManagedNamespace{Enabled: true, DomainSuffix: domain}}} + if err := cl.Create(ctx, resolver); !apierrors.IsInvalid(err) { + t.Fatalf("invalid managed domain %q was not rejected: %v", domain, err) + } + } + resolver := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "custom-domain", Namespace: "project"}, Spec: dnsv1alpha1.DNSResolverContextSpec{ConsumerID: "custom", ManagedNamespace: dnsv1alpha1.DNSResolverContextManagedNamespace{Enabled: true, DomainSuffix: "private.example.net"}}} + requireAccessTestSuccess(t, cl.Create(ctx, resolver)) + changed := resolver.DeepCopy() + changed.Spec.ManagedNamespace.DomainSuffix = "different.internal" + if err := cl.Update(ctx, changed); !apierrors.IsInvalid(err) { + t.Fatalf("domain mutation was not rejected: %v", err) + } + raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resolver) + requireAccessTestSuccess(t, err) + unstructured.RemoveNestedField(raw, "spec", "managedNamespace") + removed := &unstructured.Unstructured{Object: raw} + removed.SetAPIVersion(dnsv1alpha1.GroupVersion.String()) + removed.SetKind("DNSResolverContext") + if err := cl.Update(ctx, removed); !apierrors.IsInvalid(err) { + t.Fatalf("domain removal was not rejected: %v", err) + } + }) now := time.Now().UTC().Truncate(time.Second) resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "context", Namespace: "project"}, Spec: dnsv1alpha1.DNSResolverContextSpec{ConsumerID: "opaque-consumer"}} readyContext := func(c *dnsv1alpha1.DNSResolverContext) { diff --git a/internal/internaldns/controlplane/context_reconciler.go b/internal/internaldns/controlplane/context_reconciler.go index 24de7a80..68b9c06b 100644 --- a/internal/internaldns/controlplane/context_reconciler.go +++ b/internal/internaldns/controlplane/context_reconciler.go @@ -20,6 +20,7 @@ import ( apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) @@ -114,10 +115,9 @@ func (r *Reconciler) reconcileManagedNamespaces(ctx context.Context, ns string, } continue } - consumerToken := model.OpaqueToken(string(m.Spec.VPCRef.UID)) lifetimeToken := model.OpaqueToken(m.Labels["internal-dns.miloapis.com/context-uid"]) - suffix := canonicalName(consumerToken + "." + m.Spec.DomainSuffix) + suffix := canonicalName(m.Spec.DomainSuffix) zoneName := "managed-" + lifetimeToken[:12] zone := &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: ns, Labels: map[string]string{"dns.networking.miloapis.com/managed": "true"}}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: suffix, DNSZoneClassName: m.Spec.DNSZoneClassName, Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}} if r.Scheme != nil { @@ -155,7 +155,7 @@ func (r *Reconciler) reconcileManagedNamespaces(ctx context.Context, ns string, } base := m.DeepCopy() accepted := apimeta.FindStatusCondition(assoc.Status.Conditions, dnsValueAccepted) - ready := accepted != nil && accepted.Status == metav1.ConditionTrue && assoc.Status.ResolvedDNSZoneRef.UID == zone.UID + ready := zone.Spec.DomainName == suffix && metav1.IsControlledBy(zone, m) && accepted != nil && accepted.Status == metav1.ConditionTrue && assoc.Status.ResolvedDNSZoneRef.UID == zone.UID if ready { m.Status.DNSZoneRef = dnsv1alpha1.DNSObjectReference{Name: zone.Name, UID: zone.UID} m.Status.AssociationRef = dnsv1alpha1.DNSObjectReference{Name: assoc.Name, UID: assoc.UID} @@ -190,6 +190,8 @@ func (r *Reconciler) reconcileResolverContexts(ctx context.Context, ns string, n consumerOwners[c.Spec.ConsumerID] = c.UID } valid = valid && !duplicate + invalidDomain := c.Spec.ManagedNamespace.Enabled && len(validation.IsDNS1123Subdomain(c.Spec.ManagedNamespace.DomainSuffix)) > 0 + valid = valid && !invalidDomain previousReady := apimeta.FindStatusCondition(c.Status.Conditions, "Ready") if !valid && c.Status.AccessWriterEpoch > 0 && previousReady != nil && previousReady.Status == metav1.ConditionTrue { c.Status.AccessWriterEpoch++ @@ -204,9 +206,10 @@ func (r *Reconciler) reconcileResolverContexts(ctx context.Context, ns string, n } } ready := valid && !c.Spec.ManagedNamespace.Enabled + c.Status.ManagedNamespace = dnsv1alpha1.DNSResolverContextManagedNamespaceStatus{} if c.Spec.ManagedNamespace.Enabled && valid { name := "context-" + model.OpaqueToken(string(c.UID))[:20] - m := &dnsv1alpha1.DNSManagedNamespace{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns, Labels: map[string]string{"internal-dns.miloapis.com/platform-owned": "true", "internal-dns.miloapis.com/context-uid": string(c.UID)}}, Spec: dnsv1alpha1.DNSManagedNamespaceSpec{ProjectUID: r.Options.ProjectUID, VPCRef: dnsv1alpha1.DNSObjectReference{Name: c.Name, UID: types.UID(c.Spec.ConsumerID)}, DomainSuffix: r.Options.ManagedDomainSuffix, DNSZoneClassName: r.Options.PrivateZoneClassName}} + m := &dnsv1alpha1.DNSManagedNamespace{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns, Labels: map[string]string{"internal-dns.miloapis.com/platform-owned": "true", "internal-dns.miloapis.com/context-uid": string(c.UID)}}, Spec: dnsv1alpha1.DNSManagedNamespaceSpec{ProjectUID: r.Options.ProjectUID, VPCRef: dnsv1alpha1.DNSObjectReference{Name: c.Name, UID: types.UID(c.Spec.ConsumerID)}, DomainSuffix: c.Spec.ManagedNamespace.DomainSuffix, DNSZoneClassName: r.Options.PrivateZoneClassName}} if r.Scheme != nil { if err := controllerutil.SetControllerReference(c, m, r.Scheme); err != nil { return err @@ -218,9 +221,9 @@ func (r *Reconciler) reconcileResolverContexts(ctx context.Context, ns string, n if err := r.Get(ctx, client.ObjectKeyFromObject(m), m); err != nil { return err } - c.Status.ManagedNamespace.DNSZoneRef = m.Status.DNSZoneRef - c.Status.ManagedNamespace.Suffix = m.Status.CanonicalSuffix - if condition := apimeta.FindStatusCondition(m.Status.Conditions, dnsValueAccepted); condition != nil && condition.Status == metav1.ConditionTrue && m.Status.DNSZoneRef.UID != "" { + if condition := apimeta.FindStatusCondition(m.Status.Conditions, dnsValueAccepted); condition != nil && condition.Status == metav1.ConditionTrue && condition.ObservedGeneration == m.Generation && m.Status.DNSZoneRef.UID != "" && metav1.IsControlledBy(m, c) && m.Spec.DomainSuffix == c.Spec.ManagedNamespace.DomainSuffix && m.Status.CanonicalSuffix == c.Spec.ManagedNamespace.DomainSuffix { + c.Status.ManagedNamespace.DNSZoneRef = m.Status.DNSZoneRef + c.Status.ManagedNamespace.Suffix = m.Status.CanonicalSuffix ready = true } } @@ -230,6 +233,9 @@ func (r *Reconciler) reconcileResolverContexts(ctx context.Context, ns string, n if duplicate { reason, msg = "DuplicateConsumerID", "another live resolver context owns this consumerID" } + if invalidDomain { + reason, msg = "InvalidManagedDomain", "an enabled managed namespace requires a valid domainSuffix" + } } if valid && !ready { status, reason, msg = metav1.ConditionFalse, "Preparing", "managed namespace is not ready" diff --git a/internal/internaldns/controlplane/managed_domain_test.go b/internal/internaldns/controlplane/managed_domain_test.go new file mode 100644 index 00000000..b2c56402 --- /dev/null +++ b/internal/internaldns/controlplane/managed_domain_test.go @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package controlplane + +import ( + "context" + "testing" + "time" + + dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" +) + +func TestManagedDomainIsSharedWithSeparateContextZones(t *testing.T) { + s := runtime.NewScheme() + if err := dnsv1alpha1.AddToScheme(s); err != nil { + t.Fatal(err) + } + domains := map[string]string{"vpc-a": "datum.internal", "vpc-b": "datum.internal", "custom": "private.example.net"} + objects := make([]client.Object, 0, len(domains)) + for name, domain := range domains { + objects = append(objects, &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "project", UID: types.UID(name + "-context"), Generation: 1}, Spec: dnsv1alpha1.DNSResolverContextSpec{ConsumerID: "project-uid/" + name, ManagedNamespace: dnsv1alpha1.DNSResolverContextManagedNamespace{Enabled: true, DomainSuffix: domain}}}) + } + c := fake.NewClientBuilder().WithScheme(s).WithObjects(objects...).WithStatusSubresource(&dnsv1alpha1.DNSResolverContext{}, &dnsv1alpha1.DNSManagedNamespace{}, &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"}} + r.Options.defaults() + ctx := context.Background() + now := time.Now() + if err := r.reconcileResolverContexts(ctx, "project", now); err != nil { + t.Fatal(err) + } + if err := r.reconcileManagedNamespaces(ctx, "project", now); err != nil { + t.Fatal(err) + } + var zones dnsv1alpha1.DNSZoneList + if err := c.List(ctx, &zones); err != nil { + t.Fatal(err) + } + if len(zones.Items) != len(domains) || zones.Items[0].UID == zones.Items[1].UID { + t.Fatalf("expected separate zones: %#v", zones.Items) + } + var associations dnsv1alpha1.DNSZoneAssociationList + if err := c.List(ctx, &associations); err != nil { + t.Fatal(err) + } + for i := range associations.Items { + a := &associations.Items[i] + a.Status.ResolvedDNSZoneRef = a.Spec.DNSZoneRef + setCondition(&a.Status.Conditions, "Accepted", metav1.ConditionTrue, "Ready", "", a.Generation, now) + if err := c.Status().Update(ctx, a); err != nil { + t.Fatal(err) + } + } + if err := r.reconcileManagedNamespaces(ctx, "project", now); err != nil { + t.Fatal(err) + } + if err := r.reconcileResolverContexts(ctx, "project", now); err != nil { + t.Fatal(err) + } + var contexts dnsv1alpha1.DNSResolverContextList + if err := c.List(ctx, &contexts); err != nil { + t.Fatal(err) + } + for _, resolver := range contexts.Items { + if resolver.Status.ManagedNamespace.Suffix != domains[resolver.Name] || resolver.Status.ManagedNamespace.DNSZoneRef.UID == "" { + t.Fatalf("unallocated context: %#v", resolver.Status) + } + var zone dnsv1alpha1.DNSZone + if err := c.Get(ctx, client.ObjectKey{Namespace: "project", Name: resolver.Status.ManagedNamespace.DNSZoneRef.Name}, &zone); err != nil { + t.Fatal(err) + } + if zone.Spec.DomainName != domains[resolver.Name] { + t.Fatalf("requested %q, allocated %q", domains[resolver.Name], zone.Spec.DomainName) + } + } + if contexts.Items[0].Status.ManagedNamespace.DNSZoneRef.UID == contexts.Items[1].Status.ManagedNamespace.DNSZoneRef.UID { + t.Fatal("contexts share a zone lifetime") + } + zone := &zones.Items[0] + zone.Spec.DomainName = "different.internal" + if err := c.Update(ctx, zone); err != nil { + t.Fatal(err) + } + if err := r.reconcileManagedNamespaces(ctx, "project", now); err != nil { + t.Fatal(err) + } + if err := r.reconcileResolverContexts(ctx, "project", now); err != nil { + t.Fatal(err) + } + if err := c.List(ctx, &contexts); err != nil { + t.Fatal(err) + } + for _, resolver := range contexts.Items { + if resolver.Status.ManagedNamespace.DNSZoneRef.UID == zone.UID { + t.Fatal("a mismatched zone domain remained allocated") + } + } +} + +func TestManagedNamespaceRequiresRequestedDomain(t *testing.T) { + for _, domain := range []string{"", "bad..internal", "*.internal"} { + t.Run(domain, func(t *testing.T) { + s := runtime.NewScheme() + if err := dnsv1alpha1.AddToScheme(s); err != nil { + t.Fatal(err) + } + resolver := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "consumer", Namespace: "project", UID: "context-uid", Generation: 1}, Spec: dnsv1alpha1.DNSResolverContextSpec{ConsumerID: "opaque-consumer", ManagedNamespace: dnsv1alpha1.DNSResolverContextManagedNamespace{Enabled: true, DomainSuffix: domain}}} + c := fake.NewClientBuilder().WithScheme(s).WithObjects(resolver).WithStatusSubresource(resolver).Build() + r := &Reconciler{Client: c, Scheme: s} + if err := r.reconcileResolverContexts(context.Background(), "project", time.Now()); err != nil { + t.Fatal(err) + } + if err := c.Get(context.Background(), client.ObjectKeyFromObject(resolver), resolver); err != nil { + t.Fatal(err) + } + if len(resolver.Status.Conditions) != 1 || resolver.Status.Conditions[0].Reason != "InvalidManagedDomain" { + t.Fatalf("invalid domain was accepted: %#v", resolver.Status) + } + var namespaces dnsv1alpha1.DNSManagedNamespaceList + if err := c.List(context.Background(), &namespaces); err != nil || len(namespaces.Items) != 0 { + t.Fatalf("invalid domain allocated a namespace: %#v, %v", namespaces.Items, err) + } + }) + } +} diff --git a/internal/internaldns/controlplane/naming.go b/internal/internaldns/controlplane/naming.go index f5499ef0..52ce3ca7 100644 --- a/internal/internaldns/controlplane/naming.go +++ b/internal/internaldns/controlplane/naming.go @@ -38,8 +38,7 @@ func ResolveNaming(ctx context.Context, c client.Client, namespace string, proje m := &managed.Items[i] accepted := apimeta.FindStatusCondition(m.Status.Conditions, dnsValueAccepted) if m.Spec.ProjectUID == projectUID && m.Spec.VPCRef.UID == vpcUID && accepted != nil && accepted.Status == metav1.ConditionTrue { - prefix := classPrefix(class) - out = append(out, ResolvedName{DNSZoneRef: m.Status.DNSZoneRef, Name: label + "." + prefix, Canonical: true}) + out = append(out, ResolvedName{DNSZoneRef: m.Status.DNSZoneRef, Name: label, Canonical: true}) break } } @@ -71,14 +70,6 @@ func ResolveNaming(ctx context.Context, c client.Client, namespace string, proje return out, nil } -func classPrefix(class dnsv1alpha1.DNSRegistrationClass) string { - switch class { - case dnsv1alpha1.DNSRegistrationClassInstanceIdentity: - return "instances" - default: - return "services" - } -} func dnsLabel(s string) string { s = strings.ToLower(strings.TrimSpace(s)) var b strings.Builder diff --git a/internal/internaldns/controlplane/naming_test.go b/internal/internaldns/controlplane/naming_test.go index 53062cb3..e5992041 100644 --- a/internal/internaldns/controlplane/naming_test.go +++ b/internal/internaldns/controlplane/naming_test.go @@ -22,7 +22,7 @@ func TestResolveNamingReturnsCanonicalThenAuthorizedAliases(t *testing.T) { if err != nil { t.Fatal(err) } - if len(names) != 2 || names[0].Name != "web-01.instances" || !names[0].Canonical || names[1].Name != "web-01.instances" || names[1].Canonical { + if len(names) != 2 || names[0].Name != "web-01" || !names[0].Canonical || names[1].Name != "web-01.instances" || names[1].Canonical { t.Fatalf("unexpected names: %#v", names) } } diff --git a/internal/internaldns/controlplane/reconciler.go b/internal/internaldns/controlplane/reconciler.go index 8c7623f5..05755851 100644 --- a/internal/internaldns/controlplane/reconciler.go +++ b/internal/internaldns/controlplane/reconciler.go @@ -30,7 +30,6 @@ type ReconcilerOptions struct { LeaseDuration time.Duration Now func() time.Time AddressAllocator AddressAllocator - ManagedDomainSuffix string PrivateZoneClassName string ServingRegions []ServingRegion MaxAccessLease time.Duration diff --git a/internal/internaldns/runtime/config.go b/internal/internaldns/runtime/config.go index 44cfd98d..cc561b6a 100644 --- a/internal/internaldns/runtime/config.go +++ b/internal/internaldns/runtime/config.go @@ -73,7 +73,6 @@ type Config struct { RegionalTransports []RegionalTransportConfig `json:"regionalTransports,omitempty"` Admission AdmissionConfig `json:"admission,omitempty"` OwnershipLeaseSeconds int64 `json:"ownershipLeaseSeconds,omitempty"` - ManagedDomainSuffix string `json:"managedDomainSuffix,omitempty"` PrivateZoneClassName string `json:"privateZoneClassName,omitempty"` Agent serving.Config `json:"agent,omitempty"` Watchdog watchdog.Config `json:"watchdog,omitempty"` diff --git a/internal/internaldns/runtime/run.go b/internal/internaldns/runtime/run.go index 194d7818..ae58f04c 100644 --- a/internal/internaldns/runtime/run.go +++ b/internal/internaldns/runtime/run.go @@ -120,8 +120,8 @@ func runControlPlane(ctx context.Context, cfg Config, kubeconfig string) error { ProjectUID: project.ProjectUID, SourceClusterUID: project.SourceClusterUID, ProjectNamespace: project.Namespace, PlatformNamespace: cfg.PlatformNamespace, Region: cfg.Region, Shard: cfg.Shard, Identity: identity, - LeaseDuration: cfg.ownershipLease(), - AddressAllocator: allocator, ManagedDomainSuffix: cfg.ManagedDomainSuffix, + LeaseDuration: cfg.ownershipLease(), + AddressAllocator: allocator, PrivateZoneClassName: cfg.PrivateZoneClassName, ServingRegions: regions, MaxAccessLease: cfg.Admission.maxAccessLease(), }} diff --git a/test/internaldns/kubernetes/qualify/main.go b/test/internaldns/kubernetes/qualify/main.go index e5202f50..e25a673f 100644 --- a/test/internaldns/kubernetes/qualify/main.go +++ b/test/internaldns/kubernetes/qualify/main.go @@ -325,7 +325,7 @@ func (s *suite) permissions(p *project) error { } func (s *suite) prepare(p *project) error { - p.resolverContext = dnsv1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "application-network", Namespace: p.Namespace}, Spec: dnsv1.DNSResolverContextSpec{ConsumerID: "qualification/" + p.Name, ManagedNamespace: dnsv1.DNSResolverContextManagedNamespace{Enabled: true}}} + p.resolverContext = dnsv1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "application-network", Namespace: p.Namespace}, Spec: dnsv1.DNSResolverContextSpec{ConsumerID: "qualification/" + p.Name, ManagedNamespace: dnsv1.DNSResolverContextManagedNamespace{Enabled: true, DomainSuffix: "datum.internal"}}} if err := p.integration.Create(s.ctx, &p.resolverContext); err != nil { return err } diff --git a/test/internaldns/kubernetes/render/main.go b/test/internaldns/kubernetes/render/main.go index 9ff2f81e..daa64a3d 100644 --- a/test/internaldns/kubernetes/render/main.go +++ b/test/internaldns/kubernetes/render/main.go @@ -392,7 +392,6 @@ func configure() { cfg := exampleConfigs("control-plane.example.yaml")["internal-dns-control-plane-config"] cfg.ConsumerPrefix = "fd53::/64" cfg.ClusterPrefix = "fd54::/64" - cfg.ManagedDomainSuffix = "managed.internal" cfg.OwnershipLeaseSeconds = 10 cfg.Projects = []runtime.ProjectConfig{{Name: "project-a", ProjectUID: "project-a-uid", SourceClusterUID: "source-cluster-a-uid", Namespace: projectNS, Kubeconfig: "/sources/project-a.json"}, {Name: "project-b", ProjectUID: "project-b-uid", SourceClusterUID: "source-cluster-b-uid", Namespace: projectNS, Kubeconfig: "/sources/project-b.json"}} cfg.NodeBackends = []model.Backend{{MemberID: "node-bind-0", Address: "127.0.0.1", Port: 5300}} From 13777130c7822b68d0c815a3c97b2933b1ce7ab7 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 12:48:46 -0500 Subject: [PATCH 2/4] test: verify isolated answers under the shared private domain --- test/internaldns/kubernetes/qualify/main.go | 7 +++++- .../kubernetes/qualify/scenarios.go | 23 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/test/internaldns/kubernetes/qualify/main.go b/test/internaldns/kubernetes/qualify/main.go index e25a673f..16ee1bca 100644 --- a/test/internaldns/kubernetes/qualify/main.go +++ b/test/internaldns/kubernetes/qualify/main.go @@ -289,9 +289,14 @@ func (s *suite) run(path string) error { } } a, b := &s.env.Projects[0], &s.env.Projects[1] - if a.resolverContext.UID == b.resolverContext.UID || a.zone.UID == b.zone.UID || a.resolverContext.Status.ManagedNamespace.Suffix == b.resolverContext.Status.ManagedNamespace.Suffix { + if a.resolverContext.UID == b.resolverContext.UID || a.zone.UID == b.zone.UID || a.resolverContext.Status.ManagedNamespace.DNSZoneRef.UID == b.resolverContext.Status.ManagedNamespace.DNSZoneRef.UID { return errors.New("source projects lost their distinct context, zone, or managed namespace identity") } + for _, p := range []*project{a, b} { + if p.resolverContext.Status.ManagedNamespace.Suffix != p.resolverContext.Spec.ManagedNamespace.DomainSuffix { + return fmt.Errorf("%s managed suffix does not match its requested domain", p.Name) + } + } s.pass("independent projects retain distinct context, zone, and managed namespace identities", map[string]string{"a": string(a.resolverContext.UID), "b": string(b.resolverContext.UID)}) if err := s.scenarios(a, b); err != nil { return err diff --git a/test/internaldns/kubernetes/qualify/scenarios.go b/test/internaldns/kubernetes/qualify/scenarios.go index 25725c68..fa7e3cd8 100644 --- a/test/internaldns/kubernetes/qualify/scenarios.go +++ b/test/internaldns/kubernetes/qualify/scenarios.go @@ -84,15 +84,28 @@ func (s *suite) scenarios(a, b *project) error { return err } s.pass("negative caches and overlapping endpoint address spaces stay isolated", nil) - // The product discovers the zone from context status rather than accepting a - // zone selection on the Compute resource. - if _, err := s.publish(a, a.resolverContext.Status.ManagedNamespace.DNSZoneRef, "managed", "web.instances", "10.42.0.11", "", 90*time.Second); err != nil { + // Products discover their managed zone through context status. + if _, err := s.publish(a, a.resolverContext.Status.ManagedNamespace.DNSZoneRef, "managed", "web", "10.42.0.11", "", 90*time.Second); err != nil { return err } - managedName := "web.instances." + a.resolverContext.Status.ManagedNamespace.Suffix - if err := s.expect(a, managedName, "A", false, dns.RcodeSuccess, "10.42.0.11"); err != nil { + managedName := "web." + a.resolverContext.Status.ManagedNamespace.Suffix + for _, tcp := range []bool{false, true} { + if err := s.expect(b, managedName, "A", tcp, dns.RcodeNameError); err != nil { + return err + } + } + if _, err := s.publish(b, b.resolverContext.Status.ManagedNamespace.DNSZoneRef, "managed", "web", "10.42.0.21", "", 90*time.Second); err != nil { return err } + for _, tcp := range []bool{false, true} { + if err := s.expect(a, managedName, "A", tcp, dns.RcodeSuccess, "10.42.0.11"); err != nil { + return err + } + if err := s.expect(b, managedName, "A", tcp, dns.RcodeSuccess, "10.42.0.21"); err != nil { + return err + } + } + s.pass("the same hostname in the shared managed domain resolves within its own context over UDP and TCP", managedName) apps := &dnsv1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: "apps", Namespace: a.Namespace}, Spec: dnsv1.DNSZoneSpec{DomainName: "apps.internal", DNSZoneClassName: "private-bind", Visibility: dnsv1.DNSZoneVisibilityPrivate}} if err := a.customer.Create(s.ctx, apps); err != nil { return err From 3e3ce63ee1f43b02f5655361ff3ae231e6f5f56a Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 14 Oct 2026 17:15:50 -0500 Subject: [PATCH 3/4] test: qualify the default internal domain --- test/internaldns/kubernetes/qualify/main.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/internaldns/kubernetes/qualify/main.go b/test/internaldns/kubernetes/qualify/main.go index e3e2beff..bfeb22b1 100644 --- a/test/internaldns/kubernetes/qualify/main.go +++ b/test/internaldns/kubernetes/qualify/main.go @@ -287,9 +287,14 @@ func (s *suite) run(path string) error { } } a, b := &s.env.Projects[0], &s.env.Projects[1] - if a.resolverContext.UID == b.resolverContext.UID || a.zone.UID == b.zone.UID || a.resolverContext.Status.ManagedNamespace.Suffix == b.resolverContext.Status.ManagedNamespace.Suffix { + if a.resolverContext.UID == b.resolverContext.UID || a.zone.UID == b.zone.UID || a.resolverContext.Status.ManagedNamespace.DNSZoneRef.Name == b.resolverContext.Status.ManagedNamespace.DNSZoneRef.Name { return errors.New("source projects lost their distinct context, zone, or managed namespace identity") } + for _, p := range s.env.Projects { + if p.resolverContext.Status.ManagedNamespace.Suffix != "datum.internal" { + return errors.New("managed namespace did not use the default domain") + } + } s.pass("independent projects retain distinct context, zone, and managed namespace identities", map[string]string{"a": string(a.resolverContext.UID), "b": string(b.resolverContext.UID)}) if err := s.scenarios(a, b); err != nil { return err From 8434064cb60ec1ed7752818905b2f12c2a926929 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 21 Oct 2026 10:12:29 -0500 Subject: [PATCH 4/4] test: specify the qualification DNS domain --- test/internaldns/kubernetes/qualify/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/internaldns/kubernetes/qualify/main.go b/test/internaldns/kubernetes/qualify/main.go index bfeb22b1..00ea92f4 100644 --- a/test/internaldns/kubernetes/qualify/main.go +++ b/test/internaldns/kubernetes/qualify/main.go @@ -324,7 +324,7 @@ func (s *suite) permissions(p *project) error { } func (s *suite) prepare(p *project) error { - p.resolverContext = dnsv1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "application-network", Namespace: p.Namespace}, Spec: dnsv1.DNSResolverContextSpec{ConsumerID: "qualification/" + p.Name, ManagedNamespace: dnsv1.DNSResolverContextManagedNamespace{Enabled: true}}} + p.resolverContext = dnsv1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "application-network", Namespace: p.Namespace}, Spec: dnsv1.DNSResolverContextSpec{ConsumerID: "qualification/" + p.Name, ManagedNamespace: dnsv1.DNSResolverContextManagedNamespace{Enabled: true, DomainSuffix: "datum.internal"}}} if err := p.integration.Create(s.ctx, &p.resolverContext); err != nil { return err }