1 parent
98b65945f6
commit
0b6dcbd547
13 files changed
+417
-68
No files matched your search
+11
-4
@@ -67,7 +67,8 @@ type Hooks struct {
|
||||
DomainCheck func(name string) (any, error)
|
||||
// Resolve runs one query through the live pipeline and reports the
|
||||
// outcome, so the console can test the server it is talking to.
|
||||
Resolve func(ctx context.Context, name, qtype string) (any, error)
|
||||
Resolve func(ctx context.Context, name, qtype string) (any, error)
|
||||
ResolveUpstream func(ctx context.Context, name, qtype, upstream string) (any, error)
|
||||
// ListDomains pages the compiled rules.
|
||||
ListDomains func(q DomainQuery) (any, error)
|
||||
// AddDomain adds an inline rule and persists it.
|
||||
@@ -75,9 +76,12 @@ type Hooks struct {
|
||||
// DeleteDomain removes a rule and persists the change.
|
||||
DeleteDomain func(ctx context.Context, name, action, list string) error
|
||||
// LocalZones lists, saves and deletes UI-managed DNS master files.
|
||||
LocalZones func(ctx context.Context) ([]ZoneFile, error)
|
||||
SaveLocalZone func(ctx context.Context, name, content string) error
|
||||
DeleteLocalZone func(ctx context.Context, name string) error
|
||||
LocalZones func(ctx context.Context) ([]ZoneFile, error)
|
||||
SaveLocalZone func(ctx context.Context, name, content string) error
|
||||
DeleteLocalZone func(ctx context.Context, name string) error
|
||||
AddressOverrides func(ctx context.Context) (any, error)
|
||||
SaveAddressOverride func(ctx context.Context, domain, address string) error
|
||||
DeleteAddressOverride func(ctx context.Context, domain string) error
|
||||
// ListZones returns the compiled local zones with their records and
|
||||
// delegations, and whether the API may rewrite them.
|
||||
ListZones func() (any, error)
|
||||
@@ -290,6 +294,9 @@ func (s *Server) routes(cfg *config.Config) *http.ServeMux {
|
||||
mux.Handle("DELETE "+base+"domains/{name...}", s.authed(s.handleDomainDelete, true))
|
||||
mux.Handle("GET "+base+"domains/check", s.authed(s.handleDomainCheck, false))
|
||||
mux.Handle("POST "+base+"dns/test", s.authed(s.handleDNSTest, false))
|
||||
mux.Handle("GET "+base+"address-overrides", s.authed(s.handleAddressOverrides, false))
|
||||
mux.Handle("PUT "+base+"address-overrides", s.authed(s.handleAddressOverridePut, true))
|
||||
mux.Handle("DELETE "+base+"address-overrides/{domain}", s.authed(s.handleAddressOverrideDelete, true))
|
||||
mux.Handle("GET "+base+"local-zones", s.authed(s.handleLocalZones, false))
|
||||
mux.Handle("PUT "+base+"local-zones/{name}", s.authed(s.handleLocalZonePut, true))
|
||||
mux.Handle("DELETE "+base+"local-zones/{name}", s.authed(s.handleLocalZoneDelete, true))
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -188,8 +190,9 @@ func (s *Server) handleDNSTest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
Name string `json:"name"`
|
||||
QType string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
QType string `json:"type"`
|
||||
Upstream string `json:"upstream"`
|
||||
}
|
||||
if err := decodeJSON(r, &in); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "malformed request body")
|
||||
@@ -200,7 +203,20 @@ func (s *Server) handleDNSTest(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
v, err := s.hooks.Resolve(r.Context(), name, in.QType)
|
||||
resolve := s.hooks.Resolve
|
||||
if in.Upstream != "" {
|
||||
resolve = func(ctx context.Context, n, qt string) (any, error) {
|
||||
if s.hooks.ResolveUpstream == nil {
|
||||
return nil, errors.New("direct upstream lookup unavailable")
|
||||
}
|
||||
return s.hooks.ResolveUpstream(ctx, n, qt, in.Upstream)
|
||||
}
|
||||
}
|
||||
if resolve == nil {
|
||||
writeError(w, http.StatusNotImplemented, "dns test not available")
|
||||
return
|
||||
}
|
||||
v, err := resolve(r.Context(), name, in.QType)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"name": name, "error": err.Error()})
|
||||
return
|
||||
@@ -208,6 +224,50 @@ func (s *Server) handleDNSTest(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, v)
|
||||
}
|
||||
|
||||
func (s *Server) handleAddressOverrides(w http.ResponseWriter, r *http.Request) {
|
||||
if s.hooks.AddressOverrides == nil {
|
||||
writeError(w, http.StatusNotImplemented, "address overrides unavailable")
|
||||
return
|
||||
}
|
||||
v, err := s.hooks.AddressOverrides(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, v)
|
||||
}
|
||||
func (s *Server) handleAddressOverridePut(w http.ResponseWriter, r *http.Request) {
|
||||
if s.hooks.SaveAddressOverride == nil {
|
||||
writeError(w, http.StatusNotImplemented, "address overrides are read-only")
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
Domain string `json:"domain"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
if decodeJSON(r, &in) != nil {
|
||||
writeError(w, http.StatusBadRequest, "malformed request body")
|
||||
return
|
||||
}
|
||||
if err := s.hooks.SaveAddressOverride(r.Context(), in.Domain, in.Address); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
func (s *Server) handleAddressOverrideDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if s.hooks.DeleteAddressOverride == nil {
|
||||
writeError(w, http.StatusNotImplemented, "address overrides are read-only")
|
||||
return
|
||||
}
|
||||
domain := r.PathValue("domain")
|
||||
if err := s.hooks.DeleteAddressOverride(r.Context(), domain); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleLocalZones(w http.ResponseWriter, r *http.Request) {
|
||||
if s.hooks.LocalZones == nil {
|
||||
writeError(w, http.StatusNotImplemented, "local zone editor is unavailable")
|
||||
|
||||
+25
-6
@@ -56,12 +56,13 @@ type App struct {
|
||||
cfg *config.Store
|
||||
ver string
|
||||
|
||||
blocks *blocklist.Manager
|
||||
cache *cache.Manager
|
||||
cstore *cache.Store
|
||||
logger *qlog.Logger
|
||||
aclH *acl.Holder
|
||||
local *localzone.Holder
|
||||
blocks *blocklist.Manager
|
||||
overrideMu sync.Mutex
|
||||
cache *cache.Manager
|
||||
cstore *cache.Store
|
||||
logger *qlog.Logger
|
||||
aclH *acl.Holder
|
||||
local *localzone.Holder
|
||||
// zones is the Kubernetes-sourced zone client.
|
||||
zones *kubezones.Client
|
||||
// managedZones is the console-managed zone directory: the only zone data
|
||||
@@ -316,6 +317,23 @@ func (a *App) startBlocklist() {
|
||||
Config: a.cfg,
|
||||
Logger: a.log,
|
||||
})
|
||||
if a.zones != nil {
|
||||
ctx := context.Background()
|
||||
content, exists, err := a.zones.Data(ctx, "address-overrides.rpz")
|
||||
if err == nil && !exists {
|
||||
if seed, readErr := os.ReadFile("/etc/fancy-dns/rpz/address-overrides.rpz"); readErr == nil {
|
||||
content = string(seed)
|
||||
if saveErr := a.zones.SaveData(ctx, "address-overrides.rpz", content); saveErr != nil {
|
||||
a.log.Warn("seed address overrides failed", "error", saveErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
a.blocks.SetManagedRPZ(content)
|
||||
} else {
|
||||
a.log.Warn("load address overrides failed", "error", err)
|
||||
}
|
||||
}
|
||||
// Compile once before serving so the first queries are not unprotected.
|
||||
if _, err := a.blocks.RefreshAll(context.Background()); err != nil {
|
||||
a.markDegraded("blocklist: " + err.Error())
|
||||
@@ -576,6 +594,7 @@ func (a *App) startCluster(ctx context.Context, cfg *config.Config) {
|
||||
// probes and the metrics gauge.
|
||||
func (a *App) startBackground(ctx context.Context, cfg *config.Config) {
|
||||
go a.watchLocalZones(ctx)
|
||||
go a.watchAddressOverrides(ctx)
|
||||
// Cache snapshots.
|
||||
if a.cstore != nil {
|
||||
interval := cfg.Cache.Persist.Interval.D()
|
||||
|
||||
+164
-38
@@ -26,47 +26,173 @@ import (
|
||||
// components. Everything the UI can read or change goes through here.
|
||||
func (a *App) hooks() api.Hooks {
|
||||
return api.Hooks{
|
||||
Store: a.cfg,
|
||||
Logger: a.log,
|
||||
Version: a.ver,
|
||||
StartedAt: a.startedAt,
|
||||
Dashboard: a.dashboard,
|
||||
Status: a.status,
|
||||
Health: a.health,
|
||||
BlocklistState: a.blocklistState,
|
||||
BlocklistStats: a.blocklistStats,
|
||||
RefreshBlocklists: a.refreshBlocklists,
|
||||
ToggleBlocklist: a.toggleBlocklist,
|
||||
DomainCheck: a.domainCheck,
|
||||
Resolve: a.resolveTest,
|
||||
ListDomains: a.listDomains,
|
||||
AddDomain: a.addDomain,
|
||||
DeleteDomain: a.deleteDomain,
|
||||
ListZones: a.listZones,
|
||||
AddZoneRecord: a.addZoneRecord,
|
||||
PutZoneRecord: a.putZoneRecord,
|
||||
DeleteZoneRecord: a.deleteZoneRecord,
|
||||
DeleteZone: a.deleteZone,
|
||||
AddZoneDelegation: a.addZoneDelegation,
|
||||
DeleteZoneDelegation: a.deleteZoneDelegation,
|
||||
LocalZones: a.listLocalZones,
|
||||
SaveLocalZone: a.saveLocalZone,
|
||||
DeleteLocalZone: a.deleteLocalZone,
|
||||
GetACL: a.getACL,
|
||||
PutACL: a.putACL,
|
||||
GetUpstream: a.getUpstreams,
|
||||
PutUpstream: a.putUpstream,
|
||||
SearchLog: a.searchLog,
|
||||
LogStats: a.logStats,
|
||||
SubscribeLog: a.subscribeLog,
|
||||
ClearLog: a.clearLog,
|
||||
CacheStats: a.cacheStats,
|
||||
CacheTop: a.cacheTop,
|
||||
FlushCache: a.flushCache,
|
||||
Flush: a.flush,
|
||||
Store: a.cfg,
|
||||
Logger: a.log,
|
||||
Version: a.ver,
|
||||
StartedAt: a.startedAt,
|
||||
Dashboard: a.dashboard,
|
||||
Status: a.status,
|
||||
Health: a.health,
|
||||
BlocklistState: a.blocklistState,
|
||||
BlocklistStats: a.blocklistStats,
|
||||
RefreshBlocklists: a.refreshBlocklists,
|
||||
ToggleBlocklist: a.toggleBlocklist,
|
||||
DomainCheck: a.domainCheck,
|
||||
Resolve: a.resolveTest,
|
||||
ResolveUpstream: a.resolveTestUpstream,
|
||||
AddressOverrides: a.listAddressOverrides,
|
||||
SaveAddressOverride: a.saveAddressOverride,
|
||||
DeleteAddressOverride: a.deleteAddressOverride,
|
||||
ListDomains: a.listDomains,
|
||||
AddDomain: a.addDomain,
|
||||
DeleteDomain: a.deleteDomain,
|
||||
ListZones: a.listZones,
|
||||
AddZoneRecord: a.addZoneRecord,
|
||||
PutZoneRecord: a.putZoneRecord,
|
||||
DeleteZoneRecord: a.deleteZoneRecord,
|
||||
DeleteZone: a.deleteZone,
|
||||
AddZoneDelegation: a.addZoneDelegation,
|
||||
DeleteZoneDelegation: a.deleteZoneDelegation,
|
||||
LocalZones: a.listLocalZones,
|
||||
SaveLocalZone: a.saveLocalZone,
|
||||
DeleteLocalZone: a.deleteLocalZone,
|
||||
GetACL: a.getACL,
|
||||
PutACL: a.putACL,
|
||||
GetUpstream: a.getUpstreams,
|
||||
PutUpstream: a.putUpstream,
|
||||
SearchLog: a.searchLog,
|
||||
LogStats: a.logStats,
|
||||
SubscribeLog: a.subscribeLog,
|
||||
ClearLog: a.clearLog,
|
||||
CacheStats: a.cacheStats,
|
||||
CacheTop: a.cacheTop,
|
||||
FlushCache: a.flushCache,
|
||||
Flush: a.flush,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) resolveTestUpstream(ctx context.Context, name, qtype, upstream string) (any, error) {
|
||||
if a.pool == nil {
|
||||
return nil, errors.New("upstream pool is not running")
|
||||
}
|
||||
up := a.pool.Get(strings.TrimSpace(upstream))
|
||||
if up == nil {
|
||||
return nil, fmt.Errorf("unknown upstream %q", upstream)
|
||||
}
|
||||
qt := dns.TypeA
|
||||
if qtype != "" {
|
||||
var ok bool
|
||||
qt, ok = dns.StringToType[strings.ToUpper(strings.TrimSpace(qtype))]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown record type %q", qtype)
|
||||
}
|
||||
}
|
||||
q := new(dns.Msg)
|
||||
q.SetQuestion(dns.Fqdn(name), qt)
|
||||
q.RecursionDesired = true
|
||||
started := time.Now()
|
||||
resp, addr, err := a.pool.Exchange(ctx, q, []string{up.Addr()}, 5*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]any{"name": dns.Fqdn(name), "type": dns.TypeToString[qt], "rcode": "SERVFAIL", "action": "upstream", "duration_ms": time.Since(started).Milliseconds(), "upstream": addr, "group": "", "cache": "bypass", "local": false, "truncated": false, "blocked": false, "block_source": "", "answers": []string{}}
|
||||
if resp != nil {
|
||||
out["rcode"] = dns.RcodeToString[resp.Rcode]
|
||||
ans := []string{}
|
||||
for _, rr := range resp.Answer {
|
||||
ans = append(ans, strings.TrimSpace(rr.String()))
|
||||
}
|
||||
out["answers"] = ans
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type addressOverride struct {
|
||||
Domain string `json:"domain"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
func (a *App) listAddressOverrides(ctx context.Context) (any, error) {
|
||||
if a.zones == nil {
|
||||
return map[string]any{"items": []addressOverride{}, "writable": false}, nil
|
||||
}
|
||||
raw, ok, err := a.zones.Data(ctx, "address-overrides.rpz")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := parseAddressOverrides(raw)
|
||||
return map[string]any{"items": items, "writable": true, "initialized": ok}, nil
|
||||
}
|
||||
func parseAddressOverrides(raw string) []addressOverride {
|
||||
out := []addressOverride{}
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) >= 3 && !strings.HasPrefix(f[0], "$") && (strings.EqualFold(f[1], "A") || strings.EqualFold(f[1], "AAAA")) {
|
||||
out = append(out, addressOverride{Domain: strings.TrimSuffix(f[0], "."), Address: f[2]})
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Domain < out[j].Domain })
|
||||
return out
|
||||
}
|
||||
func (a *App) saveAddressOverride(ctx context.Context, domain, address string) error {
|
||||
return a.mutateAddressOverride(ctx, domain, address, false)
|
||||
}
|
||||
func (a *App) deleteAddressOverride(ctx context.Context, domain string) error {
|
||||
return a.mutateAddressOverride(ctx, domain, "", true)
|
||||
}
|
||||
func (a *App) mutateAddressOverride(ctx context.Context, domain, address string, remove bool) error {
|
||||
a.overrideMu.Lock()
|
||||
defer a.overrideMu.Unlock()
|
||||
if a.zones == nil {
|
||||
return errors.New("address overrides are writable only in Kubernetes")
|
||||
}
|
||||
domain = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(domain)), ".")
|
||||
if _, ok := dns.IsDomainName(dns.Fqdn(domain)); !ok || domain == "" {
|
||||
return errors.New("enter a valid domain name")
|
||||
}
|
||||
ip, err := netip.ParseAddr(strings.TrimSpace(address))
|
||||
if !remove && err != nil {
|
||||
return errors.New("enter a valid IPv4 or IPv6 address")
|
||||
}
|
||||
raw, _, err := a.zones.Data(ctx, "address-overrides.rpz")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := parseAddressOverrides(raw)
|
||||
next := make([]addressOverride, 0, len(items)+1)
|
||||
for _, it := range items {
|
||||
if it.Domain != domain {
|
||||
next = append(next, it)
|
||||
}
|
||||
}
|
||||
if !remove {
|
||||
next = append(next, addressOverride{Domain: domain, Address: ip.String()})
|
||||
}
|
||||
sort.Slice(next, func(i, j int) bool { return next[i].Domain < next[j].Domain })
|
||||
var b strings.Builder
|
||||
b.WriteString("$TTL 60\n")
|
||||
for _, it := range next {
|
||||
typ := "A"
|
||||
if strings.Contains(it.Address, ":") {
|
||||
typ = "AAAA"
|
||||
}
|
||||
fmt.Fprintf(&b, "%s %s %s\n", it.Domain, typ, it.Address)
|
||||
}
|
||||
if err := a.zones.SaveData(ctx, "address-overrides.rpz", b.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
if a.blocks != nil {
|
||||
a.blocks.SetManagedRPZ(b.String())
|
||||
if _, err := a.blocks.RefreshAll(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if a.cache != nil {
|
||||
a.cache.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) listLocalZones(ctx context.Context) ([]api.ZoneFile, error) {
|
||||
if a.zones == nil {
|
||||
return nil, fmt.Errorf("local zone editor is available only in Kubernetes")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (a *App) watchAddressOverrides(ctx context.Context) {
|
||||
if a.zones == nil || a.blocks == nil {
|
||||
return
|
||||
}
|
||||
t := time.NewTicker(2 * time.Second)
|
||||
defer t.Stop()
|
||||
var last string
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
content, ok, err := a.zones.Data(ctx, "address-overrides.rpz")
|
||||
if err != nil || !ok || content == last {
|
||||
continue
|
||||
}
|
||||
a.overrideMu.Lock()
|
||||
a.blocks.SetManagedRPZ(content)
|
||||
if _, err := a.blocks.RefreshAll(ctx); err == nil {
|
||||
if a.cache != nil {
|
||||
a.cache.Flush()
|
||||
}
|
||||
last = content
|
||||
}
|
||||
a.overrideMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,10 @@ type Manager struct {
|
||||
http *http.Client
|
||||
stats *StateHolder
|
||||
|
||||
mu sync.Mutex
|
||||
running map[string]bool // in-flight refreshes, by source name
|
||||
mu sync.Mutex
|
||||
running map[string]bool // in-flight refreshes, by source name
|
||||
managedMu sync.RWMutex
|
||||
managedRPZ string
|
||||
|
||||
// trigger lets the cluster ask one instance to refresh (leader election).
|
||||
trigger chan string
|
||||
@@ -126,6 +128,13 @@ func (m *Manager) Holder() *Holder { return m.hold }
|
||||
// State returns the detailed state the API serves.
|
||||
func (m *Manager) State() *State { return m.stats.Get() }
|
||||
|
||||
// SetManagedRPZ replaces the shared UI-managed address override source.
|
||||
func (m *Manager) SetManagedRPZ(content string) {
|
||||
m.managedMu.Lock()
|
||||
m.managedRPZ = content
|
||||
m.managedMu.Unlock()
|
||||
}
|
||||
|
||||
// RefreshAll compiles every enabled source plus the local zones. The previous
|
||||
// set is kept if every source fails, so a bad deploy never leaves the
|
||||
// resolver with no blocklists at all.
|
||||
@@ -137,6 +146,20 @@ func (m *Manager) RefreshAll(ctx context.Context) (State, error) {
|
||||
sources []Source
|
||||
errs = map[string]string{}
|
||||
)
|
||||
m.managedMu.RLock()
|
||||
managedRPZ := m.managedRPZ
|
||||
m.managedMu.RUnlock()
|
||||
if strings.TrimSpace(managedRPZ) != "" {
|
||||
rules, st, err := Parse(FormatRPZ, []byte(managedRPZ), ParseOptions{Subdomains: true, DefaultTTL: 60})
|
||||
if err != nil {
|
||||
errs["managed:address-overrides"] = err.Error()
|
||||
} else {
|
||||
for _, r := range rules {
|
||||
b.Add(r, "managed:address-overrides", nil)
|
||||
}
|
||||
b.NoteSource(SourceMeta{Name: "managed:address-overrides", Format: string(FormatRPZ), Rules: len(rules), Bytes: st.Bytes, UpdatedAt: start, Duration: config.Duration(time.Since(start)), Enabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
// Inline block/allow entries first: they are cheap, and putting them
|
||||
// before the lists means a later list cannot override an operator's
|
||||
|
||||
@@ -42,6 +42,16 @@ type Client struct {
|
||||
// InCluster returns nil outside Kubernetes. The projected service account
|
||||
// token must be available; RBAC limits it to the fancy-dns-zones ConfigMap.
|
||||
func InCluster() (*Client, error) {
|
||||
name := os.Getenv("FANCYDNS_ZONE_CONFIGMAP")
|
||||
if name == "" {
|
||||
name = defaultName
|
||||
}
|
||||
return InClusterConfigMap(name)
|
||||
}
|
||||
|
||||
// InClusterConfigMap returns a client for a named ConfigMap. RBAC on the
|
||||
// service account still limits which ConfigMaps it can read or patch.
|
||||
func InClusterConfigMap(name string) (*Client, error) {
|
||||
host := os.Getenv("KUBERNETES_SERVICE_HOST")
|
||||
if host == "" {
|
||||
return nil, nil
|
||||
@@ -69,9 +79,8 @@ func InCluster() (*Client, error) {
|
||||
if !pool.AppendCertsFromPEM(ca) {
|
||||
return nil, errors.New("kube zones: invalid service account CA")
|
||||
}
|
||||
name := os.Getenv("FANCYDNS_ZONE_CONFIGMAP")
|
||||
if name == "" {
|
||||
name = defaultName
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return nil, errors.New("kube zones: ConfigMap name is required")
|
||||
}
|
||||
return &Client{
|
||||
baseURL: "https://" + net.JoinHostPort(host, port),
|
||||
@@ -119,14 +128,47 @@ func (c *Client) Delete(ctx context.Context, name string) error {
|
||||
return c.change(ctx, filename(name), nil)
|
||||
}
|
||||
|
||||
// Data reads one arbitrary ConfigMap data key. It is used for console-owned
|
||||
// shared files that are not zone master files.
|
||||
func (c *Client) Data(ctx context.Context, key string) (string, bool, error) {
|
||||
if c == nil {
|
||||
return "", false, errors.New("kube zones: Kubernetes ConfigMap is unavailable")
|
||||
}
|
||||
cm, _, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
value, ok := cm.Data[key]
|
||||
return value, ok, nil
|
||||
}
|
||||
|
||||
// SaveData creates or replaces one arbitrary ConfigMap data key, retrying
|
||||
// resource-version conflicts so independent edits to different keys merge.
|
||||
func (c *Client) SaveData(ctx context.Context, key, content string) error {
|
||||
if c == nil {
|
||||
return errors.New("kube zones: Kubernetes ConfigMap is unavailable")
|
||||
}
|
||||
if key == "" || len(key) > 253 || strings.ContainsAny(key, `/\\`) {
|
||||
return errors.New("kube zones: invalid ConfigMap data key")
|
||||
}
|
||||
if len(content) > 512*1024 {
|
||||
return errors.New("kube zones: ConfigMap data exceeds 512 KiB")
|
||||
}
|
||||
return c.changeData(ctx, key, &content)
|
||||
}
|
||||
|
||||
func filename(name string) string { return "zone--" + strings.TrimSuffix(name, ".") + ".zone" }
|
||||
|
||||
func (c *Client) change(ctx context.Context, name string, content *string) error {
|
||||
if !validKey(name) {
|
||||
return errors.New("zone filename must be a DNS name ending in .zone")
|
||||
}
|
||||
return c.changeData(ctx, name, content)
|
||||
}
|
||||
|
||||
func (c *Client) changeData(ctx context.Context, name string, content *string) error {
|
||||
if content != nil && len(*content) > 512*1024 {
|
||||
return errors.New("zone file exceeds 512 KiB")
|
||||
return errors.New("ConfigMap data exceeds 512 KiB")
|
||||
}
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
_, rv, err := c.get(ctx)
|
||||
|
||||
Vendored
+2
-2
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>web</title>
|
||||
<script type="module" crossorigin src="/assets/index-CzcgOHn-.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-FxcuHhfL.css">
|
||||
<script type="module" crossorigin src="/assets/index-DnNommMg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bf5OWE1r.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in new issue
Block a user