Merge pull request 'config: drop estate-specific defaults and docs' (#7) from benvin/generic-config into main
Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
+2
-3
@@ -1,6 +1,5 @@
|
||||
# Container image for pdbmux, the merging PuppetDB proxy daemon. pdbmux is a
|
||||
# k8s-only service (deployed via argocd-apps), so it ships as a distroless
|
||||
# static image rather than an RPM.
|
||||
# Container image for pdbmux, the merging PuppetDB proxy daemon. It ships only
|
||||
# as a distroless static image; there is no OS package.
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
# pdbmux — merging PuppetDB proxy
|
||||
|
||||
`pdbmux` is a small HTTP daemon that fronts **two** PuppetDB backends and serves
|
||||
a single, merged PuppetDB v4 query surface on one address. Point `node-lookup`,
|
||||
`pblastreport`, Puppetboard, or anything else at `pdbmux` instead of a raw
|
||||
PuppetDB and it sees one consistent view spanning both.
|
||||
`pdbmux` is a small HTTP daemon that fronts **several** PuppetDB backends and
|
||||
serves a single, merged PuppetDB v4 query surface on one address. Point
|
||||
Puppetboard, or any other PuppetDB API client, at `pdbmux` instead of a raw
|
||||
PuppetDB and it sees one consistent view spanning all of them.
|
||||
|
||||
## Why
|
||||
|
||||
During the VM→k8s Puppet migration there are two PuppetDBs:
|
||||
Running more than one PuppetDB — during a migration between two of them, or
|
||||
across regions — means a given node's current data lives in exactly one at any
|
||||
moment, and consumers have to know which, or query each in turn. Consider two
|
||||
backends being merged during a migration:
|
||||
|
||||
- **old** — the legacy Consul-registered `http://puppetdbapi.service.consul:8080`
|
||||
- **new** — the k8s `https://puppetdb.k8s.syd1.au.unkin.net` (TLS terminated at
|
||||
the gateway; backends are plain PuppetDB on 8080)
|
||||
- **old** — the PuppetDB nodes are moving off, e.g. `http://puppetdb1.example.com:8080`
|
||||
- **new** — the PuppetDB nodes are moving on to, e.g. `http://puppetdb2.example.com:8080`
|
||||
|
||||
Nodes move from old to new as they migrate, so at any moment a given node's
|
||||
current data lives in exactly one of them. `pdbmux` merges both so consumers
|
||||
Nodes move from old to new as they migrate. `pdbmux` merges both so consumers
|
||||
don't have to know (or query twice) which PuppetDB a node currently lives in.
|
||||
The backend names are arbitrary labels; there is no fixed number of backends.
|
||||
|
||||
## Endpoints
|
||||
|
||||
@@ -24,16 +26,16 @@ not PQL) is forwarded verbatim.
|
||||
|
||||
| Path | Behaviour |
|
||||
|---|---|
|
||||
| `GET /pdb/query/v4/nodes` | Fan out to both backends, dedupe by `certname`, keep the record with the newer `report_timestamp`. |
|
||||
| `GET /pdb/query/v4/facts` | Fan out to both, and per `certname` keep **all** facts from the backend that owns that node (see merge semantics). |
|
||||
| `GET /pdb/query/v4/reports` | Fan out to both and serve the **union**, deduped by report `hash`, re-ordered and re-paged across the two backends. |
|
||||
| `GET /pdb/query/v4/events` | Fan out to both and serve the **union**, deduped by record identity, re-ordered and re-paged. |
|
||||
| `GET /pdb/query/v4/reports/<hash>/{events,logs,metrics}` | Ask both; serve the answer from whichever backend actually holds that report. `404` when neither does. |
|
||||
| `GET /pdb/query/v4/nodes` | Fan out to all backends, dedupe by `certname`, keep the record with the newer `report_timestamp`. |
|
||||
| `GET /pdb/query/v4/facts` | Fan out to all, and per `certname` keep **all** facts from the backend that owns that node (see merge semantics). |
|
||||
| `GET /pdb/query/v4/reports` | Fan out to all and serve the **union**, deduped by report `hash`, re-ordered and re-paged across backends. |
|
||||
| `GET /pdb/query/v4/events` | Fan out to all and serve the **union**, deduped by record identity, re-ordered and re-paged. |
|
||||
| `GET /pdb/query/v4/reports/<hash>/{events,logs,metrics}` | Ask every backend; serve the answer from whichever backend actually holds that report. `404` when neither does. |
|
||||
| `GET /pdb/query/v4/*` (any other) | Transparently proxied to the **primary** backend, unmerged, streamed verbatim. |
|
||||
| `GET /healthz` | Per-backend reachability. `200 {"status":"ok"}` if all reachable, `200 degraded` if some fail, `503 down` if all fail. |
|
||||
|
||||
Fan-out is concurrent. If one backend errors or times out, `pdbmux` serves the
|
||||
survivor's results and logs a warning; a merged endpoint only returns `502` when
|
||||
surviving backends' results and logs a warning; a merged endpoint only returns `502` when
|
||||
**every** backend fails. Response records are passed through as raw JSON so
|
||||
unknown fields survive untouched.
|
||||
|
||||
@@ -42,12 +44,12 @@ unknown fields survive untouched.
|
||||
- **`/nodes`** — dedupe by `certname`; the record with the strictly-newer
|
||||
`report_timestamp` wins. On a tie (or when a node exists in only one backend),
|
||||
the **preferred** backend's record is kept.
|
||||
- **`/facts`** — node-level granularity. For a `certname` present in both
|
||||
backends, `pdbmux` keeps **all** of that node's facts from **one** backend and
|
||||
- **`/facts`** — node-level granularity. For a `certname` present in more than
|
||||
one backend, `pdbmux` keeps **all** of that node's facts from **one** backend and
|
||||
drops the other's, chosen by the merge strategy:
|
||||
- **`freshness`** (default) — attribute each `certname` to whichever backend
|
||||
holds its newer `report_timestamp`. `pdbmux` derives this from a per-certname
|
||||
freshness map built by querying `/nodes` from both backends, cached for
|
||||
freshness map built by querying `/nodes` from every backend, cached for
|
||||
`freshness_ttl` (default 30s). Ties/fallbacks use `prefer`.
|
||||
- **`static`** — always keep the `prefer` backend's facts for shared nodes.
|
||||
No extra `/nodes` query.
|
||||
@@ -81,18 +83,22 @@ Each backend applies `order_by`/`limit`/`offset` to its own slice only, so
|
||||
|
||||
Precedence (lowest → highest): **defaults < config file < env vars (`PDBMUX_*`) < flags**.
|
||||
|
||||
Config file: `$XDG_CONFIG_HOME/pdbmux/config.yaml`. In Kubernetes, configuration
|
||||
is supplied entirely via `PDBMUX_*` env vars (no config file), which is the
|
||||
supported deployment path — see [Deployment](#deployment).
|
||||
Config file: `$XDG_CONFIG_HOME/pdbmux/config.yaml`. In a container, configuration
|
||||
is supplied entirely via `PDBMUX_*` env vars (no config file) — see
|
||||
[Deployment](#deployment).
|
||||
|
||||
**`backends` has no default.** `pdbmux` refuses to start until at least one
|
||||
backend is configured, via the config file or `PDBMUX_BACKENDS`. `primary` and
|
||||
`prefer` default to the first configured backend.
|
||||
|
||||
```yaml
|
||||
# ~/.config/pdbmux/config.yaml (local dev; in k8s use PDBMUX_* env instead)
|
||||
# ~/.config/pdbmux/config.yaml (local dev; in a container use PDBMUX_* env instead)
|
||||
listen: ":8080"
|
||||
backends:
|
||||
- name: old
|
||||
url: http://puppetdbapi.service.consul:8080
|
||||
url: http://puppetdb1.example.com:8080
|
||||
- name: new
|
||||
url: https://puppetdb.k8s.syd1.au.unkin.net
|
||||
url: https://puppetdb2.example.com
|
||||
primary: new # backend used for non-merged /pdb/query/v4/* pass-through
|
||||
merge: freshness # freshness | static
|
||||
prefer: new # winner on ties / static merge / fallback
|
||||
@@ -120,16 +126,18 @@ Flags: `--listen`, `--primary`, `--merge`.
|
||||
```bash
|
||||
pdbmux # start the proxy (serve is the default action)
|
||||
pdbmux serve # explicit
|
||||
pdbmux config init # write a default config file
|
||||
pdbmux config init # write an example config file to edit
|
||||
pdbmux config show # print active config after all overrides
|
||||
pdbmux version
|
||||
```
|
||||
|
||||
Point a consumer at it:
|
||||
Point a consumer at it — any PuppetDB v4 client works, it just needs the
|
||||
`pdbmux` base URL in place of a PuppetDB one. For Puppetboard, that is its
|
||||
PuppetDB host/port setting; for a raw query:
|
||||
|
||||
```bash
|
||||
node-lookup --url http://localhost:8080/pdb/query/v4/facts -R
|
||||
NODE_LOOKUP_URL=http://localhost:8080/pdb/query/v4/facts pblastreport somehost
|
||||
curl -s --get http://localhost:8080/pdb/query/v4/nodes \
|
||||
--data-urlencode 'query=["=","certname","host1.example.com"]'
|
||||
```
|
||||
|
||||
## Build
|
||||
@@ -145,30 +153,31 @@ Requires Go 1.25+. Dependencies: `github.com/spf13/cobra` (CLI),
|
||||
|
||||
## Deployment
|
||||
|
||||
`pdbmux` runs **in Kubernetes** as a container, in line with the all-in-k8s
|
||||
estate direction — it is not shipped as a per-VM RPM/systemd service. The image
|
||||
is built and pushed on every `v*` tag (`.woodpecker/docker.yaml`) to:
|
||||
|
||||
```
|
||||
artifactapi.k8s.syd1.au.unkin.net/docker-internal/pdbmux:<tag>
|
||||
```
|
||||
`pdbmux` is distributed as a container image only — there is no OS package. The
|
||||
image is built and pushed on every `v*` tag (`.woodpecker/docker.yaml`); the
|
||||
registry and repository are pipeline settings, so point them at your own.
|
||||
|
||||
It is a minimal static (`CGO_ENABLED=0`) binary on a distroless base
|
||||
(`Dockerfile`), configured entirely via `PDBMUX_*` env vars, with a single HTTP
|
||||
listener and `/healthz` for liveness/readiness probes.
|
||||
listener and `/healthz` for liveness/readiness probes. It is stateless, so run
|
||||
as many replicas as you like behind an ordinary Service/Ingress.
|
||||
|
||||
The Deployment/Service/Gateway manifests live in the estate's `argocd-apps` repo
|
||||
under `apps/base/pdbmux/` (namespace `pdbmux`, 2 replicas), and it is exposed to
|
||||
VM/workstation `node-lookup` consumers over HTTPS at:
|
||||
A container needs at minimum `PDBMUX_BACKENDS`; everything else has a default:
|
||||
|
||||
```
|
||||
https://pdbmux.k8s.syd1.au.unkin.net
|
||||
```yaml
|
||||
env:
|
||||
- name: PDBMUX_BACKENDS
|
||||
value: "old=http://puppetdb1.example.com:8080,new=http://puppetdb2.example.com:8080"
|
||||
- name: PDBMUX_PRIMARY
|
||||
value: "new"
|
||||
- name: PDBMUX_PREFER
|
||||
value: "new"
|
||||
```
|
||||
|
||||
Locally you can still run the binary directly for development:
|
||||
Locally you can run the binary directly for development:
|
||||
|
||||
```bash
|
||||
PDBMUX_BACKENDS='old=http://puppetdbapi.service.consul:8080,new=http://puppetdb.puppet.svc.cluster.local:8080' \
|
||||
PDBMUX_BACKENDS='old=http://puppetdb1.example.com:8080,new=http://puppetdb2.example.com:8080' \
|
||||
pdbmux serve
|
||||
curl -s localhost:8080/healthz
|
||||
```
|
||||
|
||||
@@ -18,19 +18,18 @@ const (
|
||||
|
||||
// defaultListen is the default HTTP listen address.
|
||||
defaultListen = ":8080"
|
||||
// defaultOldURL / defaultNewURL are the two PuppetDBs merged during the
|
||||
// VM -> k8s migration. old = legacy Consul-registered puppetdbapi; new =
|
||||
// the k8s PuppetDB behind the gateway (TLS terminated there).
|
||||
defaultOldURL = "http://puppetdbapi.service.consul:8080"
|
||||
defaultNewURL = "https://puppetdb.k8s.syd1.au.unkin.net"
|
||||
// defaultPrimary is the backend name used for pass-through (non-merged)
|
||||
// /pdb/query/v4/* paths and as static precedence for merge fallback.
|
||||
defaultPrimary = "new"
|
||||
|
||||
defaultTimeout = 10 * time.Second
|
||||
defaultFreshnessTTL = 30 * time.Second
|
||||
)
|
||||
|
||||
// exampleBackends are the placeholder backends written by `config init`. They
|
||||
// are a scaffold to edit, not a working configuration.
|
||||
var exampleBackends = []Backend{
|
||||
{Name: "primary", URL: "http://puppetdb1.example.com:8080"},
|
||||
{Name: "secondary", URL: "http://puppetdb2.example.com:8080"},
|
||||
}
|
||||
|
||||
// Backend is one upstream PuppetDB. URL is the base URL (scheme://host[:port]),
|
||||
// without the /pdb/query/v4/... path — that is appended per request.
|
||||
type Backend struct {
|
||||
@@ -67,23 +66,28 @@ const (
|
||||
mergeStatic = "static"
|
||||
)
|
||||
|
||||
// DefaultConfig returns the built-in defaults: both migration PuppetDBs,
|
||||
// freshness merge, "new" primary/preferred.
|
||||
// DefaultConfig returns the built-in defaults. Backends have no default: they
|
||||
// must come from the config file or PDBMUX_BACKENDS. Primary and Prefer default
|
||||
// to the first configured backend (see normalize).
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Listen: defaultListen,
|
||||
Backends: []Backend{
|
||||
{Name: "old", URL: defaultOldURL},
|
||||
{Name: "new", URL: defaultNewURL},
|
||||
},
|
||||
Primary: defaultPrimary,
|
||||
Listen: defaultListen,
|
||||
Merge: mergeFreshness,
|
||||
Prefer: defaultPrimary,
|
||||
Timeout: defaultTimeout,
|
||||
FreshnessTTL: defaultFreshnessTTL,
|
||||
}
|
||||
}
|
||||
|
||||
// ExampleConfig returns DefaultConfig with placeholder backends filled in, as
|
||||
// written by `config init`.
|
||||
func ExampleConfig() Config {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Backends = append([]Backend(nil), exampleBackends...)
|
||||
cfg.Primary = exampleBackends[0].Name
|
||||
cfg.Prefer = exampleBackends[0].Name
|
||||
return cfg
|
||||
}
|
||||
|
||||
// ConfigDir returns the XDG_CONFIG_HOME/pdbmux directory.
|
||||
func ConfigDir() string {
|
||||
base := os.Getenv("XDG_CONFIG_HOME")
|
||||
@@ -103,6 +107,10 @@ func ConfigPath() string {
|
||||
// Precedence (lowest -> highest): defaults < config file < env vars < flags
|
||||
// (flags are applied by the caller). Backends can be overridden wholesale via
|
||||
// PDBMUX_BACKENDS ("name=url,name=url").
|
||||
//
|
||||
// Load reports only read/parse errors; the result is not validated, so commands
|
||||
// that do not serve (config init, version) still work on an unconfigured host.
|
||||
// Callers that serve must call Validate after applying flags.
|
||||
func Load() (Config, error) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
@@ -118,10 +126,7 @@ func Load() (Config, error) {
|
||||
}
|
||||
|
||||
applyEnv(&cfg, os.Getenv)
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
cfg.normalize()
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -175,10 +180,25 @@ func parseBackends(s string) []Backend {
|
||||
return out
|
||||
}
|
||||
|
||||
// normalize fills in values that default to something derived rather than
|
||||
// constant: Primary and Prefer both fall back to the first configured backend.
|
||||
func (c *Config) normalize() {
|
||||
if len(c.Backends) == 0 {
|
||||
return
|
||||
}
|
||||
if c.Primary == "" {
|
||||
c.Primary = c.Backends[0].Name
|
||||
}
|
||||
if c.Prefer == "" {
|
||||
c.Prefer = c.Backends[0].Name
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks the config is internally consistent and usable.
|
||||
func (c Config) Validate() error {
|
||||
if len(c.Backends) == 0 {
|
||||
return fmt.Errorf("no backends configured")
|
||||
return fmt.Errorf("no backends configured: set %sBACKENDS to \"name=url,name=url\" or add a backends list to %s",
|
||||
envPrefix, ConfigPath())
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, b := range c.Backends {
|
||||
@@ -228,9 +248,10 @@ func writeDefaultConfig() error {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return fmt.Errorf("config already exists at %s", path)
|
||||
}
|
||||
data, _ := yaml.Marshal(DefaultConfig())
|
||||
data, _ := yaml.Marshal(ExampleConfig())
|
||||
header := []byte("# pdbmux configuration\n" +
|
||||
"# A merging proxy over two PuppetDBs (old Consul + new k8s) during migration.\n" +
|
||||
"# A merging proxy presenting one PuppetDB v4 query surface over several\n" +
|
||||
"# PuppetDB backends. The backend URLs below are placeholders — edit them.\n" +
|
||||
"# Env overrides: PDBMUX_LISTEN, PDBMUX_PRIMARY, PDBMUX_MERGE, PDBMUX_PREFER,\n" +
|
||||
"# PDBMUX_TIMEOUT, PDBMUX_FRESHNESS_TTL, PDBMUX_BACKENDS (name=url,name=url).\n\n")
|
||||
if err := os.WriteFile(path, append(header, data...), 0o644); err != nil {
|
||||
|
||||
+71
-15
@@ -3,26 +3,69 @@ package main
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoad_Defaults(t *testing.T) {
|
||||
// testConfigValid returns a minimal valid config for Validate/merge tests, with
|
||||
// neutral placeholder backends.
|
||||
func testConfigValid() Config {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Backends = []Backend{
|
||||
{Name: "old", URL: "http://localhost:18080"},
|
||||
{Name: "new", URL: "http://localhost:18081"},
|
||||
}
|
||||
cfg.Primary = "new"
|
||||
cfg.Prefer = "new"
|
||||
return cfg
|
||||
}
|
||||
|
||||
func TestDefaultConfig_NoBackends(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
if cfg.Listen != defaultListen {
|
||||
t.Errorf("listen = %q, want %q", cfg.Listen, defaultListen)
|
||||
}
|
||||
if len(cfg.Backends) != 0 {
|
||||
t.Errorf("defaults must not ship backends, got %+v", cfg.Backends)
|
||||
}
|
||||
if cfg.Merge != mergeFreshness {
|
||||
t.Errorf("merge = %q, want %q", cfg.Merge, mergeFreshness)
|
||||
}
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Error("defaults alone must not validate: backends are required")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_NoBackendsLoadsButFailsValidation(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
clearEnv(t)
|
||||
|
||||
// Load itself must succeed so `config init` / `version` work unconfigured.
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
err = cfg.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("expected a validation error when no backends are configured")
|
||||
}
|
||||
if !strings.Contains(err.Error(), envPrefix+"BACKENDS") {
|
||||
t.Errorf("error should name the env var to set, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_PrimaryDefaultsToFirstBackend(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
clearEnv(t)
|
||||
t.Setenv(envPrefix+"BACKENDS", "a=http://localhost:18080,b=http://localhost:18081")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Listen != defaultListen {
|
||||
t.Errorf("listen = %q, want %q", cfg.Listen, defaultListen)
|
||||
}
|
||||
if len(cfg.Backends) != 2 || cfg.Backends[0].Name != "old" || cfg.Backends[1].Name != "new" {
|
||||
t.Errorf("unexpected default backends: %+v", cfg.Backends)
|
||||
}
|
||||
if cfg.Merge != mergeFreshness || cfg.Primary != "new" {
|
||||
t.Errorf("unexpected defaults merge=%s primary=%s", cfg.Merge, cfg.Primary)
|
||||
if cfg.Primary != "a" || cfg.Prefer != "a" {
|
||||
t.Errorf("primary/prefer should default to first backend, got %q/%q", cfg.Primary, cfg.Prefer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +78,8 @@ func TestLoad_FileAndEnvOverride(t *testing.T) {
|
||||
if err := os.MkdirAll(cfgDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := "listen: :9999\nmerge: static\nprimary: old\nprefer: old\n"
|
||||
body := "listen: :9999\nmerge: static\nprimary: old\nprefer: old\n" +
|
||||
"backends:\n - name: old\n url: http://localhost:18080\n - name: new\n url: http://localhost:18081\n"
|
||||
if err := os.WriteFile(filepath.Join(cfgDir, configFileName), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -56,7 +100,7 @@ func TestLoad_FileAndEnvOverride(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApplyEnv_Backends(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg := testConfigValid()
|
||||
env := map[string]string{
|
||||
envPrefix + "BACKENDS": "a=http://a:8080,b=http://b:8080",
|
||||
envPrefix + "PRIMARY": "a",
|
||||
@@ -91,7 +135,7 @@ func TestValidate(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg := testConfigValid()
|
||||
tc.mutate(&cfg)
|
||||
err := cfg.Validate()
|
||||
if (err != nil) != tc.wantErr {
|
||||
@@ -112,9 +156,21 @@ func TestParseBackends(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrimaryBackend(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
if cfg.PrimaryBackend().URL != defaultNewURL {
|
||||
t.Errorf("primary backend URL = %q, want %q", cfg.PrimaryBackend().URL, defaultNewURL)
|
||||
cfg := testConfigValid()
|
||||
if got, want := cfg.PrimaryBackend().URL, cfg.Backends[1].URL; got != want {
|
||||
t.Errorf("primary backend URL = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExampleConfig_IsValidAndNeutral(t *testing.T) {
|
||||
cfg := ExampleConfig()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("example config must validate: %v", err)
|
||||
}
|
||||
for _, b := range cfg.Backends {
|
||||
if !strings.Contains(b.URL, "example.com") {
|
||||
t.Errorf("example backend %q must use a placeholder host, got %q", b.Name, b.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
// Command pdbmux is a small merging HTTP proxy over two PuppetDB backends.
|
||||
// Command pdbmux is a small merging HTTP proxy over several PuppetDB backends.
|
||||
//
|
||||
// During the VM -> k8s Puppet migration there are two PuppetDBs — the legacy
|
||||
// Consul-registered one and the new k8s one — and nodes move between them as
|
||||
// they migrate. pdbmux presents a single merged PuppetDB v4 query surface so
|
||||
// node-lookup, pblastreport and Puppetboard (and anything else) see one
|
||||
// consistent view:
|
||||
// Running more than one PuppetDB — during a migration between them, or across
|
||||
// regions — means a given node's current data lives in exactly one of them at
|
||||
// any moment. pdbmux presents a single merged PuppetDB v4 query surface so
|
||||
// clients see one consistent view:
|
||||
//
|
||||
// - GET /pdb/query/v4/nodes — fan out to both backends, dedupe by certname,
|
||||
// - GET /pdb/query/v4/nodes — fan out to all backends, dedupe by certname,
|
||||
// keep the record with the newer report_timestamp.
|
||||
// - GET /pdb/query/v4/facts — fan out to both, and for a certname present in
|
||||
// both keep ALL facts from the backend holding that node's newer report
|
||||
// (freshness merge) or a static preferred backend (static merge).
|
||||
// - GET /pdb/query/v4/reports and /events — fan out to both and serve the
|
||||
// deduped union, re-ordered and re-paged across the two backends, because
|
||||
// reports are immutable history and a migrated node has some in each.
|
||||
// - GET /pdb/query/v4/facts — fan out to all, and for a certname present in
|
||||
// more than one keep ALL facts from the backend holding that node's newer
|
||||
// report (freshness merge) or a static preferred backend (static merge).
|
||||
// - GET /pdb/query/v4/reports and /events — fan out to all and serve the
|
||||
// deduped union, re-ordered and re-paged across backends, because reports
|
||||
// are immutable history and a moved node has some in each.
|
||||
// - GET /pdb/query/v4/reports/<hash>/{events,logs,metrics} — served by
|
||||
// whichever backend actually holds that report.
|
||||
// - any other GET /pdb/query/v4/* — transparently proxied to the primary.
|
||||
@@ -21,8 +20,8 @@
|
||||
//
|
||||
// The query param is forwarded verbatim (PuppetDB AST JSON); order_by, limit,
|
||||
// offset and include_total are re-applied over the merged result set. If one
|
||||
// backend errors/times out, the other's results are served; only if both fail
|
||||
// does a merged endpoint return 502.
|
||||
// backend errors/times out, the others' results are served; only if every
|
||||
// backend fails does a merged endpoint return 502.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -72,11 +71,11 @@ func main() {
|
||||
|
||||
root := &cobra.Command{
|
||||
Use: appName,
|
||||
Short: "Merging HTTP proxy over two PuppetDB backends.",
|
||||
Long: "pdbmux presents a single merged PuppetDB v4 query surface over the old\n" +
|
||||
"(Consul) and new (k8s) PuppetDBs during the migration, so node-lookup,\n" +
|
||||
"pblastreport and Puppetboard see one consistent view. Running pdbmux with\n" +
|
||||
"no subcommand (or `pdbmux serve`) starts the proxy.",
|
||||
Short: "Merging HTTP proxy over several PuppetDB backends.",
|
||||
Long: "pdbmux presents a single merged PuppetDB v4 query surface over several\n" +
|
||||
"PuppetDB backends, so clients see one consistent view of nodes, facts and\n" +
|
||||
"reports spanning all of them. Running pdbmux with no subcommand (or\n" +
|
||||
"`pdbmux serve`) starts the proxy.",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return serve(cmd) },
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user