Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8d6de60
feat: Manage private DNS contexts and resolver access
scotwells Oct 12, 2026
10a428b
Exercise recovery with API-enforced access validation
scotwells Oct 12, 2026
79877e1
fix: Align DNS integration boundaries
scotwells Oct 13, 2026
3d65ba8
refactor: Consolidate DNS outage behavior
scotwells Oct 13, 2026
cde282e
fix: Remove unused recovery test import
scotwells Oct 13, 2026
3fbc1fd
refactor: Consolidate DNS continuity behavior
scotwells Oct 13, 2026
1520b1e
refactor: Simplify private DNS integration
scotwells Oct 13, 2026
b06637b
refactor: Keep DNS permissions aligned with APIs
scotwells Oct 13, 2026
d3d808a
fix: Keep DNS review branches buildable
scotwells Oct 13, 2026
60b9051
fix: Align DNS validation and permission checks
scotwells Oct 13, 2026
6fad1a3
docs: Describe DNS mapping validation
scotwells Oct 13, 2026
f773735
docs: Describe DNS mappings and reference validation
scotwells Oct 13, 2026
aca3e3a
feat: Simplify private DNS publication and access
scotwells Oct 13, 2026
f46eb94
fix: Use authoritative DNS reads consistently
scotwells Oct 13, 2026
b855910
fix: Preserve independent DNS review builds
scotwells Oct 13, 2026
0ce96d3
chore: Sync private DNS stack with main
scotwells Oct 14, 2026
88af97e
chore: sync internal DNS with main
scotwells Oct 20, 2026
3acef17
chore: merge latest parent branch
scotwells Oct 21, 2026
227f6c5
chore: refresh stacked validation
scotwells Oct 21, 2026
6f47d20
chore: merge DNS environment preparation
scotwells Oct 21, 2026
1b483f0
fix: protect resolver address and consumer ownership
scotwells Oct 22, 2026
9e34dd0
Merge commit '29eea5f' into codex/dns-review-233
scotwells Oct 22, 2026
ae2a3d6
fix: protect retained regional DNS address claims
scotwells Oct 22, 2026
08e8ce5
Merge branch 'feat/internal-dns-delivery-contracts' into feat/interna…
scotwells Oct 22, 2026
2513558
fix: Stop setting the removed DNSZoneAssociation managed flag
scotwells Oct 22, 2026
86295fd
Merge branch 'feat/internal-dns-delivery-contracts' into feat/interna…
scotwells Oct 22, 2026
24b9061
Merge branch 'feat/internal-dns-delivery-contracts' into feat/interna…
scotwells Oct 22, 2026
9a52cea
Merge branch 'feat/internal-dns-delivery-contracts' into feat/interna…
scotwells Oct 22, 2026
be9b933
Merge branch 'feat/internal-dns-delivery-contracts' into feat/interna…
scotwells Oct 22, 2026
a489e2d
fix: report invalid managed domains and retry transient destination c…
scotwells Oct 22, 2026
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
134 changes: 134 additions & 0 deletions internal/internaldns/controlplane/access_recovery_envtest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package controlplane

import (
"context"
"os"
"path/filepath"
"testing"
"time"

dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1"
"go.miloapis.com/dns-operator/internal/internaldns/platform"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)

// TestAccessRecoveryPreservesSourceFencesWithCEL exercises the actual CRD
// transition rules together with controller withdrawal, renewal, and UID pins.
func TestAccessRecoveryPreservesSourceFencesWithCEL(t *testing.T) {
assets := os.Getenv("KUBEBUILDER_ASSETS")
if assets == "" {
t.Skip("set KUBEBUILDER_ASSETS to run access recovery against the API server")
}
environment := &envtest.Environment{BinaryAssetsDirectory: assets, CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true}
cfg, err := environment.Start()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := environment.Stop(); err != nil {
t.Error(err)
}
})
scheme := runtime.NewScheme()
requireAccessTestSuccess(t, dnsv1alpha1.AddToScheme(scheme))
requireAccessTestSuccess(t, corev1.AddToScheme(scheme))
cl, err := client.New(cfg, client.Options{Scheme: scheme})
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
for _, ns := range []string{"project", "platform"} {
requireAccessTestSuccess(t, cl.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}))
}
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) {
t.Helper()
requireAccessTestSuccess(t, cl.Create(ctx, c))
c.Generation = 1
apimeta.SetStatusCondition(&c.Status.Conditions, metav1.Condition{Type: "Ready", Status: metav1.ConditionTrue, ObservedGeneration: c.Generation, Reason: "Accepted", LastTransitionTime: metav1.NewTime(now)})
requireAccessTestSuccess(t, cl.Status().Update(ctx, c))
}
readyContext(resolverContext)
access := &dnsv1alpha1.DNSResolverAccessBinding{ObjectMeta: metav1.ObjectMeta{Name: "access", Namespace: "project"}, Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: resolverContext.Name, UID: resolverContext.UID}, Region: "central", DestinationAddress: "fd70:100::10"}}
requireAccessTestSuccess(t, cl.Create(ctx, access))
r := &Reconciler{Client: cl, Scheme: scheme, Options: ReconcilerOptions{ProjectUID: "project-uid", PlatformNamespace: "platform", Region: "central", Shard: "shared", AddressAllocator: &platform.Allocator{Client: cl, Namespace: "platform"}}}
reconcile := func(at time.Time) {
t.Helper()
requireAccessTestSuccess(t, r.reconcileContextBindings(ctx, "project", nil, map[string]*dnsv1alpha1.DNSZone{}, at))
}
reconcile(now)
var bindings dnsv1alpha1.DNSResolverBindingList
requireAccessTestSuccess(t, cl.List(ctx, &bindings, client.InNamespace("platform")))
if len(bindings.Items) != 1 {
t.Fatalf("got %d bindings", len(bindings.Items))
}
binding := bindings.Items[0]
key := client.ObjectKeyFromObject(&binding)
reconcile(now.Add(365 * 24 * time.Hour))
requireAccessTestSuccess(t, cl.Get(ctx, key, &binding))
if binding.Spec.Tombstone || binding.Spec.Version.Sequence != 1 || binding.Spec.Configuration.Revision != 1 {
t.Fatalf("elapsed time changed access: %#v", binding.Spec)
}
requireAccessTestSuccess(t, cl.Get(ctx, client.ObjectKeyFromObject(access), access))
// Schema validation preserves listener identity.
for _, mutate := range []func(*dnsv1alpha1.DNSResolverAccessBinding){func(a *dnsv1alpha1.DNSResolverAccessBinding) { a.Spec.DestinationAddress = "fd70:100::20" }, func(a *dnsv1alpha1.DNSResolverAccessBinding) { a.Spec.ContextRef.UID = "another-context" }} {
candidate := access.DeepCopy()
mutate(candidate)
if err := cl.Update(ctx, candidate); !apierrors.IsInvalid(err) {
t.Fatalf("identity mutation accepted: %v", err)
}
}
// An incomplete cached List cannot revoke a live access binding.
stale := &Reconciler{Client: &accessListSnapshotClient{Client: cl, accesses: nil}, Scheme: scheme, Options: r.Options}
requireAccessTestSuccess(t, stale.reconcileContextBindings(ctx, "project", nil, nil, now.Add(365*24*time.Hour)))
requireAccessTestSuccess(t, cl.Get(ctx, key, &binding))
if binding.Spec.Tombstone {
t.Fatal("incomplete List withdrew live access")
}
requireAccessTestSuccess(t, cl.Delete(ctx, resolverContext))
reconcile(now.Add(365 * 24 * time.Hour))
requireAccessTestSuccess(t, cl.Get(ctx, key, &binding))
if !binding.Spec.Tombstone || binding.Spec.Version.Sequence != 2 {
t.Fatal("explicit context deletion did not withdraw access")
}
replacement := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: resolverContext.Name, Namespace: "project"}, Spec: resolverContext.Spec}
readyContext(replacement)
reconcile(now.Add(365 * 24 * time.Hour))
requireAccessTestSuccess(t, cl.Get(ctx, key, &binding))
if !binding.Spec.Tombstone {
t.Fatal("replacement context inherited retired access")
}
requireAccessTestSuccess(t, cl.Delete(ctx, access))
access = &dnsv1alpha1.DNSResolverAccessBinding{ObjectMeta: metav1.ObjectMeta{Name: "replacement", Namespace: "project"}, Spec: access.Spec}
access.Spec.ContextRef.UID = replacement.UID
access.Spec.DestinationAddress = "fd70:100::20"
requireAccessTestSuccess(t, cl.Create(ctx, access))
reconcile(now.Add(365 * 24 * time.Hour))
requireAccessTestSuccess(t, cl.List(ctx, &bindings, client.InNamespace("platform")))
active := 0
for _, b := range bindings.Items {
if !b.Spec.Tombstone {
active++
if b.Spec.Source.ResolverContextRef.UID != replacement.UID {
t.Fatal("new binding uses retired context")
}
}
}
if active != 1 {
t.Fatalf("active bindings=%d", active)
}
}

func requireAccessTestSuccess(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
Loading
Loading