The runtime can now take ClientGroup, BlockList and DNSZone objects (fancydns.k6n.net/v1alpha1) from a cluster with -kube, from a pod's service account or a kubeconfig alike. internal/kubesource watches them with dynamic informers, converts and validates each object on its own, and publishes an immutable snapshot. A bad object is skipped and keeps its last good version, so one broken edit cannot remove a running policy; an invalid merged config is dropped whole. The last snapshot is saved so a restart without apiserver access still serves. config.Store gains an overlay: Base() is what the YAML says, Get() is base plus cluster objects. Everything that writes the config back (mutateConfig, SetAdminAddr, the settings editor) now starts from Base(), otherwise cluster objects would be copied into the file and shadow their own objects. YAML wins every name collision. putACL and toggleBlocklist refuse to edit cluster-owned objects. BlockList has no path field and refuses file:// URLs so a namespaced object cannot pick files the runtime reads or writes. DNSZone is rendered through the zone writer's own prove-by-reparse step, with the SOA serial taken from metadata.generation so every replica renders identical bytes. qlog: Flush reset the pending slice to [:0], so entries logged while it was still storing the batch were appended over the same backing array. The race detector found it through the new Kubernetes integration test. pending is now set to nil, with a regression test that fails without the change. CRD manifests, RBAC and examples are in deploy/k8s/crds; tests pin the schemas to the Go spec structs so the two cannot drift. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
84 lines
3.6 KiB
Go
84 lines
3.6 KiB
Go
// Package kubesource turns fancy-dns custom resources (DNSZone, ClientGroup,
|
|
// BlockList) into the runtime's own configuration types. It is read-only: the
|
|
// runtime watches the cluster and never writes to it.
|
|
//
|
|
// Objects are decoded from unstructured data into the plain structs below, so
|
|
// there is no generated clientset or deepcopy code to keep in step with the
|
|
// CRD manifests in deploy/k8s/crds.
|
|
package kubesource
|
|
|
|
import (
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
"git.k8s-git-k6n.net/mats/fancy-dns/internal/config"
|
|
)
|
|
|
|
// APIGroup and Version name the custom resources.
|
|
const (
|
|
APIGroup = "fancydns.k6n.net"
|
|
Version = "v1alpha1"
|
|
)
|
|
|
|
// The resources the runtime watches.
|
|
var (
|
|
ZoneGVR = schema.GroupVersionResource{Group: APIGroup, Version: Version, Resource: "dnszones"}
|
|
GroupGVR = schema.GroupVersionResource{Group: APIGroup, Version: Version, Resource: "clientgroups"}
|
|
ListGVR = schema.GroupVersionResource{Group: APIGroup, Version: Version, Resource: "blocklists"}
|
|
allGVRs = []schema.GroupVersionResource{ZoneGVR, GroupGVR, ListGVR}
|
|
kindNames = map[string]string{ZoneGVR.Resource: "DNSZone", GroupGVR.Resource: "ClientGroup", ListGVR.Resource: "BlockList"}
|
|
)
|
|
|
|
// DNSZoneSpec declares one authoritative zone. The zone's name is spec.zone,
|
|
// not metadata.name, because a DNS name is not always a legal object name.
|
|
type DNSZoneSpec struct {
|
|
Zone string `json:"zone"`
|
|
// TTL is the zone's $TTL and SOA minimum; 0 means the writer's default.
|
|
TTL uint32 `json:"ttl,omitempty"`
|
|
// SOA is the SOA rdata ("ns1.example.com. hostmaster.example.com. ..."),
|
|
// or empty to synthesise one from the first apex NS record.
|
|
SOA string `json:"soa,omitempty"`
|
|
Records []ZoneRecord `json:"records,omitempty"`
|
|
}
|
|
|
|
// ZoneRecord is one resource record. Value is presentation-format RDATA.
|
|
type ZoneRecord struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
TTL uint32 `json:"ttl,omitempty"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
// ClientGroupSpec mirrors config.Group. The group's name is metadata.name.
|
|
// Groups are evaluated first-match-wins, so Priority orders them: lower runs
|
|
// first, ties break on name.
|
|
type ClientGroupSpec struct {
|
|
Priority int32 `json:"priority,omitempty"`
|
|
Comment string `json:"comment,omitempty"`
|
|
Match config.Match `json:"match"`
|
|
Policy string `json:"policy"`
|
|
AllowDomains []string `json:"allow_domains,omitempty"`
|
|
DenyDomains []string `json:"deny_domains,omitempty"`
|
|
SkipBlocklist bool `json:"skip_blocklist,omitempty"`
|
|
SkipCache bool `json:"skip_cache,omitempty"`
|
|
RateLimit int `json:"rate_limit,omitempty"`
|
|
MaxConcurrent int `json:"max_concurrent,omitempty"`
|
|
LogQueries bool `json:"log_queries,omitempty"`
|
|
Upstreams []string `json:"upstreams,omitempty"`
|
|
}
|
|
|
|
// BlockListSpec mirrors config.ListSource for a remote list. There is no path
|
|
// field on purpose: a namespace-scoped object must not be able to name a file
|
|
// the runtime reads or writes. file:// URLs are refused for the same reason.
|
|
type BlockListSpec struct {
|
|
URL string `json:"url"`
|
|
Format string `json:"format,omitempty"`
|
|
Mirror []string `json:"mirror,omitempty"`
|
|
Disabled bool `json:"disabled,omitempty"`
|
|
Refresh string `json:"refresh,omitempty"`
|
|
Timeout string `json:"timeout,omitempty"`
|
|
MaxBytes int64 `json:"max_bytes,omitempty"`
|
|
Categories []string `json:"categories,omitempty"`
|
|
Comment string `json:"comment,omitempty"`
|
|
InsecureSkipTLSVerify bool `json:"insecure_skip_tls_verify,omitempty"`
|
|
}
|