Stage 5: installer chart + release infra, kind e2e pass through the chart
Release / test (push) Successful in 2m48s
CI / chart (push) Successful in 1s
CI / security (push) Successful in 1m3s
CI / test (push) Successful in 2m5s
Release / chart (push) Successful in 4s
Release / image (push) Successful in 7m6s
Release / scan-image (push) Failing after 33s

Chart (charts/terdut-operator) generated via kubebuilder's own helm/v2-alpha
plugin from config/'s kustomize output -- CRDs + manager Deployment/RBAC
come from the same markers every other stage already generates, one source
of truth. Hand-added on top: the optional terdutServer values block
(DESIGN.md §10's "helm install and get a server" path, off by default) and
the release-skill plumbing -- .release.conf, release-vars/helm-lint/push/
helm-package/helm-push/release Makefile targets, .gitea/workflows/release.yaml
(test -> image/chart -> scan-image) -- mirroring terdut-server's own shape
(registry/namespace convention, multi-arch buildx push, trivy/govulncheck/
gitleaks scans). ci.yaml gains security and chart jobs to match.

Two real issues caught while wiring this, fixed before either shipped:
- Dockerfile's builder stage didn't pin --platform=$BUILDPLATFORM, which
  would have made a multi-arch release build fail outright on this org's
  runners (no binfmt registration) -- same fix terdut-server's own
  Dockerfile already needed for the same reason.
- govulncheck found one real, reachable finding: google.golang.org/grpc
  v1.82.1 (transitive via controller-runtime's otel exporter), fixed by
  bumping to v1.83.1.

Full golden-path kind e2e pass, this time through `helm install` rather than
raw kustomize: TerdutServer (real terdut-server v0.33.0 image) -> TerdutTeam
-> one of each child kind, each confirmed Ready and then independently
confirmed against terdut-server's own API from inside the cluster (not just
the operator's own status). Deleted every CR in reverse order and confirmed
server-side cleanup the same independent way for all three child kinds, the
team, and the server. No new bugs found -- Stage 1's own kind pass already
caught what a real cluster catches that envtest can't.

Also dropped the kubebuilder helm plugin's default .github/workflows/
scaffold, same as Stage 0 already did for the main scaffold: this org runs
on Gitea, not GitHub.

Not done here, deliberately: an actual tagged release. release-preflight
found no terdut-operator/ entry under Ryuvia/charts yet to bump -- that
one-time wrapper bootstrap is a decision about deploying this operator for
real, not a side effect of finishing this stage.

make fmt lint test helm-lint build all clean.
This commit is contained in:
Niklas Ye
2026-10-01 14:47:10 +02:00
parent 048f4448c4
commit b4ccdb09d5
50 changed files with 3190 additions and 22 deletions
@@ -0,0 +1,457 @@
{{- if .Values.crd.enabled }}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
{{- if .Values.crd.keep }}
"helm.sh/resource-policy": keep
{{- end }}
controller-gen.kubebuilder.io/version: v0.22.0
name: terdutservers.terdut.ryuvia.com
spec:
group: terdut.ryuvia.com
names:
kind: TerdutServer
listKind: TerdutServerList
plural: terdutservers
singular: terdutserver
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.replicas
name: Replicas
type: integer
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Reason
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: TerdutServer is the Schema for the terdutservers API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: spec defines the desired state of TerdutServer
properties:
allowedTeams:
description: |-
allowedTeams gates cross-namespace TerdutTeams (DESIGN.md §4.6).
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: |-
AllowedTeamsNamespaces gates which namespaces a TerdutTeam may resolve a
cross-namespace serverRef into this TerdutServer from (DESIGN.md §4.6).
Same-namespace TerdutTeams are always allowed, regardless of this field.
Modeled on Gateway API's Gateway.spec.allowedListeners.namespaces.
properties:
from:
default: None
description: |-
from selects which namespaces may attach. Same is equivalent to None in
effect (same-namespace is unrestricted either way) but kept for parity
with the upstream enum this mirrors, and to make the policy
self-documenting in a diff.
enum:
- None
- Same
- All
- Selector
type: string
selector:
description: |-
selector is required, and only meaningful, when from is Selector: a
standard label selector over Namespace objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: object
type: object
database:
description: |-
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:
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
tag:
minLength: 1
type: string
required:
- repository
- tag
type: object
networking:
description: |-
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:
- database
- image
- networking
type: object
status:
description: status defines the observed state of TerdutServer
properties:
conditions:
description: conditions represent the current state of the TerdutServer
resource.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
credentialsSecretRef:
description: |-
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
raw value.
minLength: 1
type: string
name:
description: name is the Secret's name.
minLength: 1
type: string
required:
- key
- name
type: object
observedGeneration:
description: |-
observedGeneration is the .metadata.generation this status was last
computed against — the standard way a client (or `kubectl wait`)
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
type: object
served: true
storage: true
subresources:
status: {}
{{- end }}