Compare commits
2 Commits
20ffba604b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ccd317bec | |||
| 6ff9eebefc |
@@ -83,7 +83,17 @@
|
||||
"Bash(kubectl -n egress-proxies-operator-system get pods -o wide)",
|
||||
"Bash(kubectl -n egress-proxies-operator-system get deploy egress-proxies-operator-controller-manager -o jsonpath='{.spec.template.spec.containers[0].args}')",
|
||||
"Bash(kubectl -n egress-proxies-operator-system logs deploy/egress-proxies-operator-controller-manager)",
|
||||
"Bash(python3 -c \"import json; d=json.load\\(open\\('docs/deploy/sa_key.json'\\)\\); print\\(d.get\\('type'\\), d.get\\('client_email'\\)\\)\")"
|
||||
"Bash(python3 -c \"import json; d=json.load\\(open\\('docs/deploy/sa_key.json'\\)\\); print\\(d.get\\('type'\\), d.get\\('client_email'\\)\\)\")",
|
||||
"Bash(tea pr *)",
|
||||
"Bash(git worktree *)",
|
||||
"Bash(python3 -c \"import yaml; yaml.safe_load\\(open\\('.gitea/workflows/build.yaml'\\)\\); print\\('YAML OK'\\)\")",
|
||||
"Bash(ruby -ryaml -e \"YAML.load_file\\('.gitea/workflows/build.yaml'\\); puts 'YAML OK'\")",
|
||||
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator status --short --branch)",
|
||||
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator log --oneline -1)",
|
||||
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator tag 0.01)",
|
||||
"Bash(git -C /Users/jan.novak/srv/go/egress-proxies-operator push origin 0.01)",
|
||||
"Bash(chmod +x docs/demo/run-demo.sh)",
|
||||
"Bash(bash -n docs/demo/run-demo.sh)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"/Users/jan.novak/srv/go/egress-proxies-operator/.claude",
|
||||
|
||||
56
README.md
56
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/<type>/` 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=<Proxy UID>`, 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) (`"<type>": <pkg>.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
|
||||
|
||||
Reference in New Issue
Block a user