package migrate import ( "context" "strings" "testing" "time" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynfake "k8s.io/client-go/dynamic/fake" "git.k8s-git-k6n.net/mats/fancy-dns/internal/config" "git.k8s-git-k6n.net/mats/fancy-dns/internal/kubeedit" "git.k8s-git-k6n.net/mats/fancy-dns/internal/kubesource" ) // The data the live deployment actually has. const liveRPZ = `$TTL 60 tornberg.me A 10.10.3.10 knatofs.se A 10.10.3.10 k6n.net A 10.10.3.10 slask.local A 10.10.3.10 iamu.dev A 10.10.3.10 ` const homeZone = `$TTL 300 $ORIGIN home.example.com. @ IN SOA ns1.home.example.com. hostmaster.home.example.com. 2024010101 3600 600 604800 300 @ IN NS ns1.home.example.com. ns1 IN A 10.0.0.53 nas 60 IN A 10.0.0.20 mail IN MX 10 mx.home.example.com. txt IN TXT "v=spf1 -all" *.apps IN A 10.0.0.30 ` func liveConfig(t *testing.T) *config.Config { t.Helper() cfg := config.Default() cfg.Block.Lists = []config.ListSource{ {Name: "edge-router-x-address-overrides", Format: "rpz", Path: "/etc/fancy-dns/rpz/address-overrides.rpz", Enabled: true}, {Name: "haas", Format: "plain", URL: "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", Path: "/var/lib/fancy-dns/lists/hosts", Enabled: true, Refresh: config.Duration(24 * time.Hour), Categories: []string{"ads", "tracking"}}, {Name: "local file", Format: "hosts", Path: "/etc/x/hosts", Enabled: true}, } cfg.Block.BlockEntries = []string{"ads.example.com", "*.tracker.example", "bad name"} cfg.Block.AllowEntries = []string{"good.example"} cfg.ACL.Groups = []config.Group{ {Name: "kids", Policy: "block_only", Match: config.Match{CIDRs: []string{"10.0.20.0/24"}}}, {Name: "Bad Name", Policy: "allow", Match: config.Match{CIDRs: []string{"10.0.30.0/24"}}}, } return cfg } func TestConvertEverythingTheDeploymentHolds(t *testing.T) { res := Convert(Inputs{ ZoneFiles: map[string]string{"home.example.com": homeZone, "broken.example": "not a zone @@@ ("}, RPZ: liveRPZ + "bad.test CNAME x.example.\n", Config: liveConfig(t), Namespace: "dns", }) kinds := map[string]int{} for _, o := range res.Objects { kinds[o.GetKind()]++ if o.GetNamespace() != "dns" || o.GetLabels()["app.kubernetes.io/managed-by"] != managedBy { t.Errorf("%s/%s: namespace/labels = %q %v", o.GetKind(), o.GetName(), o.GetNamespace(), o.GetLabels()) } } want := map[string]int{"DNSZone": 1, "AddressOverride": 5, "BlockList": 1, "DomainRule": 2, "ClientGroup": 1} for k, n := range want { if kinds[k] != n { t.Errorf("%s: %d objects, want %d (%v)", k, kinds[k], n, kinds) } } skipped := strings.Join(res.Skipped, "\n") for _, must := range []string{"broken.example", "bad.test", "local file", "bad name", "Bad Name"} { if !strings.Contains(skipped, must) { t.Errorf("%q should have been reported as skipped:\n%s", must, skipped) } } notes := strings.Join(res.Notes, "\n") for _, must := range []string{"edge-router-x-address-overrides", "haas", "blocklist.block_entries"} { if !strings.Contains(notes, must) { t.Errorf("notes should mention %q:\n%s", must, notes) } } } // TestConvertedObjectsAreWhatTheResolverAccepts applies the result to a cluster // and reads it back through the resolver's own source: nothing converted may be // rejected there, and the data must come out the same. func TestConvertedObjectsAreWhatTheResolverAccepts(t *testing.T) { res := Convert(Inputs{ ZoneFiles: map[string]string{"home.example.com": homeZone}, RPZ: liveRPZ, Config: liveConfig(t), Namespace: "dns", }) client := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), map[schema.GroupVersionResource]string{ kubesource.ZoneGVR: "DNSZoneList", kubesource.GroupGVR: "ClientGroupList", kubesource.ListGVR: "BlockListList", kubesource.OverrideGVR: "AddressOverrideList", kubesource.RuleGVR: "DomainRuleList", }) ed := kubeedit.New(client, "dns") for _, o := range res.Objects { if err := ed.Apply(context.Background(), o, "migrate"); err != nil { t.Fatalf("apply %s/%s: %v", o.GetKind(), o.GetName(), err) } } src, err := kubesource.New(kubesource.Options{Client: client, Namespace: "dns", Debounce: 10 * time.Millisecond}) if err != nil { t.Fatal(err) } got := make(chan *kubesource.Snapshot, 4) src.OnChange(func(s *kubesource.Snapshot) { got <- s }) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go func() { _ = src.Run(ctx) }() var sn *kubesource.Snapshot select { case sn = <-got: case <-time.After(5 * time.Second): t.Fatal("no snapshot") } if len(sn.Problems) != 0 { t.Fatalf("the resolver rejected migrated objects: %+v", sn.Problems) } if len(sn.Overrides) != 5 || sn.Overrides[0].Domain != "iamu.dev" || sn.Overrides[0].Addresses[0] != "10.10.3.10" { t.Errorf("overrides = %+v", sn.Overrides) } if len(sn.Groups) != 1 || sn.Groups[0].Name != "kids" || sn.Groups[0].Policy != "block_only" { t.Errorf("groups = %+v", sn.Groups) } if len(sn.Lists) != 1 || sn.Lists[0].Name != "haas" || !sn.Lists[0].Enabled || sn.Lists[0].Path != "" { t.Errorf("lists = %+v", sn.Lists) } if strings.Join(sn.BlockNames, ",") != "ads.example.com,*.tracker.example" || strings.Join(sn.AllowNames, ",") != "good.example" { t.Errorf("rules = %v / %v", sn.BlockNames, sn.AllowNames) } text := sn.Zones["home.example.com."] if text == "" { t.Fatalf("zones = %v", sn.Zones) } for _, line := range []string{"ns1", "10.0.0.20", "mx.home.example.com.", "v=spf1 -all", "*.apps", "IN SOA"} { if !strings.Contains(text, line) { t.Errorf("rendered zone lacks %q:\n%s", line, text) } } // nas had its own TTL (60); the rest inherit the zone's 300. if !strings.Contains(text, "nas") || !strings.Contains(text, " 1m IN A ") && !strings.Contains(text, " 60 IN A ") { t.Errorf("nas lost its TTL:\n%s", text) } } func TestYAMLRoundTrips(t *testing.T) { res := Convert(Inputs{RPZ: liveRPZ, Namespace: "dns"}) out, err := YAML(res.Objects) if err != nil { t.Fatal(err) } s := string(out) if strings.Count(s, "---") != 4 || !strings.Contains(s, "kind: AddressOverride") || !strings.Contains(s, "namespace: dns") { t.Fatalf("YAML:\n%s", s) } }