diff --git a/deploy/k8s/00-config.yaml b/deploy/k8s/00-config.yaml index 0edcc8a..9649e2d 100644 --- a/deploy/k8s/00-config.yaml +++ b/deploy/k8s/00-config.yaml @@ -86,20 +86,10 @@ data: blocklist: enabled: true default_action: nxdomain - local_zones: [/etc/fancy-dns/zones/*.zone] - lists: - - name: edge-router-x-address-overrides - format: rpz - path: /etc/fancy-dns/rpz/address-overrides.rpz - enabled: true - refresh: 0s - - name: haas - format: plain - url: https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts - path: /var/lib/fancy-dns/lists/hosts - enabled: true - refresh: 24h - categories: [ads, tracking] + # Lists, zones, address redirects, inline rules and client groups are + # cluster objects now (BlockList, DNSZone, AddressOverride, DomainRule, + # ClientGroup); see deploy/MIGRATION.md. `fancy-dns-console migrate` + # converts what was here. acl: default_policy: allow diff --git a/deploy/k8s/05-serviceaccount.yaml b/deploy/k8s/05-serviceaccount.yaml index 0be9911..6e66e8a 100644 --- a/deploy/k8s/05-serviceaccount.yaml +++ b/deploy/k8s/05-serviceaccount.yaml @@ -1,6 +1,5 @@ -# The UI edits the shared `fancy-dns-zones` ConfigMap. Its projected volume is -# read-only; these narrowly scoped permissions let the API update only that -# one ConfigMap so both DNS replicas receive the same records. +# The resolver's identity. It reads the fancy-dns objects through the Role in +# crds/rbac.yaml and writes nothing; the console has its own account. apiVersion: v1 kind: ServiceAccount metadata: @@ -9,28 +8,3 @@ metadata: labels: app.kubernetes.io/name: fancy-dns automountServiceAccountToken: true ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: fancy-dns-zones - namespace: dns -rules: - - apiGroups: [""] - resources: ["configmaps"] - resourceNames: ["fancy-dns-zones"] - verbs: ["get", "patch"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: fancy-dns-zones - namespace: dns -subjects: - - kind: ServiceAccount - name: fancy-dns - namespace: dns -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: fancy-dns-zones diff --git a/deploy/k8s/06-zones.yaml b/deploy/k8s/06-zones.yaml deleted file mode 100644 index b9fb7c2..0000000 --- a/deploy/k8s/06-zones.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: fancy-dns-zones - namespace: dns - labels: - app.kubernetes.io/name: fancy-dns - app.kubernetes.io/component: local-zones -# Leave data unmanaged by GitOps. The Local zones page writes one -# zone--.zone -# key per zone through the API; server-side apply preserves those user fields. diff --git a/deploy/k8s/07-rpz.yaml b/deploy/k8s/07-rpz.yaml deleted file mode 100644 index 9a5b512..0000000 --- a/deploy/k8s/07-rpz.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: fancy-dns-rpz - namespace: dns - labels: - app.kubernetes.io/name: fancy-dns - app.kubernetes.io/component: address-overrides -data: - address-overrides.rpz: | - $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 diff --git a/deploy/k8s/10-deployment.yaml b/deploy/k8s/10-deployment.yaml index 724d813..e96d1e2 100644 --- a/deploy/k8s/10-deployment.yaml +++ b/deploy/k8s/10-deployment.yaml @@ -27,7 +27,6 @@ spec: annotations: k8s-helper.io/git-url: "https://git.k6n.net/mats/fancy-dns" k8s-helper.io/git-ssh: "git@git.k6n.net:mats/fancy-dns.git" - dns.k6n.net/zones-api: "enabled" # A config change is a SIGHUP, not a restart: the server re-reads the # file and hot-swaps every component. This annotation is for # stakater/reloader-class controllers; without one, `kubectl rollout @@ -66,6 +65,10 @@ spec: args: - -config - /etc/fancy-dns/config.yaml + # Read ClientGroup, BlockList, DNSZone, AddressOverride and + # DomainRule objects from this namespace with the pod's service + # account. The console (console/console.yaml) is what writes them. + - -kube env: # Substituted into the config by the loader, so the admin API key # comes from the Secret and not from the ConfigMap. @@ -136,12 +139,6 @@ spec: mountPath: /etc/fancy-dns/config.yaml subPath: config.yaml readOnly: true - - name: zones - mountPath: /etc/fancy-dns/zones - readOnly: true - - name: rpz - mountPath: /etc/fancy-dns/rpz - readOnly: true - name: tls mountPath: /etc/fancy-dns/tls readOnly: true @@ -153,17 +150,6 @@ spec: - name: config configMap: name: fancy-dns-config - # Local zones are ordinary master files. The API writes them to the - # shared ConfigMap, which projects one zone--.zone file per zone here. - # The GitOps manifest creates the ConfigMap empty; the UI owns its data. - - name: zones - configMap: - name: fancy-dns-zones - optional: false - - name: rpz - configMap: - name: fancy-dns-rpz - optional: false - name: tls secret: secretName: fancy-dns-tls diff --git a/deploy/k8s/kustomization.yaml b/deploy/k8s/kustomization.yaml index bbba73f..bd5c9ad 100644 --- a/deploy/k8s/kustomization.yaml +++ b/deploy/k8s/kustomization.yaml @@ -11,8 +11,8 @@ namespace: dns resources: - 00-config.yaml - 05-serviceaccount.yaml - - 06-zones.yaml - - 07-rpz.yaml + - crds/crds.yaml + - crds/rbac.yaml - 10-deployment.yaml - 20-service.yaml - 30-ingress.yaml diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 529235b..9954a3b 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -578,6 +578,9 @@ func (s *Server) Commit(next *config.Config) error { return s.commit(next) } // parseConfigYAML decodes a config document on top of the defaults, which is // what makes a partial document from the UI editor work. func parseConfigYAML(text string) (*config.Config, error) { + if strings.TrimSpace(text) == "" { + return nil, errors.New("the document is empty") + } next := config.Default() if err := yaml.Unmarshal([]byte(text), next); err != nil { return nil, err @@ -640,7 +643,7 @@ func configWarnings(cfg *config.Config) []string { add("blocklist.enabled: blocklists are off, so this instance is an open resolver for everything") } else if len(cfg.Block.Lists) == 0 && len(cfg.Block.LocalZones) == 0 && len(cfg.Block.BlockEntries) == 0 { - add("blocklist: enabled but no source is configured") + add("blocklist: enabled but this config names no source (fine if BlockList or DomainRule objects supply it)") } for _, l := range cfg.Block.Lists { if strings.HasPrefix(l.URL, "http://") { diff --git a/internal/api/settings_test.go b/internal/api/settings_test.go index ae6fd55..6e6d9b9 100644 --- a/internal/api/settings_test.go +++ b/internal/api/settings_test.go @@ -146,7 +146,7 @@ func TestSettingsPutRejects(t *testing.T) { {"broken yaml", `{"config_yaml": "api: [\n"}`, "cannot parse config"}, { "invalid config", - `{"config_yaml": "blocklist:\n enabled: true\n lists: []\n"}`, + `{"config_yaml": "acl:\n default_policy: maybe\n"}`, "invalid config", }, } @@ -261,11 +261,9 @@ func TestSettingsValidate(t *testing.T) { }) t.Run("empty document is not valid", func(t *testing.T) { - // Not the obvious answer, and worth pinning: an empty document - // decodes onto config.Default, and the shipped defaults enable the - // blocklist without a single source, so Validate rejects them. The - // UI therefore cannot be used to "reset to nothing"; a reset to - // defaults has to send a real default document. + // An empty document would decode onto the defaults and be valid, so the + // editor could be used to wipe the config by accident. It is refused + // outright; a reset to defaults has to send a real default document. res := h.do(http.MethodPost, "/api/settings/validate", `{"config_yaml": ""}`, "X-API-Key: rw-key") if res.status != http.StatusOK { t.Fatalf("validate = %d, want 200: %s", res.status, res.body) @@ -297,7 +295,7 @@ func TestSettingsValidate(t *testing.T) { t.Run("invalid config", func(t *testing.T) { res := h.do(http.MethodPost, "/api/settings/validate", - `{"config_yaml": "blocklist:\n enabled: true\n lists: []\n"}`, "X-API-Key: rw-key") + `{"config_yaml": "acl:\n default_policy: maybe\n"}`, "X-API-Key: rw-key") if res.status != http.StatusOK { t.Fatalf("validate = %d, want 200: %s", res.status, res.body) } @@ -316,8 +314,8 @@ func TestSettingsValidate(t *testing.T) { msg, _ := m["message"].(string) joined += msg + "\n" } - if !strings.Contains(joined, "blocklist is enabled but no lists") { - t.Errorf("errors = %q, want the blocklist complaint", joined) + if !strings.Contains(joined, "acl.default_policy must be") { + t.Errorf("errors = %q, want the default_policy complaint", joined) } // fieldOf falls back to "config" for a message with no dotted path, // which is what this one is. Pinned so the editor's field highlighting diff --git a/internal/config/config.go b/internal/config/config.go index 69c9534..8462565 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1152,12 +1152,10 @@ func (c *Config) Validate() error { add("blocklist.lists[%d] %q: refresh must not be negative", i, l.Name) } } - if c.Block.Enabled { - if len(c.Block.Lists) == 0 && len(c.Block.LocalZones) == 0 && - len(c.Block.BlockEntries) == 0 && len(c.Block.AllowEntries) == 0 { - add("blocklist is enabled but no lists, local_zones or inline entries are configured") - } - } + // An enabled blocklist with nothing configured is not an error: BlockList, + // DomainRule and AddressOverride objects can supply every rule, and the + // config file cannot know about them. configWarnings says so for a + // standalone config. switch c.ACL.DefaultPolicy { case "allow", "deny", "block_only":