Stage 1: TerdutServer full lifecycle (Deployment, Service, both database
CI / test (push) Successful in 1m46s
CI / test (push) Successful in 1m46s
paths, self-registration bootstrap)
Replaces the bring-your-own-only Stage 1 (commit 1be7cf2) wholesale, per
the redesign in the previous two commits: the operator creates every
server it manages, so self-registration (DESIGN.md §6) is the only
bootstrap path, and Deployment/Service/database management builds
together with it (ROADMAP.md Stage 1) rather than behind a separate
later stage.
Grounded in terdut-server's actual chart (charts/terdut-server/templates/
deployment.yaml, values.yaml), not reconstructed from DESIGN.md's
illustrative YAML alone -- env var names, the password-via-PGPASSWORD
convention, the Recreate deployment strategy, /healthz probes, and the
TERDUT_OPERATOR_MODE=true decision (always on here, unlike the chart's
default-off: every write this operator's own future controllers make
goes through a service account already) all match that source exactly.
- api/v1alpha1: full TerdutServerSpec (image, replicas, networking,
database, sweeper, deadman, notify, oidc, passwordLogin, allowedTeams).
spec.database is a oneOf (dsn xor postgresClusterRef) via CEL
XValidation. No spec.credentialsSecretRef -- removed entirely in the
prior redesign commit, not carried forward.
- internal/controller:
- terdutserver_deployment.go: Deployment + Service via CreateOrUpdate,
owned (OwnerReference), env built field-for-field against the chart.
- terdutserver_database.go: both §8 paths. The Zalando path resolves
the postgresql.acid.zalan.do CR by convention (database/role both
"terdut", matching every DESIGN.md example) and only ever confirms
its generated credentials Secret exists -- never reads the value,
same "wire a secretKeyRef, don't read it" posture the DSN path takes.
classifyClusterGetError is its own function specifically so the
CRD-not-installed case (meta.IsNoMatchError) is unit-testable without
a real client.
- terdutserver_bootstrap.go: self-registration, checkpointed against
both real crash windows (DESIGN.md §6 point 1) -- an admin-key
checkpoint Secret, and adopt-via-GET+mint-new-key on a 409 from
creating the service account. BootstrapStateLost is its own error
type so Reconcile can route it to a condition instead of an infinite
retry.
- terdutserver_controller.go: ties it together -- finalizer add, DB
resolution, Deployment/Service reconcile, wait for a ready replica,
bootstrap, Ready/Bootstrapped/DatabaseReady conditions. Finalizer on
delete only removes the generated Secrets: terdut-server's API can't
delete a user or service account, only revoke keys, so there's
nothing server-side to undo.
- internal/tdclient: added Bootstrap, CreateInstanceServiceAccount,
GetServiceAccountByName, CreateServiceAccountKey, matching
terdut-server's real handlers' request/response shapes (internal/api/
users.go, service_accounts.go in that repo) field-for-field.
- Tests: envtest suite covering the full DSN-path lifecycle end to end
(finalizer -> Deployment/Service -> simulated readiness -> real
bootstrap against an httptest.Server fake), the adopt-on-409 recovery
path, BootstrapStateLost, both Zalando outcomes (cluster not found;
cluster + Secret found -> real DSN -> Ready), and deletion. A minimal
test-only stub of the Zalando CRD (internal/controller/testdata) lets
envtest create fixture objects without a real postgres-operator
installed. 74.0%/44.7% coverage, 0 lint issues.
- Two things scoped down from §8's full ambition, called out in code and
ROADMAP.md rather than silently dropped: no live watch on the
Zalando-generated Secret for rotation (periodic resync notices
eventually, not immediately), no Gateway API HTTPRoute creation from
spec.networking (would add a new dependency; nothing about proving
bootstrap works depends on external ingress existing). Both are
near-term follow-ups.
Verified locally: make fmt lint test build all clean.
This commit is contained in:
@@ -15,9 +15,9 @@ spec:
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.endpoint
|
||||
name: Endpoint
|
||||
type: string
|
||||
- jsonPath: .spec.replicas
|
||||
name: Replicas
|
||||
type: integer
|
||||
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
name: Ready
|
||||
type: string
|
||||
@@ -52,8 +52,8 @@ spec:
|
||||
allowedTeams:
|
||||
description: |-
|
||||
allowedTeams gates cross-namespace TerdutTeams (DESIGN.md §4.6).
|
||||
Unused until TerdutTeam exists (Stage 2); present now so this CRD's
|
||||
schema doesn't need a breaking change to grow it later.
|
||||
Unused until TerdutTeam exists (ROADMAP.md Stage 2); present now so
|
||||
this CRD's schema doesn't need a breaking change to grow it later.
|
||||
properties:
|
||||
namespaces:
|
||||
description: |-
|
||||
@@ -125,45 +125,226 @@ spec:
|
||||
x-kubernetes-map-type: atomic
|
||||
type: object
|
||||
type: object
|
||||
credentialsSecretRef:
|
||||
database:
|
||||
description: |-
|
||||
credentialsSecretRef names a Secret, in the operator's own namespace,
|
||||
that a human has already created by minting an instance-scoped service
|
||||
account with their own admin session (POST /api/service-accounts,
|
||||
DESIGN.md §6) and placing its raw key under the given key. Set, and
|
||||
the Secret found: the controller adopts it outright and skips
|
||||
bootstrap entirely — this is the expected path for Stage 1, not a
|
||||
fallback (DESIGN.md §6 explains why self-registration cannot complete
|
||||
unauthenticated in the setup this stage actually exercises).
|
||||
|
||||
Unset: the controller has no way to acquire a credential in this
|
||||
stage (self-registration lands in Stage 5) and reports
|
||||
Ready: False, reason: CredentialsSecretRefRequired.
|
||||
DatabaseSpec is the Postgres connection this TerdutServer uses. Exactly
|
||||
one of dsn or postgresClusterRef must be set (DESIGN.md §8) — this
|
||||
operator provisions no database either way, only wires up one that
|
||||
exists.
|
||||
properties:
|
||||
key:
|
||||
description: key is the data key inside the Secret holding the
|
||||
raw value.
|
||||
dsn:
|
||||
description: |-
|
||||
dsn is a DSN with no password in it, e.g.
|
||||
"postgres://terdut@terdut-postgres:5432/terdut?sslmode=require" --
|
||||
mutually exclusive with postgresClusterRef.
|
||||
type: string
|
||||
passwordSecretRef:
|
||||
description: |-
|
||||
passwordSecretRef is where PGPASSWORD comes from for the dsn path.
|
||||
pgx falls back to libpq's environment variables for anything the DSN
|
||||
omits, so the password never appears in the DSN string itself. Unused
|
||||
on the postgresClusterRef path -- the Zalando-generated Secret is
|
||||
wired in directly instead.
|
||||
properties:
|
||||
key:
|
||||
description: key is the data key inside the Secret holding
|
||||
the raw value.
|
||||
minLength: 1
|
||||
type: string
|
||||
name:
|
||||
description: name is the Secret's name.
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
- name
|
||||
type: object
|
||||
postgresClusterRef:
|
||||
description: |-
|
||||
postgresClusterRef names a Zalando postgres-operator CR instead of a
|
||||
plain DSN -- mutually exclusive with dsn.
|
||||
properties:
|
||||
name:
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
x-kubernetes-validations:
|
||||
- message: exactly one of dsn or postgresClusterRef must be set
|
||||
rule: '(has(self.dsn) ? 1 : 0) + (has(self.postgresClusterRef) ?
|
||||
1 : 0) == 1'
|
||||
deadman:
|
||||
description: |-
|
||||
DeadmanSpec controls dead man's switch alerts. Matchers/Timeout/Severity
|
||||
map straight to TERDUT_DEADMAN_MATCHERS/TERDUT_DEADMAN_TIMEOUT/
|
||||
TERDUT_DEADMAN_SEVERITY.
|
||||
properties:
|
||||
matchers:
|
||||
type: string
|
||||
severity:
|
||||
type: string
|
||||
timeout:
|
||||
type: string
|
||||
type: object
|
||||
image:
|
||||
description: ImageSpec is the terdut-server image to run.
|
||||
properties:
|
||||
repository:
|
||||
minLength: 1
|
||||
type: string
|
||||
name:
|
||||
description: name is the Secret's name.
|
||||
tag:
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
- name
|
||||
- repository
|
||||
- tag
|
||||
type: object
|
||||
endpoint:
|
||||
networking:
|
||||
description: |-
|
||||
endpoint is the base URL of an already-running terdut-server this
|
||||
TerdutServer represents, e.g. "http://terdut.oncall.svc:8080". This
|
||||
stage never creates or manages a Deployment/Service for it — the
|
||||
server is expected to already exist, deployed some other way (its own
|
||||
Helm chart, by hand).
|
||||
minLength: 1
|
||||
type: string
|
||||
NetworkingSpec is how this TerdutServer is reached from outside the
|
||||
cluster.
|
||||
|
||||
hostname/gatewayListener describe the intended Gateway API HTTPRoute
|
||||
(matching charts/terdut-server's own templates/httpproxy.yaml, despite its
|
||||
name — that chart carries a Gateway API HTTPRoute, not a Contour
|
||||
HTTPProxy), but creating that HTTPRoute isn't implemented yet: it needs
|
||||
the Gateway API types as a new dependency, and nothing about proving a
|
||||
TerdutServer boots and bootstraps a real server depends on external
|
||||
ingress existing. Tracked as a near-term follow-up, not deferred to a
|
||||
later ROADMAP.md stage the way Deployment/database/bootstrap once were.
|
||||
properties:
|
||||
gatewayListener:
|
||||
description: |-
|
||||
gatewayListener is the HTTPRoute's sectionName once it exists. Empty
|
||||
attaches to every matching listener, including plaintext HTTP.
|
||||
type: string
|
||||
hostname:
|
||||
description: hostname the HTTPRoute will carry once it exists.
|
||||
type: string
|
||||
servicePort:
|
||||
default: 8080
|
||||
description: |-
|
||||
servicePort is both the Service's port and the HTTPRoute's backend
|
||||
port once it exists. Defaults to 8080, matching the chart's own
|
||||
service.port default.
|
||||
format: int32
|
||||
type: integer
|
||||
type: object
|
||||
notify:
|
||||
description: |-
|
||||
NotifySpec controls push notifications via ntfy. Empty ntfyURL disables
|
||||
notifications entirely (matches the chart's own default).
|
||||
properties:
|
||||
fallbackTopic:
|
||||
type: string
|
||||
ntfyURL:
|
||||
type: string
|
||||
repeatEvery:
|
||||
type: string
|
||||
tokenSecretRef:
|
||||
description: |-
|
||||
tokenSecretRef is an optional bearer token for an access-controlled
|
||||
ntfy. Leave unset for an open ntfy.
|
||||
properties:
|
||||
key:
|
||||
description: key is the data key inside the Secret holding
|
||||
the raw value.
|
||||
minLength: 1
|
||||
type: string
|
||||
name:
|
||||
description: name is the Secret's name.
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
oidc:
|
||||
description: |-
|
||||
OIDCSpec controls single sign-on. Fields the chart also exposes but
|
||||
DESIGN.md's spec doesn't (usernameClaim, emailClaim, groupsClaim,
|
||||
trustEmail) use terdut-server's own defaults
|
||||
(preferred_username/email/groups/false) rather than being added here
|
||||
speculatively.
|
||||
properties:
|
||||
adminGroup:
|
||||
type: string
|
||||
allowedGroups:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
clientID:
|
||||
type: string
|
||||
clientSecretRef:
|
||||
description: |-
|
||||
SecretKeyRef names one data key inside a Secret. Every use of this type in
|
||||
TerdutServerSpec resolves in the TerdutServer's own namespace (it's wired
|
||||
straight into the Deployment's pod spec as a secretKeyRef env source,
|
||||
which Kubernetes itself only allows same-namespace) -- unlike the
|
||||
generated credentials Secret (DESIGN.md §6), which always lives in the
|
||||
operator's own namespace and is never referenced through this type.
|
||||
properties:
|
||||
key:
|
||||
description: key is the data key inside the Secret holding
|
||||
the raw value.
|
||||
minLength: 1
|
||||
type: string
|
||||
name:
|
||||
description: name is the Secret's name.
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
- name
|
||||
type: object
|
||||
enabled:
|
||||
type: boolean
|
||||
issuer:
|
||||
type: string
|
||||
name:
|
||||
default: SSO
|
||||
type: string
|
||||
scopes:
|
||||
default: openid profile email
|
||||
type: string
|
||||
sessionMaxAge:
|
||||
default: 12h
|
||||
type: string
|
||||
type: object
|
||||
passwordLogin:
|
||||
default: true
|
||||
description: |-
|
||||
passwordLogin: whether a user may sign in, or sign up, with a
|
||||
password.
|
||||
type: boolean
|
||||
replicas:
|
||||
default: 1
|
||||
description: |-
|
||||
replicas. terdut-server is not horizontally-scale-tested; keep this
|
||||
at its default of 1 unless you've verified otherwise -- the sweeper
|
||||
and the notifier are unsynchronised singletons.
|
||||
format: int32
|
||||
type: integer
|
||||
sweeper:
|
||||
description: |-
|
||||
SweeperSpec controls incident auto-resolve/archive timing. Values are
|
||||
Go duration strings (e.g. "6h"), passed straight through to the
|
||||
TERDUT_STALE_AFTER/TERDUT_ARCHIVE_AFTER env vars exactly as written --
|
||||
not a structured metav1.Duration, since terdut-server parses them itself
|
||||
and a round-trip through a different type would buy nothing.
|
||||
properties:
|
||||
archiveAfter:
|
||||
type: string
|
||||
staleAfter:
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- endpoint
|
||||
- database
|
||||
- image
|
||||
- networking
|
||||
type: object
|
||||
status:
|
||||
description: status defines the observed state of TerdutServer
|
||||
@@ -231,9 +412,10 @@ spec:
|
||||
x-kubernetes-list-type: map
|
||||
credentialsSecretRef:
|
||||
description: |-
|
||||
credentialsSecretRef mirrors spec.credentialsSecretRef once adopted —
|
||||
same Secret, same key, always in the operator's own namespace. Set
|
||||
only once Bootstrapped is True.
|
||||
credentialsSecretRef is the generated instance-scoped credential
|
||||
(DESIGN.md §6) -- pure output, always in the operator's own
|
||||
namespace, under a fixed data key ("token"). Set only once
|
||||
Bootstrapped is True.
|
||||
properties:
|
||||
key:
|
||||
description: key is the data key inside the Secret holding the
|
||||
@@ -255,6 +437,12 @@ spec:
|
||||
tells "applied" from "seen" (DESIGN.md §7).
|
||||
format: int64
|
||||
type: integer
|
||||
serviceName:
|
||||
description: |-
|
||||
serviceName is the Service this controller created for the
|
||||
Deployment, so other objects can reference it without recomputing the
|
||||
naming convention.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
|
||||
@@ -8,10 +8,35 @@ rules:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
- services
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- acid.zalan.do
|
||||
resources:
|
||||
- postgresqls
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- terdut.ryuvia.com
|
||||
resources:
|
||||
|
||||
@@ -6,18 +6,28 @@ metadata:
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
name: terdutserver-sample
|
||||
spec:
|
||||
# An already-running terdut-server this TerdutServer represents — Stage 1
|
||||
# never creates or manages a Deployment/Service for it (ROADMAP.md).
|
||||
endpoint: "http://terdut.oncall.svc.cluster.local:8080"
|
||||
# Bring-your-own instance credential (DESIGN.md §6): mint this once,
|
||||
# manually, with your own admin session --
|
||||
# curl -H "Authorization: Bearer <your-own-api-key>" \
|
||||
# -X POST "$ENDPOINT/api/service-accounts" \
|
||||
# -d '{"name":"terdut-operator","scope":"instance"}'
|
||||
# -- then create this Secret, in the OPERATOR's own namespace, from the
|
||||
# "key" field of that response:
|
||||
# kubectl create secret generic terdutserver-sample-creds \
|
||||
# -n <operator namespace> --from-literal=token=<the key>
|
||||
credentialsSecretRef:
|
||||
name: terdutserver-sample-creds
|
||||
key: token
|
||||
image:
|
||||
repository: git.ryuvia.com/niklas/terdut-server
|
||||
tag: v0.20.0
|
||||
replicas: 1
|
||||
networking:
|
||||
hostname: terdut.example.com
|
||||
servicePort: 8080
|
||||
# Bring-your-own DSN (simplest path, no external CRD dependency). For the
|
||||
# Zalando postgres-operator path instead, use:
|
||||
# database:
|
||||
# postgresClusterRef:
|
||||
# name: terdut-postgres
|
||||
database:
|
||||
dsn: "postgres://terdut@terdut-postgres:5432/terdut?sslmode=require"
|
||||
passwordSecretRef:
|
||||
name: terdut-postgres-password
|
||||
key: password
|
||||
sweeper:
|
||||
staleAfter: 6h
|
||||
archiveAfter: 168h
|
||||
deadman:
|
||||
matchers: "alertname=Watchdog"
|
||||
timeout: 15m
|
||||
severity: critical
|
||||
passwordLogin: true
|
||||
|
||||
Reference in New Issue
Block a user