From 6ff9eebefc2756f9e552b05adfce1567bcb23564 Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Wed, 12 Aug 2026 00:16:54 +0200 Subject: [PATCH] Document providers and how to add one in README New Providers section: named-instance config model, a table of the implemented kubernetes and gcp providers, and a four-step guide for adding a new backend (interface contract, GC tagging, error taxonomy, wiring in config.go and main.go). Co-Authored-By: Claude --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/README.md b/README.md index fe8314b..a7783c9 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,62 @@ Cloud-init from a Secret: the Secret **must** carry the label labelled Secrets, so an unlabelled one is invisible (the Proxy reports `CloudInitError`). Rotating the Secret's content triggers VM replacement. +## Providers + +`Managed` proxies are provisioned by a provider — a small compute backend +behind one minimal interface. Providers are configured in the +`--providers-config` YAML file as **named instances**: `spec.provider` on a +Proxy refers to an entry's `name`, not its `type`, so `gcp-eu` and `gcp-us` +can be two differently-configured instances of the same `gcp` type (see +[config/samples/providers-config.yaml](config/samples/providers-config.yaml)). + +### Implemented providers + +| Type | Creates | Per-instance config | Notes | +| --- | --- | --- | --- | +| `kubernetes` | A real Squid pod (`ubuntu/squid:6.6-24.04_edge` by default) in the same cluster the operator runs in | `image` (optional) | Needs no cloud account — local dev, CI, and the kind quickstart. Pods share the cluster's egress IP, so it exercises the full lifecycle but not distinct egress paths. | +| `gcp` | A Compute Engine VM with an ephemeral external IP | `project` (required), `network`, `networkTag`, `diskSizeGb` | The real egress fleet. Deliberately uses only four API calls (Insert / Get / Delete / AggregatedList), all fire-and-forget: `Create` returns as soon as the operation is submitted and the reconciler discovers progress by polling `Get`. Auth is Application Default Credentials — workload identity in-cluster, `gcloud` ADC locally; no key-file plumbing. | + +### Adding a provider + +A new backend (Hetzner, AWS, ...) is four pieces; the contract lives in +[internal/provider/provider.go](internal/provider/provider.go): + +1. **Implement the 4-method `Provider` interface** in a new + `internal/provider//` package: + - `Create` submits and returns — it never blocks until the VM runs, and + must be idempotent keyed on `req.Name` (a deterministic name derived + from the Proxy's UID), so a repeat call after a crash finds the + existing instance instead of duplicating it. + - `Get` returns `provider.ErrNotFound` as a *normal* outcome — the + reconciler branches on it for replacement and adoption, so don't + treat it as exceptional. + - `Delete` is idempotent: deleting an already-gone instance is not an + error. + - `ListByTag` returns every instance the operator ever tagged, for + orphan GC. +2. **Tag every created resource** with `LabelManaged=true` and + `LabelUID=`, and report `CreatedAt` — orphan GC relies on + all three to find owned resources and skip in-flight creates. +3. **Classify every returned error** with `provider.Wrap` into the + four-sentinel taxonomy in + [internal/provider/errors.go](internal/provider/errors.go) + (`ErrNotFound` / `ErrQuotaExceeded` / `ErrTransient` / `ErrPermanent`) + — the reconciler decides retry, slow backoff, or latching `Failed` + purely from that classification, never from provider-specific types. +4. **Wire it up**: add a type-specific config block in + [internal/provider/config.go](internal/provider/config.go), and + register the constructor in the builtins map in + [cmd/main.go](cmd/main.go) (`"": .New`). The registry + ([internal/provider/registry](internal/provider/registry/registry.go)) + handles named instances, and the metrics wrapper is applied + automatically. + +Test against a fake API seam rather than the real cloud — see the +`instancesAPI` seam in +[internal/provider/gcp/gcp.go](internal/provider/gcp/gcp.go) for the +pattern. + ## Caveats — read these two **Changing a proxy changes its IP.** Proxies are immutable cattle: editing