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( configMap("dns", "fancy-dns-zones", map[string]any{"zone--home.example.com.zone": zone, "unrelated": "x"}), configMap("dns", "fancy-dns-rpz", map[string]any{"address-overrides.rpz": rpz}), ) 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()) } if l, _ := c.Resource(kubesource.OverrideGVR).Namespace("dns").List(context.Background(), metav1.ListOptions{}); len(l.Items) != 2 { t.Fatalf("%d overrides applied, want 2", len(l.Items)) } 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-rpz", 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()) } }