package engine import ( "bytes" "context" "fmt" "io" "log/slog" "os" "os/exec" "path/filepath" "strings" "testing" "time" "jarl/internal/auth" "jarl/internal/bind" "jarl/internal/model" "jarl/internal/render" "jarl/internal/store" "jarl/internal/testutil" ) func TestImportAndAdopt(t *testing.T) { bins := testutil.FindBind() if bins == nil || bins.Named == "" || bins.RNDC == "" || bins.Dig == "" { t.Skip("named, rndc and dig are needed") } dir := t.TempDir() legacy := filepath.Join(dir, "legacy") run := filepath.Join(dir, "run") os.MkdirAll(legacy, 0o755) os.MkdirAll(run, 0o755) keyOut, err := runEnv(bins.Env, bins.TSIGKeygen, "-a", "hmac-sha256", "rndc-key") if err != nil { t.Fatal(err) } keyFile := filepath.Join(legacy, "rndc.key") os.WriteFile(keyFile, []byte(keyOut), 0o600) dnsPort, ctlPort := freePort(t), freePort(t) files := map[string]string{ "named.conf": fmt.Sprintf(`include "%[1]s/named.conf.options"; include "%[1]s/named.conf.local"; include "%[1]s/rndc.key"; controls { inet 127.0.0.1 port %[2]d allow { 127.0.0.1; } keys { "rndc-key"; }; }; `, legacy, ctlPort), "named.conf.options": fmt.Sprintf(`acl homelab { 127.0.0.0/8; 192.168.1.0/24; }; options { directory "%[1]s"; pid-file "%[1]s/named.pid"; session-keyfile "%[1]s/session.key"; listen-on port %[2]d { 127.0.0.1; }; listen-on-v6 { none; }; recursion no; allow-query { any; }; }; `, run, dnsPort), "named.conf.local": fmt.Sprintf(`zone "adbraun.test" { type master; file "%[1]s/db.adbraun.test"; }; zone "1.168.192.in-addr.arpa" { type master; file "%[1]s/db.192.168.1"; }; `, legacy), "db.adbraun.test": `$TTL 3600 @ IN SOA ns1.adbraun.test. admin.adbraun.test. ( 2024051201 3600 900 1209600 300 ) IN NS ns1 ns1 IN A 127.0.0.1 nas IN A 192.168.1.20 ; synology www 300 IN CNAME nas `, "db.192.168.1": `$TTL 86400 @ IN SOA ns1.adbraun.test. admin.adbraun.test. ( 2024051201 3600 900 1209600 300 ) @ IN NS ns1.adbraun.test. 20 IN PTR nas.adbraun.test. `, } for name, content := range files { os.WriteFile(filepath.Join(legacy, name), []byte(content), 0o644) } mainConf := filepath.Join(legacy, "named.conf") var namedLog bytes.Buffer cmd := exec.Command(bins.Named, "-c", mainConf, "-g", "-n", "1") cmd.Env = append(os.Environ(), bins.Env...) cmd.Stdout, cmd.Stderr = &namedLog, &namedLog if err := cmd.Start(); err != nil { t.Fatal(err) } defer func() { cmd.Process.Kill() cmd.Wait() if t.Failed() { t.Logf("named log:\n%s", namedLog.String()) } }() tools := &bind.Tools{NamedCheckConf: bins.CheckConf, NamedCheckZone: bins.CheckZone, RNDC: bins.RNDC, Env: bins.Env, RNDCArgs: []string{"-k", keyFile, "-s", "127.0.0.1", "-p", fmt.Sprint(ctlPort)}} for i := 0; ; i++ { if _, err := tools.Status(context.Background()); err == nil { break } if i > 100 { t.Fatalf("named did not start:\n%s", namedLog.String()) } time.Sleep(100 * time.Millisecond) } dig := func(name, typ string) string { out, _ := runEnv(bins.Env, bins.Dig, "+short", "+norec", "-p", fmt.Sprint(dnsPort), "@127.0.0.1", name, typ) return strings.TrimSpace(out) } if got := dig("nas.adbraun.test", "A"); got != "192.168.1.20" { t.Fatalf("legacy setup broken: %q", got) } st, err := store.Open(filepath.Join(dir, "jarl.db")) if err != nil { t.Fatal(err) } defer st.Close() cfg := Config{ Paths: render.Paths{ConfigDir: filepath.Join(dir, "jarl/etc"), ZoneDir: filepath.Join(dir, "jarl/zones"), CacheDir: filepath.Join(dir, "jarl/cache")}, MainConfig: mainConf, StagingDir: filepath.Join(dir, "staging"), ReloadTimeout: 10 * time.Second, } e := New(st, tools, cfg, slog.New(slog.NewTextHandler(io.Discard, nil))) admin := &model.User{Username: "admin", Admin: true} st.CreateUser(context.Background(), admin) p := &auth.Principal{User: admin, Method: "local"} ctx := context.Background() imp, err := e.Import(ctx, mainConf, p, false) if err != nil { t.Fatal(err) } if !imp.Applied { t.Fatalf("import not applied: %+v %+v", imp.Result.Error, imp.Result.Problems) } if ok, _ := e.Adopted(); ok { t.Fatal("adopted before adopt") } ad, err := e.Adopt(ctx, p) if err != nil { t.Fatalf("adopt: %v", err) } if len(ad.Serials) != 2 || ad.Serials[0].New <= ad.Serials[0].Old { t.Errorf("serials = %+v", ad.Serials) } if b, _ := os.ReadFile(ad.Backup); !strings.Contains(string(b), "named.conf.options") { t.Error("backup missing") } if ok, _ := e.Adopted(); !ok { t.Error("not adopted after adopt") } // Same answers, now from Jarl's files. if got := dig("www.adbraun.test", "A"); !strings.Contains(got, "192.168.1.20") { t.Errorf("www after adopt = %q", got) } zs, err := tools.ZoneStatus(ctx, "adbraun.test", "") if err != nil || !strings.Contains(zs.Files, cfg.Paths.ZoneDir) { t.Errorf("zone not loaded from Jarl's file: %+v %v", zs, err) } // And Jarl manages the server from now on. if _, err := e.Submit(ctx, []Request{rec("adbraun.test", "printer", "A", "192.168.1.30")}, p, "test", "", ApplyOptions{}); err != nil { t.Fatal(err) } if got := dig("printer.adbraun.test", "A"); got != "192.168.1.30" { t.Errorf("printer = %q", got) } if got := dig("20.1.168.192.in-addr.arpa", "PTR"); got != "nas.adbraun.test." { t.Errorf("PTR = %q", got) } }