package config import ( "os" "path/filepath" "strings" "testing" ) // configMapBody extracts the `config.yaml: |` block from a k8s manifest. The // ConfigMap is the thing an operator actually applies, and nothing in the test // suite loads it, so a typo in it is invisible until a pod crash-loops. func configMapBody(t *testing.T, manifest, key string) string { t.Helper() raw, err := os.ReadFile(manifest) if err != nil { t.Fatal(err) } lines := strings.Split(string(raw), "\n") header := " " + key + ": |" start := -1 for i, line := range lines { if strings.TrimRight(line, " \r") == header { start = i + 1 break } } if start < 0 { t.Fatalf("%s has no %q block", manifest, key) } var body []string for _, line := range lines[start:] { // The block scalar's content is indented past the key; the first // line that is not ends the block. if strings.TrimSpace(line) != "" && !strings.HasPrefix(line, " ") { break } body = append(body, strings.TrimPrefix(line, " ")) } if len(body) == 0 { t.Fatalf("%s: the %q block is empty", manifest, key) } return strings.Join(body, "\n") } // writeTemp writes body to a temp config file and returns its path. Load // resolves relative paths against the file's directory, so the fixture has to // be a real file in a real directory rather than an in-memory string. func writeTemp(t *testing.T, body string) string { t.Helper() path := filepath.Join(t.TempDir(), "config.yaml") if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } return path } // TestExampleConfigsValidate loads the two configuration files we ship and // requires both to survive Load, which ends in Validate. Everything else in // this file is free to be wrong; a shipped example that cannot start is not. func TestExampleConfigsValidate(t *testing.T) { t.Run("deploy/config.example.yaml", func(t *testing.T) { cfg, err := Load(filepath.Join("..", "..", "deploy", "config.example.yaml")) if err != nil { t.Fatalf("the shipped example config does not load: %v", err) } // Spot-check that the example is not merely valid but is saying what // its comments say: the API is open for writes here, metrics are on, // and the three blocklist sources are the three the file lists. if !cfg.API.AllowConfigWrite { t.Error("config.example.yaml documents config writes but sets allow_config_write: false") } if !cfg.Metrics.Enabled || !cfg.Metrics.Protect { t.Error("config.example.yaml should ship metrics enabled and protected") } if got := len(cfg.Block.Lists); got != 3 { t.Errorf("blocklist lists = %d, want 3 (haas, easylist, local-rpz)", got) } if got := cfg.Memory.Reserve; got != 256*1024*1024 { t.Errorf("memory.reserve = %d, want 256MiB as documented", int64(got)) } // The example must not carry a key that looks like a real one. This // is the file people copy, and a working key in it is a published // credential. for _, k := range cfg.API.Auth.APIKeys { if len(k) > 12 && !strings.Contains(k, "CHANGE-ME") { t.Errorf("config.example.yaml ships an API key that is not a "+ "placeholder: %q", k) } } }) t.Run("deploy/k8s/00-config.yaml ConfigMap", func(t *testing.T) { manifest := filepath.Join("..", "..", "deploy", "k8s", "00-config.yaml") body := configMapBody(t, manifest, "config.yaml") // The key comes from a Secret, so the manifest's ${FANCYDNS_API_KEY} // has to be set for the config to load at all — which is the point: // a missing Secret is a crash loop, not an open console. t.Setenv("FANCYDNS_API_KEY", "k8s-manifest-fixture-key") cfg, err := Load(writeTemp(t, body)) if err != nil { t.Fatalf("the k8s ConfigMap does not load: %v", err) } if got := cfg.API.Auth.APIKeys; len(got) != 1 || got[0] != "k8s-manifest-fixture-key" { t.Errorf("api keys = %q, want the expanded ${FANCYDNS_API_KEY}", got) } if cfg.API.Auth.TrustLoopback { t.Error("the k8s manifest exposes the API on 0.0.0.0, so trust_loopback must be false") } if cfg.API.AllowConfigWrite { t.Error("the k8s manifest sets allow_config_write: false; a ConfigMap " + "is not writable from the pod") } // The memory envelope is matched to the StatefulSet's 1Gi limit, and a // limit above the cgroup's is the cgroup kill AGENTS.md warns about. if got := int64(cfg.Memory.Limit); got != 1024*1024*1024 { t.Errorf("memory.limit = %d, want 1GiB", got) } if got := int64(cfg.Memory.CacheMaxBytes); got != 384*1024*1024 { t.Errorf("memory.cache_max_bytes = %d, want 384MiB", got) } if got := cfg.Cache.Size; got != 500000 { t.Errorf("cache.size = %d, want the 500000 the manifest documents", got) } if !cfg.DNS.DoT.Enabled || !cfg.DNS.DoH.Enabled { t.Error("the k8s manifest enables both DoT and DoH; both are required for the Ingress") } if !cfg.Metrics.Protect { t.Error("metrics.protect must stay true in the cluster") } if got := cfg.Log.Format; got != "json" { t.Errorf("log.format = %q, want json in a cluster", got) } }) t.Run("an unset API key variable is still fatal", func(t *testing.T) { // The control for the case above: the expansion does not fall back to // something harmless when the Secret is missing. manifest := filepath.Join("..", "..", "deploy", "k8s", "00-config.yaml") t.Setenv("FANCYDNS_API_KEY", "") os.Unsetenv("FANCYDNS_API_KEY") if _, err := Load(writeTemp(t, configMapBody(t, manifest, "config.yaml"))); err == nil { t.Fatal("the k8s ConfigMap loaded with no API key in the environment; " + "a missing Secret must be a startup failure") } }) }