Files
matsandClaude Sonnet 5.5 242bcd98d9 migrate: read the live address overrides from the zones ConfigMap, not the seed
The old console kept the zone files and the live address-overrides.rpz in the
same fancy-dns-zones ConfigMap; fancy-dns-rpz was only the static seed the
overrides started from. Reading the live cluster showed the two had diverged
(6 entries against 5, the missing one being stackship.k6n.net), so the default
would have silently dropped a redirect. The default is now fancy-dns-zones, and
the test puts a different stale seed in fancy-dns-rpz so reading the wrong one
fails.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 10:34:31 +02:00

150 lines
5.5 KiB
Go

package main
import (
"bytes"
"context"
"os"
"path/filepath"
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
dynfake "k8s.io/client-go/dynamic/fake"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/kubesource"
)
const rpz = "$TTL 60\ntornberg.me A 10.10.3.10\nk6n.net A 10.10.3.10\n"
const zone = `$TTL 300
@ IN SOA ns1.home.example.com. hostmaster.home.example.com. 1 3600 600 604800 300
@ IN NS ns1.home.example.com.
ns1 IN A 10.0.0.53
nas IN A 10.0.0.20
`
func configMap(ns, name string, data map[string]any) *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1", "kind": "ConfigMap",
"metadata": map[string]any{"name": name, "namespace": ns},
"data": data,
}}
}
func fakeCluster(objs ...runtime.Object) *dynfake.FakeDynamicClient {
return dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(),
map[schema.GroupVersionResource]string{
configMaps: "ConfigMapList",
kubesource.ZoneGVR: "DNSZoneList",
kubesource.GroupGVR: "ClientGroupList",
kubesource.ListGVR: "BlockListList",
kubesource.OverrideGVR: "AddressOverrideList",
kubesource.RuleGVR: "DomainRuleList",
}, objs...)
}
func useClient(t *testing.T, c dynamic.Interface) {
t.Helper()
old := newClient
newClient = func(kubesource.ClientOptions) (dynamic.Interface, error) { return c, nil }
t.Cleanup(func() { newClient = old })
}
// TestMigrateFromTheClusterThenApply reads the two ConfigMaps the old console
// wrote, prints manifests without touching anything, and then applies them.
func TestMigrateFromTheClusterThenApply(t *testing.T) {
c := fakeCluster(
// The old console kept the zone files and the live overrides in one
// ConfigMap; fancy-dns-rpz was only the seed it started from, and is
// deliberately different here so reading the wrong one fails.
configMap("dns", "fancy-dns-zones", map[string]any{"zone--home.example.com.zone": zone, "address-overrides.rpz": rpz, "unrelated": "x"}),
configMap("dns", "fancy-dns-rpz", map[string]any{"address-overrides.rpz": "$TTL 60\nstale-seed.example A 10.0.0.1\n"}),
)
useClient(t, c)
var out, errb bytes.Buffer
if err := migrateMain([]string{"-kube-namespace", "dns"}, &out, &errb); err != nil {
t.Fatalf("dry run: %v\n%s", err, errb.String())
}
for _, want := range []string{"kind: DNSZone", "kind: AddressOverride", "domain: tornberg.me", "zone: home.example.com"} {
if !strings.Contains(out.String(), want) {
t.Errorf("manifest lacks %q:\n%s", want, out.String())
}
}
if l, _ := c.Resource(kubesource.OverrideGVR).Namespace("dns").List(context.Background(), metav1.ListOptions{}); len(l.Items) != 0 {
t.Fatal("the dry run wrote objects")
}
out.Reset()
errb.Reset()
if err := migrateMain([]string{"-kube-namespace", "dns", "-apply"}, &out, &errb); err != nil {
t.Fatalf("apply: %v\n%s", err, errb.String())
}
l, _ := c.Resource(kubesource.OverrideGVR).Namespace("dns").List(context.Background(), metav1.ListOptions{})
if len(l.Items) != 2 {
t.Fatalf("%d overrides applied, want 2", len(l.Items))
}
for _, it := range l.Items {
if strings.Contains(it.GetName(), "stale-seed") {
t.Fatal("the stale seed ConfigMap was read instead of the live one")
}
}
if l, _ := c.Resource(kubesource.ZoneGVR).Namespace("dns").List(context.Background(), metav1.ListOptions{}); len(l.Items) != 1 {
t.Fatalf("%d zones applied, want 1", len(l.Items))
}
if !strings.Contains(errb.String(), "old ConfigMaps were not touched") {
t.Errorf("no reassurance in the report:\n%s", errb.String())
}
// The ConfigMaps are exactly as they were.
cm, err := c.Resource(configMaps).Namespace("dns").Get(context.Background(), "fancy-dns-zones", metav1.GetOptions{})
if err != nil || cm.Object["data"].(map[string]any)["address-overrides.rpz"] != rpz {
t.Fatalf("the source ConfigMap changed: %v %v", cm, err)
}
// Running it again changes nothing (create-or-update).
if err := migrateMain([]string{"-kube-namespace", "dns", "-apply"}, &out, &errb); err != nil {
t.Fatalf("second apply: %v", err)
}
}
func TestMigrateOfflineFromFiles(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "home.example.com.zone"), []byte(zone), 0o600); err != nil {
t.Fatal(err)
}
rpzPath := filepath.Join(t.TempDir(), "a.rpz")
if err := os.WriteFile(rpzPath, []byte(rpz), 0o600); err != nil {
t.Fatal(err)
}
// No cluster client is built when nothing is read from one.
old := newClient
newClient = func(kubesource.ClientOptions) (dynamic.Interface, error) {
t.Fatal("a cluster client was built for an offline run")
return nil, nil
}
defer func() { newClient = old }()
var out, errb bytes.Buffer
err := migrateMain([]string{"-kube-namespace", "dns", "-zones-dir", dir, "-rpz-file", rpzPath, "-zones-configmap", "", "-rpz-configmap", ""}, &out, &errb)
if err != nil {
t.Fatalf("%v\n%s", err, errb.String())
}
if !strings.Contains(out.String(), "kind: DNSZone") || !strings.Contains(out.String(), "kind: AddressOverride") {
t.Fatalf("manifest:\n%s", out.String())
}
}
func TestMigrateMissingConfigMapsIsNotAnError(t *testing.T) {
useClient(t, fakeCluster())
var out, errb bytes.Buffer
if err := migrateMain([]string{"-kube-namespace", "dns"}, &out, &errb); err != nil {
t.Fatalf("%v", err)
}
if !strings.Contains(errb.String(), "does not exist") || !strings.Contains(errb.String(), "nothing to migrate") {
t.Fatalf("report:\n%s", errb.String())
}
}