diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 6247e88..38523e6 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -78,6 +78,54 @@ jobs: - name: Format, lint and test run: make fmt lint test - # No `chart` job yet -- there's no Helm chart until Stage 6 (ROADMAP.md). No - # `security` job yet either (govulncheck/gitleaks, as terdut-server has); add one - # alongside `test` once there's controller code worth scanning. + # Runs on every push and pull request, unlike the image scan, which needs something + # published to scan and so lives in release.yaml -- same split as terdut-server's. + # govulncheck reads the source and its module graph, gitleaks reads the working + # tree; neither sees what the other does. + security: + runs-on: ubuntu-latest + container: + image: golang:1.26.6-bookworm + volumes: + - go-mod-cache:/go/pkg/mod + - go-build-cache:/root/.cache/go-build + - gobin-cache:/go/bin + steps: + - name: Checkout + env: + REF_NAME: ${{ github.ref_name }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + if [ -n "$HEAD_SHA" ]; then + git clone "$REPO_URL" . + git checkout -q "$HEAD_SHA" + else + git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . + fi + + - name: Go vulnerability scan (govulncheck) + run: make security-go + + - name: Secret scan (gitleaks) + run: make security-secrets + + # Host mode, no `container:`: helm is baked into the runner image, and a container + # job could not install it -- get.helm.sh is unreachable from the dind bridge, same + # reason terdut-server's own chart job runs on the host. + chart: + runs-on: ubuntu-latest + steps: + - name: Checkout + env: + REF_NAME: ${{ github.ref_name }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + if [ -n "$HEAD_SHA" ]; then + git clone "$REPO_URL" . + git checkout -q "$HEAD_SHA" + else + git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . + fi + + - name: Lint and render the chart + run: make helm-lint diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..063a417 --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -0,0 +1,127 @@ +name: Release + +# Checkout, interpolation and caching conventions match ci.yaml -- see the header there +# for why there are no JS actions and why every `${{ }}` goes through `env:`. +on: + push: + tags: + - 'v*' + workflow_dispatch: + +# A tag is not normally re-pushed, so this mostly matters when one is force-moved during +# a botched release -- the superseded run stops holding runner slots. +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: true + +env: + REPO_URL: https://git.ryuvia.com/niklas/terdut-operator.git + REGISTRY: git.ryuvia.com + IMAGE: git.ryuvia.com/niklas/terdut-operator + +jobs: + # Gates every publishing job below. A tag that fails here publishes nothing: the + # image and the chart are both downstream of it. No Postgres service, unlike + # terdut-server's: this suite drives envtest (a fake API server), not a real database. + test: + runs-on: ubuntu-latest + container: + image: golang:1.26.6-bookworm + volumes: + - go-mod-cache:/go/pkg/mod + - go-build-cache:/root/.cache/go-build + - gobin-cache:/go/bin + steps: + - name: Checkout + env: + REF_NAME: ${{ github.ref_name }} + run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . + + - name: Format, lint and test + run: make fmt lint test + + # Host mode on purpose (no `container:`): this is the only context with a Docker CLI + # pointed at the dind daemon. A `container:` job would sit on the dind bridge with no + # docker socket at all -- same reason terdut-server's image job runs on the host. + image: + needs: test + runs-on: ubuntu-latest + steps: + - name: Checkout + env: + REF_NAME: ${{ github.ref_name }} + run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . + + - name: Log in to the registry + env: + TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: echo "$TOKEN" | docker login "$REGISTRY" -u niklas --password-stdin + + # buildx setup, the platform list and why there is no QEMU all live on the `push` + # target now, so the same command publishes from a laptop and from here. + - name: Build and push + env: + REF_NAME: ${{ github.ref_name }} + run: make push VERSION="$REF_NAME" + + # Also host mode: helm is baked into the runner image, and a `container:` job could + # not install it -- get.helm.sh is unreachable from the dind bridge. + chart: + needs: test + runs-on: ubuntu-latest + steps: + - name: Checkout + env: + REF_NAME: ${{ github.ref_name }} + run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . + + # One publisher, triggered by the tag -- same reasoning as terdut-server's own + # chart job: a workflow triggered by the main push cannot know the version it is + # about to be tagged with, so there is no second publisher racing this one. + - name: Refuse a non-version tag + env: + REF_NAME: ${{ github.ref_name }} + run: | + set -eu + if ! echo "$REF_NAME" | grep -qE '^v[0-9]'; then + echo "::error::refusing to publish a chart for non-version tag ${REF_NAME}" + exit 1 + fi + + # Render before publishing, so a template that does not compile is found here, + # not by Flux after the chart is already in the registry. + - name: Lint and render the chart + run: make helm-lint + + - name: Package and push + env: + REF_NAME: ${{ github.ref_name }} + TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -eu + echo "$TOKEN" | helm registry login "$REGISTRY" -u niklas --password-stdin + make helm-package helm-push VERSION="$REF_NAME" + + # Host mode, like image and chart: this needs a docker daemon to run trivy in, and a + # `container:` job would sit on the dind bridge with none. + # + # Scans the pushed image, not a local one -- trivy cannot read a locally built image + # on this runner (no docker socket in a container job, no shared filesystem with the + # dind sidecar), so it pulls from the registry. Runs after `image` rather than gating + # it: a red scan does not unpublish anything -- it means do not bump the wrapper + # chart in Ryuvia/charts to this version. This pipeline does not deploy. + scan-image: + needs: image + runs-on: ubuntu-latest + steps: + - name: Checkout + env: + REF_NAME: ${{ github.ref_name }} + run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . + + - name: Scan the pushed image (trivy) + env: + TRIVY_USERNAME: niklas + TRIVY_PASSWORD: ${{ secrets.REGISTRY_TOKEN }} + REF_NAME: ${{ github.ref_name }} + run: make security-image VERSION="$REF_NAME" diff --git a/.gitignore b/.gitignore index e0d4e42..0701790 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ bin/ # local kubeconfig/secrets some workflows write here *.kubeconfig cover.out + +# scratch output: build-installer's consolidated manifest and packaged charts +# (release-vars' HELM_CHART is charts/terdut-operator, committed; this is not) +/dist/ diff --git a/.release.conf b/.release.conf new file mode 100644 index 0000000..5a27b57 --- /dev/null +++ b/.release.conf @@ -0,0 +1,21 @@ +# Read by the `release` skill (~/.claude/skills/release). +# +# Only what the Makefile cannot already say. IMAGE, HELM_CHART and HELM_REPO come from +# `make release-vars`, so they have one definition and cannot drift from what is built. +# +# Defaults, set here only where this repo differs: +# CHARTS_REPO=$HOME/git/charts CHARTS_DIR= +# GITEA_LOGIN=Ryuvia APPVERSION_PREFIX= +# PROSE_LANG=en + +# Same as the image basename, so this is only stated to be read rather than derived. +CHARTS_DIR=terdut-operator + +# This repo writes appVersion: "v0.1.0" (see charts/terdut-operator/Chart.yaml), +# matching terdut-server's own v-prefixed style -- nothing reads the field, but people +# do, and it should say the same thing the release tag does. +APPVERSION_PREFIX=v + +# English, for the same reason as terdut-server: this is an on-call tool's operator, +# and nothing about its labels, API or docs is coupled to Swedish. +PROSE_LANG=en diff --git a/CLAUDE.md b/CLAUDE.md index 5517ba1..e2743c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,19 +7,40 @@ short pitch. ## Checks -`make fmt lint test` is the CI gate (`.gitea/workflows/ci.yaml` calls these targets -rather than restating them, same convention as terdut-server). `test` chains through -the Kubebuilder-scaffolded `manifests`/`generate` (`controller-gen`) and `setup-envtest` -targets automatically — everything needed lands in `bin/` (gitignored) on first run, no -separate tool install required beyond Go itself and network access to -`proxy.golang.org`/`storage.googleapis.com`. +`make fmt lint test helm-lint` is the CI gate (`.gitea/workflows/ci.yaml`'s `test`, +`security` and `chart` jobs call these targets rather than restating them, same +convention as terdut-server). `test` chains through the Kubebuilder-scaffolded +`manifests`/`generate` (`controller-gen`) and `setup-envtest` targets automatically — +everything needed lands in `bin/` (gitignored) on first run, no separate tool install +required beyond Go itself and network access to `proxy.golang.org`/`storage.googleapis.com`. +`security` runs `make security-go`/`security-secrets` (govulncheck/gitleaks), same as +terdut-server's own `security` job. -`make test-e2e` stands up a real `kind` cluster (`kind`/`docker` must be installed) and -is not part of the CI gate yet — it has no service-image to test against until later -ROADMAP stages produce one. +The kubebuilder-scaffolded `make test-e2e` (a disposable, generic smoke test) is separate +from the real golden-path `kind` e2e pass ROADMAP.md's Stage 5 describes (create every CRD +kind, verify against terdut-server's own API, delete, verify gone) — the latter is a +manual pass run and recorded in ROADMAP.md, not a CI job, matching Stage 1-4's own +precedent of validating against a real cluster outside CI. ## Release -Not wired yet. `.release.conf` and the release-vars Makefile target land in ROADMAP.md's -Stage 6, once there's an actual Helm chart to release — see that file before assuming -the `release` skill's terdut-server/terdut-tui conventions already apply here. +Wired as of Stage 5 (ROADMAP.md): `.release.conf`, `make release-vars`/`helm-lint`/ +`push`/`helm-package`/`helm-push`/`release`, and `.gitea/workflows/release.yaml` +(`test` → `image`/`chart` → `scan-image`) all follow terdut-server's established shape — +see that repo's Makefile/`.release.conf` for the shared reasoning, not restated here. + +The chart is `charts/terdut-operator` (via kubebuilder's own `helm/v2-alpha` plugin, +regenerate with `kubebuilder edit --plugins helm.kubebuilder.io/v2-alpha --output-dir +charts --force` after `config/` changes, then re-review — `--force` does not touch +`Chart.yaml` but does touch `values.yaml`, which carries hand-written additions, most +importantly the optional `terdutServer` block, DESIGN.md §10). It installs the operator + +CRDs + RBAC, and optionally one `TerdutServer` CR (`terdutServer.enabled`, off by default). + +**One manual step the release skill's own automation does not cover**: `release-preflight` +expects an existing `terdut-operator/` entry under `Ryuvia/charts` to bump on release +(steps 8-10 of the skill). There is no such entry yet — this repo's first-ever release +can publish its own image and chart (the `test`/`image`/`chart`/`scan-image` jobs), but +the wrapper-chart bump and PR will fail until someone creates that initial wrapper entry +in `Ryuvia/charts` by hand, the same one-time step every other onboarded repo already had +done for it before its own first release. That's a deliberate decision to deploy this +operator for real, not something to do as a side effect of finishing this stage. diff --git a/Dockerfile b/Dockerfile index 1fa4334..8934aa6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,12 @@ # Build the manager binary # Override BASE_IMAGE to build from another registry, e.g. docker.io/library/golang:1.26 ARG BASE_IMAGE=golang:1.26 -FROM ${BASE_IMAGE} AS builder +# --platform=$BUILDPLATFORM pins the builder to the machine doing the building, so a +# multi-arch build compiles both targets natively instead of running an emulated arm64 +# toolchain under QEMU -- same reasoning, and the same fix, as terdut-server's own +# Dockerfile: the CI runner has no binfmt registration and no way to get one, so +# this is not just an optimization, it is what makes the arm64 image buildable at all. +FROM --platform=$BUILDPLATFORM ${BASE_IMAGE} AS builder ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index f555240..5784e8d 100644 --- a/Makefile +++ b/Makefile @@ -259,3 +259,190 @@ endef define gomodver $(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null) endef + +##@ Helm Deployment + +## Helm binary to use for deploying the chart +HELM ?= helm +## Namespace to deploy the Helm release +HELM_NAMESPACE ?= terdut-operator-system +## Name of the Helm release +HELM_RELEASE ?= terdut-operator +## Path to the Helm chart directory. Must stay equal to HELM_CHART below +## (the release process's own name for this same path) -- two variables +## because this one is kubebuilder's own scaffold and that one is the +## release skill's contract, not because the path differs. +HELM_CHART_DIR ?= charts/terdut-operator +## Additional arguments to pass to helm commands +HELM_EXTRA_ARGS ?= + +.PHONY: install-helm +install-helm: ## Install the latest version of Helm. + @command -v $(HELM) >/dev/null 2>&1 || { \ + echo "Installing Helm..." && \ + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \ + } + +.PHONY: helm-deploy +helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG. + IMG="$(IMG)"; $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \ + --namespace $(HELM_NAMESPACE) \ + --create-namespace \ + --set manager.image.repository=$${IMG%:*} \ + --set manager.image.tag=$${IMG##*:} \ + --wait \ + --timeout 5m \ + $(HELM_EXTRA_ARGS) + +.PHONY: helm-uninstall +helm-uninstall: ## Uninstall the Helm release from the K8s cluster. + $(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) + +.PHONY: helm-status +helm-status: ## Show Helm release status. + $(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) + +.PHONY: helm-history +helm-history: ## Show Helm release history. + $(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) + +.PHONY: helm-rollback +helm-rollback: ## Rollback to previous Helm release. + $(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) + +##@ Release + +# Everything below is read by the `release` skill (~/.claude/skills/release), not by +# anything above it in this file -- mirrors terdut-server's Makefile section for +# section, adapted for this repo (no Postgres service for tests, no CLI binaries to +# cross-compile, one image + one chart to publish). + +REGISTRY := git.ryuvia.com +# The personal namespace, not ryuvia -- same reasoning as terdut-server: Gitea scopes +# package visibility to the owner with no per-package override, so publishing here +# keeps the image and chart anonymously pullable and Flux needs no registry +# credentials to pull them. +OWNER := niklas + +IMAGE := $(REGISTRY)/$(OWNER)/terdut-operator +HELM_CHART := charts/terdut-operator +HELM_REPO := oci://$(REGISTRY)/$(OWNER) + +.PHONY: release-vars +release-vars: ## Print the variables the release process reads + @printf 'IMAGE=%s\nHELM_CHART=%s\nHELM_REPO=%s\n' '$(IMAGE)' '$(HELM_CHART)' '$(HELM_REPO)' + +# database is required (dsn xor postgresClusterRef, DatabaseSpec's own CEL rule) once +# terdutServer.enabled, so the chart's own `required` calls fail a bare +# `--set terdutServer.enabled=true` the same way a real install without a database +# would be rejected at apply time -- this set gives that path something valid to +# render against, the way terdut-server's own HELM_LINT_SET supplies its one +# required field (database.dsn) for the same reason. +HELM_LINT_SET = --set terdutServer.enabled=true \ + --set terdutServer.image.tag=v0.0.0 \ + --set terdutServer.networking.hostname=terdut.example.invalid \ + --set 'terdutServer.database.dsn=postgres://terdut@terdut-postgres:5432/terdut?sslmode=require' + +.PHONY: helm-lint +helm-lint: ## Lint and render the chart + helm lint $(HELM_CHART) + helm template terdut-operator $(HELM_CHART) --namespace terdut-operator-system >/dev/null + # Second pass: the optional TerdutServer CR template (off by default, so the + # bare render above never exercises it at all). + helm lint $(HELM_CHART) $(HELM_LINT_SET) + helm template terdut-operator $(HELM_CHART) --namespace terdut-operator-system \ + $(HELM_LINT_SET) >/dev/null + +## --- publishing --- +# +# Exist so .gitea/workflows/release.yaml can call `make push` / `make helm-package +# helm-push` instead of restating the build in YAML -- one definition, runnable +# locally, reviewable in a diff. Publishing happens by pushing a tag; nothing else. +# +# VERSION is the git tag, passed in by the workflow. require-version is why a stray +# local `make push` cannot publish: dev is not a version anyone releases. + +VERSION ?= dev + +# Helm requires strict SemVer -- strip a leading 'v' if present. +CHART_VERSION := $(shell echo "$(VERSION)" | sed 's/^v//') + +# Named RELEASE_PLATFORMS, not PLATFORMS: that name is already taken above by +# kubebuilder's own docker-buildx target (a 4-platform list this repo's own release +# process doesn't use), and `?=` on an already-set variable is a silent no-op, not an +# override -- reusing it here would have quietly built s390x/ppc64le instead of the +# two platforms this target actually intends. +RELEASE_PLATFORMS ?= linux/amd64,linux/arm64 +BUILDX_BUILDER ?= terdut-operator-release + +# An isolated repo list, same reasoning as terdut-server's: the machine-wide one is +# not this build's business, and one unreachable entry in it aborts otherwise-fine +# helm commands. HELM_REPOSITORY_CACHE is deliberately NOT overridden alongside it -- +# helm writes a refreshed index to the default cache and then looks for it there. +HELM_ISOLATED = HELM_REPOSITORY_CONFIG=$(CURDIR)/.helm-repos.yaml + +.PHONY: require-version +require-version: + @test "$(VERSION)" != "dev" || \ + (echo "VERSION=dev names no release -- pass VERSION=vX.Y.Z (the workflow passes the tag)" && exit 1) + +# Multi-arch, so this is build-and-push in one step, same reasoning as +# terdut-server's own `push`: buildx cannot load a multi-platform result into the +# local image store, so there is no separate local-only `build` target here either. +# +# No QEMU: the Dockerfile's builder stage runs on $$BUILDPLATFORM and cross-compiles +# via GOOS/GOARCH, so both platforms build natively -- same fix as terdut-server's +# Dockerfile, for the same reason (this CI runner has no binfmt registration). +.PHONY: push +push: require-version ## Build and publish the multi-arch image + docker buildx create --name $(BUILDX_BUILDER) --use 2>/dev/null || docker buildx use $(BUILDX_BUILDER) + docker buildx build \ + --platform $(RELEASE_PLATFORMS) \ + --tag "$(IMAGE):latest" \ + --tag "$(IMAGE):$(VERSION)" \ + --push . + +# --version/--app-version come from the tag, so Chart.yaml's own fields decide +# nothing about what gets published -- same as terdut-server's chart. +.PHONY: helm-package +helm-package: require-version ## Package the chart, versioned from the tag + $(HELM_ISOLATED) helm package $(HELM_CHART) \ + --version $(CHART_VERSION) \ + --app-version $(VERSION) \ + --destination dist + +.PHONY: helm-push +helm-push: require-version ## Push the packaged chart to the OCI registry + $(HELM_ISOLATED) helm push dist/terdut-operator-$(CHART_VERSION).tgz $(HELM_REPO) + +.PHONY: release +release: push helm-package helm-push ## Publish image + chart (the workflow's one call) + +## --- security --- + +GOVULNCHECK_VERSION := v1.1.4 +GITLEAKS_VERSION := v8.30.0 +TRIVY_VERSION := 0.73.0 + +# Symbol-level, not dependency-level, same as terdut-server: govulncheck reports a +# vulnerability only when the code can actually reach it. +.PHONY: security-go +security-go: ## Scan Go deps for known CVEs (govulncheck) + go run golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION) ./... + +# --no-git scans the working tree rather than the history, so this catches a secret +# on the way in; it says nothing about what is already committed. +.PHONY: security-secrets +security-secrets: ## Scan the working tree for committed secrets (gitleaks) + go run github.com/zricethezav/gitleaks/v8@$(GITLEAKS_VERSION) detect --no-git \ + --source . --redact --no-banner --exit-code 1 + +# Scans the pushed image, not a local one -- trivy cannot read a locally built image +# on this runner, same reasoning as terdut-server. A red scan means: do not bump the +# wrapper chart in Ryuvia/charts to this version; it does not unpublish anything. +.PHONY: security-image +security-image: require-version ## Scan the pushed image for CVEs (needs VERSION) + docker run --rm -e TRIVY_USERNAME -e TRIVY_PASSWORD \ + -v trivy-cache:/root/.cache/trivy \ + docker.io/aquasec/trivy:$(TRIVY_VERSION) image --severity HIGH,CRITICAL \ + --ignore-unfixed --exit-code 1 $(IMAGE):$(VERSION) diff --git a/PROJECT b/PROJECT index 0e4bb85..143805b 100644 --- a/PROJECT +++ b/PROJECT @@ -6,6 +6,10 @@ cliVersion: 4.16.0 domain: ryuvia.com layout: - go.kubebuilder.io/v4 +plugins: + helm.kubebuilder.io/v2-alpha: + manifests: dist/install.yaml + output: charts projectName: terdut-operator repo: git.ryuvia.com/niklas/terdut-operator resources: diff --git a/ROADMAP.md b/ROADMAP.md index 620a5de..e5b9b9d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -181,6 +181,53 @@ New commits build forward over the old ones; no git history rewrite. → one of each child kind → verify via terdut-server's own API that each object exists with the right shape → delete the CR → verify the server-side object is gone. +- Chart built via kubebuilder's own `helm/v2-alpha` plugin from `config/`'s + kustomize output (`charts/terdut-operator`), not hand-rolled -- CRDs + + manager Deployment/RBAC come from the same markers/manifests every other + stage already generates, so there's exactly one source of truth for + them. Hand-added on top: the optional `terdutServer` values block (§10's + "helm install and get a server" path), `.release.conf`, and the + `release-vars`/`helm-lint`/`push`/`helm-package`/`helm-push`/`release` + Makefile targets `.gitea/workflows/release.yaml` calls, mirroring + terdut-server's own shape end to end (same registry/namespace + convention, same multi-arch buildx push, same trivy/govulncheck/gitleaks + scans). Also fixed while wiring this: the 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) -- caught before it ever shipped, not discovered mid-release; + and govulncheck surfaced 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. +- **Done, 2026-10-01**: the full golden-path pass above, run for real + against a `kind` cluster, installed via `helm install` (not raw + kustomize/kubectl apply -- the first time the chart itself, not just + `config/`, was exercised): `TerdutServer` (real terdut-server `v0.33.0` + image, bring-your-own DSN against a throwaway in-cluster Postgres) → + `TerdutTeam` → one `TerdutEscalationRule` + `TerdutDeadmanSwitch` + + `TerdutAlertSource`, each confirmed `Ready` and then confirmed a second + way, independent of the operator's own status: a `curl` pod inside the + cluster, authenticated with the generated team credential, hit + terdut-server's real API directly (`GET /api/teams/{id}/escalation`, + `.../deadman/switches`, `.../integrations`) and got back exactly the + policy/switch/integration each spec declared. Deleting every CR in + reverse order was verified the same way: the escalation policy came back + empty (its only available "undo"), the switch and the integration were + both gone from their list endpoints, the team no longer resolved by + name, and the Deployment/Service/every generated Secret were gone from + the cluster. No new bugs found this pass -- Stage 1's own kind e2e pass + already caught the two issues (`events.k8s.io` RBAC, the podman + `.dockerignore` fix) a real cluster catches and `envtest` can't, and + nothing since has touched that surface. +- Not done in this pass, deliberately: an actual tagged release. `make + release-vars`/`helm-lint`/`push`/`helm-package`/`helm-push` all work + locally and `.gitea/workflows/release.yaml` is wired, but + `release-preflight` found there is no `terdut-operator/` entry under + `Ryuvia/charts` yet to bump -- every other onboarded repo had that + one-time wrapper-chart bootstrap done for it before its own first + release, and this one doesn't, since deploying this operator for real is + a decision for whoever runs the cluster, not a side effect of finishing + this stage. Cutting the first real release (and creating that wrapper + entry) is therefore the next action, not yet taken. ## Deferred (§13, unchanged by this roadmap) diff --git a/charts/terdut-operator/.helmignore b/charts/terdut-operator/.helmignore new file mode 100644 index 0000000..7d92f7f --- /dev/null +++ b/charts/terdut-operator/.helmignore @@ -0,0 +1,25 @@ +# Patterns to ignore when building Helm packages. +# Operating system files +.DS_Store + +# Version control directories +.git/ +.gitignore +.bzr/ +.hg/ +.hgignore +.svn/ + +# Backup and temporary files +*.swp +*.tmp +*.bak +*.orig +*~ + +# IDE and editor-related files +.idea/ +.vscode/ + +# Helm chart artifacts +dist/chart/*.tgz diff --git a/charts/terdut-operator/Chart.yaml b/charts/terdut-operator/Chart.yaml new file mode 100644 index 0000000..81afa59 --- /dev/null +++ b/charts/terdut-operator/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: terdut-operator +description: Installs terdut-operator (CRDs + controller) for terdut-server, and optionally one TerdutServer CR +type: application + +# These fields decide nothing: `make helm-package` passes --version and +# --app-version from the release tag (same reasoning as terdut-server's own +# chart). They're for whoever reads the tree before a tag exists. +version: 0.1.0 +appVersion: "v0.1.0" + +keywords: + - kubernetes + - operator + - terdut + +home: https://git.ryuvia.com/niklas/terdut-operator + +annotations: + kubebuilder.io/generated-by: kubebuilder diff --git a/charts/terdut-operator/templates/NOTES.txt b/charts/terdut-operator/templates/NOTES.txt new file mode 100644 index 0000000..1cec53d --- /dev/null +++ b/charts/terdut-operator/templates/NOTES.txt @@ -0,0 +1,15 @@ +Thank you for installing {{ .Chart.Name }}. + +Your release is named {{ .Release.Name }}. + +The controller and CRDs have been installed in namespace {{ .Release.Namespace }}. + +To verify the installation: + + kubectl get pods -n {{ .Release.Namespace }} + kubectl get customresourcedefinitions + +To learn more about the release, try: + + $ helm status {{ .Release.Name }} -n {{ .Release.Namespace }} + $ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }} diff --git a/charts/terdut-operator/templates/_helpers.tpl b/charts/terdut-operator/templates/_helpers.tpl new file mode 100644 index 0000000..a00afcc --- /dev/null +++ b/charts/terdut-operator/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "terdut-operator.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "terdut-operator.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Namespace for generated references. +Always uses the Helm release namespace. +*/}} +{{- define "terdut-operator.namespaceName" -}} +{{- .Release.Namespace }} +{{- end }} + +{{/* +Resource name with proper truncation for Kubernetes 63-character limit. +Takes a dict with: + - .suffix: Resource name suffix (e.g., "metrics", "webhook") + - .context: Template context (root context with .Values, .Release, etc.) +Dynamically calculates safe truncation to ensure total name length <= 63 chars. +*/}} +{{- define "terdut-operator.resourceName" -}} +{{- $fullname := include "terdut-operator.fullname" .context }} +{{- $suffix := .suffix }} +{{- $maxLen := sub 62 (len $suffix) | int }} +{{- if gt (len $fullname) $maxLen }} +{{- printf "%s-%s" (trunc $maxLen $fullname | trimSuffix "-") $suffix | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" $fullname $suffix | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{/* +ServiceAccount name to use. +When enabled, use the chart's ServiceAccount name. +When disabled, serviceAccount.name must be set; use "default" to pick the namespace default ServiceAccount. +*/}} +{{- define "terdut-operator.serviceAccountName" -}} +{{- if .Values.serviceAccount.enabled }} +{{- include "terdut-operator.resourceName" (dict "suffix" "controller-manager" "context" .) }} +{{- else }} +{{- required "serviceAccount.name is required when serviceAccount.enabled=false (set name: default explicitly to use the namespace default ServiceAccount)" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/terdut-operator/templates/crd/terdutalertsources.terdut.ryuvia.com.yaml b/charts/terdut-operator/templates/crd/terdutalertsources.terdut.ryuvia.com.yaml new file mode 100644 index 0000000..f499e1f --- /dev/null +++ b/charts/terdut-operator/templates/crd/terdutalertsources.terdut.ryuvia.com.yaml @@ -0,0 +1,198 @@ +{{- 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: terdutalertsources.terdut.ryuvia.com +spec: + group: terdut.ryuvia.com + names: + kind: TerdutAlertSource + listKind: TerdutAlertSourceList + plural: terdutalertsources + singular: terdutalertsource + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.teamRef.name + name: Team + type: string + - jsonPath: .status.integrationID + name: IntegrationID + 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: TerdutAlertSource is the Schema for the terdutalertsources 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 TerdutAlertSource + properties: + kind: + default: alertmanager + description: |- + kind is the alert source type. Only "alertmanager" is supported + today, mirroring terdut-server's own CHECK constraint on + integrations.kind (internal/db/migrations/003_teams.sql) -- + confirmed against source, not assumed. Changing it after the + integration already exists rotates the webhook key (DESIGN.md §5's + reconciliation table): the old one is deleted and a fresh one + created, which breaks whatever sends to the old URL until the new + Secret is picked up. + enum: + - alertmanager + type: string + name: + description: |- + name is this source's own display name server-side -- distinct from + this object's own metadata.name. POST + /api/teams/{teamID}/integrations {"name": ...} at creation, and what + PATCH renames thereafter; renaming never rotates the webhook key. + minLength: 1 + type: string + teamRef: + description: |- + TerdutTeamRef names the TerdutTeam this resource belongs to. Always + same-namespace as the CR itself (DESIGN.md §1: only TerdutTeam.spec.serverRef + crosses namespaces in v1) -- no namespace field, unlike TerdutServerRef. + properties: + name: + minLength: 1 + type: string + required: + - name + type: object + required: + - name + - teamRef + type: object + status: + description: status defines the observed state of TerdutAlertSource + properties: + conditions: + 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 + integrationID: + description: integrationID is the server-side id. + format: int64 + type: integer + lastAppliedKind: + description: |- + lastAppliedKind is the kind the currently-live integration was + actually created with -- compared against spec.kind on every + reconcile to detect the one spec change that requires + delete-and-recreate (DESIGN.md §5), since terdut-server's own API has + no way to read a live integration's kind back for comparison. + type: string + observedGeneration: + format: int64 + type: integer + webhookURLSecretRef: + description: |- + webhookURLSecretRef names the generated Secret holding "url" and + "key" -- the integration's webhook address and credential, shown by + terdut-server's API exactly once, at creation (DESIGN.md §4.5), and + never re-readable afterward, including from this status. Lives in + this CR's own namespace with a plain OwnerReference (§7) -- unlike + TerdutServer/TerdutTeam's credential Secrets, this one never crosses + namespaces, so no finalizer cleanup is needed for it specifically. + properties: + name: + description: name is the Secret's name. + minLength: 1 + type: string + required: + - name + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +{{- end }} diff --git a/charts/terdut-operator/templates/crd/terdutdeadmanswitches.terdut.ryuvia.com.yaml b/charts/terdut-operator/templates/crd/terdutdeadmanswitches.terdut.ryuvia.com.yaml new file mode 100644 index 0000000..7beb1aa --- /dev/null +++ b/charts/terdut-operator/templates/crd/terdutdeadmanswitches.terdut.ryuvia.com.yaml @@ -0,0 +1,184 @@ +{{- 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: terdutdeadmanswitches.terdut.ryuvia.com +spec: + group: terdut.ryuvia.com + names: + kind: TerdutDeadmanSwitch + listKind: TerdutDeadmanSwitchList + plural: terdutdeadmanswitches + singular: terdutdeadmanswitch + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.teamRef.name + name: Team + type: string + - jsonPath: .status.switchID + name: SwitchID + 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: TerdutDeadmanSwitch is the Schema for the terdutdeadmanswitches + 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 TerdutDeadmanSwitch + properties: + matcher: + description: |- + matcher names the alerts this switch watches, e.g. + "alertname=Watchdog,cluster=prod". One matcher per switch -- add + another TerdutDeadmanSwitch instead of separating with ";" + (terdut-server's own restriction, mirrored here so a bad spec is + rejected at apply time). + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'one matcher per switch: add another TerdutDeadmanSwitch + instead of separating with ;' + rule: '!self.contains('';'')' + name: + description: |- + name is optional, same as the API: left empty, terdut-server derives + it from matcher's own canonical form, and that's what the + idempotent-create lookup matches against too. + type: string + severity: + default: critical + enum: + - critical + - error + - warning + - info + type: string + teamRef: + description: |- + TerdutTeamRef names the TerdutTeam this resource belongs to. Always + same-namespace as the CR itself (DESIGN.md §1: only TerdutTeam.spec.serverRef + crosses namespaces in v1) -- no namespace field, unlike TerdutServerRef. + properties: + name: + minLength: 1 + type: string + required: + - name + type: object + timeout: + description: timeout is a Go duration string, e.g. "15m". + minLength: 1 + type: string + required: + - matcher + - teamRef + - timeout + type: object + status: + description: status defines the observed state of TerdutDeadmanSwitch + properties: + conditions: + 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 + observedGeneration: + format: int64 + type: integer + switchID: + description: switchID is the server-side id. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +{{- end }} diff --git a/charts/terdut-operator/templates/crd/terdutescalationrules.terdut.ryuvia.com.yaml b/charts/terdut-operator/templates/crd/terdutescalationrules.terdut.ryuvia.com.yaml new file mode 100644 index 0000000..3fe1938 --- /dev/null +++ b/charts/terdut-operator/templates/crd/terdutescalationrules.terdut.ryuvia.com.yaml @@ -0,0 +1,195 @@ +{{- 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: terdutescalationrules.terdut.ryuvia.com +spec: + group: terdut.ryuvia.com + names: + kind: TerdutEscalationRule + listKind: TerdutEscalationRuleList + plural: terdutescalationrules + singular: terdutescalationrule + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.teamRef.name + name: Team + type: string + - 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: TerdutEscalationRule is the Schema for the terdutescalationrules + 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 TerdutEscalationRule + properties: + fallbackTopic: + type: string + levels: + items: + description: |- + EscalationLevel is one rung of the ladder: how long to wait, and who to + page if nobody's acknowledged by then. + properties: + targets: + items: + description: |- + EscalationTarget is one page within a level. username is required iff + kind is "user" (terdut-server's own validation, internal/api/escalation.go's + handleSetEscalation -- mirrored here as a CEL rule so a bad spec is + rejected at apply time, not discovered on the next failed PUT). + properties: + kind: + description: EscalationTargetKind is who one rung of the + ladder pages. + enum: + - oncall + - user + type: string + username: + type: string + required: + - kind + type: object + x-kubernetes-validations: + - message: username is required when kind is user + rule: self.kind != 'user' || has(self.username) + - message: username must not be set when kind is oncall + rule: self.kind != 'oncall' || !has(self.username) + minItems: 1 + type: array + timeout: + description: timeout is a Go duration string, e.g. "5m". + minLength: 1 + type: string + required: + - targets + - timeout + type: object + minItems: 1 + type: array + repeatCount: + format: int64 + maximum: 10 + minimum: 0 + type: integer + teamRef: + description: |- + TerdutTeamRef names the TerdutTeam this resource belongs to. Always + same-namespace as the CR itself (DESIGN.md §1: only TerdutTeam.spec.serverRef + crosses namespaces in v1) -- no namespace field, unlike TerdutServerRef. + properties: + name: + minLength: 1 + type: string + required: + - name + type: object + required: + - levels + - teamRef + type: object + status: + description: status defines the observed state of TerdutEscalationRule + properties: + conditions: + 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 + observedGeneration: + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +{{- end }} diff --git a/charts/terdut-operator/templates/crd/terdutservers.terdut.ryuvia.com.yaml b/charts/terdut-operator/templates/crd/terdutservers.terdut.ryuvia.com.yaml new file mode 100644 index 0000000..5cd0b5d --- /dev/null +++ b/charts/terdut-operator/templates/crd/terdutservers.terdut.ryuvia.com.yaml @@ -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 }} diff --git a/charts/terdut-operator/templates/crd/terdutteams.terdut.ryuvia.com.yaml b/charts/terdut-operator/templates/crd/terdutteams.terdut.ryuvia.com.yaml new file mode 100644 index 0000000..3272061 --- /dev/null +++ b/charts/terdut-operator/templates/crd/terdutteams.terdut.ryuvia.com.yaml @@ -0,0 +1,198 @@ +{{- 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: terdutteams.terdut.ryuvia.com +spec: + group: terdut.ryuvia.com + names: + kind: TerdutTeam + listKind: TerdutTeamList + plural: terdutteams + singular: terdutteam + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.serverRef.name + name: Server + type: string + - jsonPath: .status.teamID + name: TeamID + 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: TerdutTeam is the Schema for the terdutteams 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 TerdutTeam + properties: + displayName: + description: |- + displayName is this team's name, both in terdut-server's own data + (POST /api/teams {"name": ...}) and as the identity POST /api/teams + and GET /api/teams?name= correlate on (DESIGN.md §5's idempotent- + create rule, via TEAM-LOOKUP.md). + minLength: 1 + type: string + oidc: + description: |- + TerdutTeamOIDC binds which identity-provider groups grant membership and + ownership of this team (DESIGN.md §4.2). Both empty means no group grants + either role here — matches terdut-server's own NULLIF-on-empty-string + handling (internal/api/oidc_teams.go). + properties: + memberGroup: + type: string + ownerGroup: + type: string + type: object + serverRef: + description: serverRef names the TerdutServer this team belongs to. + properties: + name: + minLength: 1 + type: string + namespace: + type: string + required: + - name + type: object + required: + - displayName + - serverRef + type: object + status: + description: status defines the observed state of TerdutTeam + properties: + conditions: + 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 this team's own scoped credential + (DESIGN.md §6 point 3) -- pure output, always in the operator's own + namespace, under a fixed data key ("token"). + 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: + format: int64 + type: integer + serverEndpoint: + description: |- + serverEndpoint is the resolved TerdutServer's base URL, resolved once + here so no child controller (TerdutEscalationRule, TerdutDeadmanSwitch, + TerdutAlertSource) ever needs its own RBAC on terdutservers just to + find out where to send a request (DESIGN.md §5). + type: string + teamID: + description: |- + teamID is the server-side id -- needed by every child object's + controller (DESIGN.md §4.2). + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +{{- end }} diff --git a/charts/terdut-operator/templates/manager/manager.yaml b/charts/terdut-operator/templates/manager/manager.yaml new file mode 100644 index 0000000..559a0d4 --- /dev/null +++ b/charts/terdut-operator/templates/manager/manager.yaml @@ -0,0 +1,161 @@ +{{- if or (not (hasKey .Values.manager "enabled")) (.Values.manager.enabled) }} +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + control-plane: controller-manager + {{- with .Values.manager.labels }} + {{- with omit . "app.kubernetes.io/managed-by" "app.kubernetes.io/name" "helm.sh/chart" "app.kubernetes.io/instance" "control-plane" }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "controller-manager" "context" $) }} + namespace: {{ .Release.Namespace }} + {{- if .Values.manager.annotations }} + annotations: + {{- toYaml .Values.manager.annotations | nindent 4 }} + {{- end }} +spec: + {{- with .Values.manager.strategy }} + strategy: {{ toYaml . | nindent 6 }} + {{- end }} + replicas: {{ .Values.manager.replicas }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + {{- with .Values.manager.pod }} + {{- with .annotations }} + {{- with omit . "kubectl.kubernetes.io/default-container" }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + {{- end }} + labels: + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + control-plane: controller-manager + {{- with .Values.manager.pod }} + {{- with .labels }} + {{- with omit . "app.kubernetes.io/name" "helm.sh/chart" "app.kubernetes.io/instance" "app.kubernetes.io/managed-by" "control-plane" }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + {{- end }} + spec: + {{- with .Values.manager.topologySpreadConstraints }} + topologySpreadConstraints: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.manager.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.manager.tolerations }} + tolerations: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.manager.affinity }} + affinity: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.manager.nodeSelector }} + nodeSelector: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.manager.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - args: + {{- if .Values.metrics.enabled }} + - --metrics-bind-address=:{{ .Values.metrics.port }} + {{- if not .Values.metrics.secure }} + - --metrics-secure=false + {{- end }} + {{- else }} + # Bind to :0 to disable the controller-runtime managed metrics server + - --metrics-bind-address=0 + {{- end }} + - --health-probe-bind-address=:{{ .Values.manager.healthProbe.port }} + {{- range .Values.manager.args }} + - {{ tpl . $ }} + {{- end }} + command: + - /manager + env: + {{- if or .Values.manager.env (and (kindIs "map" .Values.manager.envOverrides) (not (empty .Values.manager.envOverrides))) }} + {{- if .Values.manager.env }} + {{- toYaml .Values.manager.env | nindent 10 }} + {{- end }} + {{- if kindIs "map" .Values.manager.envOverrides }} + {{- range $k, $v := .Values.manager.envOverrides }} + - name: {{ $k }} + value: {{ $v | quote }} + {{ end }} + {{- end }} + {{- else }} + [] + {{- end }} + image: "{{ .Values.manager.image.repository | default "controller" }}{{- if not (contains "@" (.Values.manager.image.repository | default "controller")) }}:{{ .Values.manager.image.tag | default .Chart.AppVersion }}{{- end }}" + {{- with .Values.manager.image.pullPolicy }} + imagePullPolicy: {{ . }} + {{- end }} + livenessProbe: + httpGet: + path: /healthz + port: {{ .Values.manager.healthProbe.port }} + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: {{ .Values.manager.healthProbe.port }} + name: health + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: {{ .Values.manager.healthProbe.port }} + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- if .Values.manager.resources }} + {{- toYaml .Values.manager.resources | nindent 10 }} + {{- else }} + {} + {{- end }} + securityContext: + {{- if .Values.manager.securityContext }} + {{- toYaml .Values.manager.securityContext | nindent 10 }} + {{- else }} + {} + {{- end }} + volumeMounts: + {{- if .Values.manager.extraVolumeMounts }} + {{- toYaml .Values.manager.extraVolumeMounts | nindent 10 }} + {{- else }} + [] + {{- end }} + securityContext: + {{- if .Values.manager.podSecurityContext }} + {{- toYaml .Values.manager.podSecurityContext | nindent 8 }} + {{- else }} + {} + {{- end }} + serviceAccountName: {{ include "terdut-operator.serviceAccountName" . }} + {{- if and (hasKey .Values.manager "terminationGracePeriodSeconds") (ne .Values.manager.terminationGracePeriodSeconds nil) }} + terminationGracePeriodSeconds: {{ .Values.manager.terminationGracePeriodSeconds }} + {{- end }} + volumes: + {{- if .Values.manager.extraVolumes }} + {{- toYaml .Values.manager.extraVolumes | nindent 8 }} + {{- else }} + [] + {{- end }} +{{- end }} diff --git a/charts/terdut-operator/templates/metrics/controller-manager-metrics-service.yaml b/charts/terdut-operator/templates/metrics/controller-manager-metrics-service.yaml new file mode 100644 index 0000000..b0c5e26 --- /dev/null +++ b/charts/terdut-operator/templates/metrics/controller-manager-metrics-service.yaml @@ -0,0 +1,22 @@ +{{- if .Values.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + control-plane: controller-manager + name: {{ include "terdut-operator.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + ports: + - name: {{ if .Values.metrics.secure }}https{{ else }}http{{ end }} + port: {{ .Values.metrics.port }} + protocol: TCP + targetPort: {{ .Values.metrics.port }} + selector: + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + control-plane: controller-manager +{{- end }} diff --git a/charts/terdut-operator/templates/network-policy/allow-metrics-traffic.yaml b/charts/terdut-operator/templates/network-policy/allow-metrics-traffic.yaml new file mode 100644 index 0000000..28c2c85 --- /dev/null +++ b/charts/terdut-operator/templates/network-policy/allow-metrics-traffic.yaml @@ -0,0 +1,25 @@ +{{- if and .Values.networkPolicy.enabled .Values.metrics.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "allow-metrics-traffic" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + podSelector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + metrics: enabled + ports: + - port: {{ .Values.metrics.port }} + protocol: TCP +{{- end }} diff --git a/charts/terdut-operator/templates/prometheus/controller-manager-metrics-monitor.yaml b/charts/terdut-operator/templates/prometheus/controller-manager-metrics-monitor.yaml new file mode 100644 index 0000000..74f41b2 --- /dev/null +++ b/charts/terdut-operator/templates/prometheus/controller-manager-metrics-monitor.yaml @@ -0,0 +1,53 @@ +{{- if .Values.prometheus.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + control-plane: controller-manager + {{- with .Values.prometheus.labels }} + {{- with omit . "app.kubernetes.io/managed-by" "app.kubernetes.io/name" "helm.sh/chart" "app.kubernetes.io/instance" "control-plane" }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- with .Values.prometheus.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "controller-manager-metrics-monitor" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + endpoints: + - {{- if .Values.metrics.secure }} + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + {{- end }} + path: /metrics + port: {{ if .Values.metrics.secure }}https{{ else }}http{{ end }} + scheme: {{ if .Values.metrics.secure }}https{{ else }}http{{ end }} + {{- if .Values.metrics.secure }} + tlsConfig: + serverName: {{ include "terdut-operator.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ .Release.Namespace }}.svc + {{- if .Values.certManager.enabled }} + ca: + secret: + name: metrics-server-cert + key: ca.crt + cert: + secret: + name: metrics-server-cert + key: tls.crt + keySecret: + name: metrics-server-cert + key: tls.key + {{- else }} + insecureSkipVerify: true + {{- end }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + control-plane: controller-manager +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/controller-manager.yaml b/charts/terdut-operator/templates/rbac/controller-manager.yaml new file mode 100644 index 0000000..c4342a0 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/controller-manager.yaml @@ -0,0 +1,21 @@ +{{- if .Values.serviceAccount.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + {{- with .Values.serviceAccount.labels }} + {{- with omit . "app.kubernetes.io/managed-by" "app.kubernetes.io/name" "helm.sh/chart" "app.kubernetes.io/instance" }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + name: {{ include "terdut-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/leader-election-role.yaml b/charts/terdut-operator/templates/rbac/leader-election-role.yaml new file mode 100644 index 0000000..7199592 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/leader-election-role.yaml @@ -0,0 +1,42 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "leader-election-role" "context" $) }} + namespace: {{ .Release.Namespace }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/charts/terdut-operator/templates/rbac/leader-election-rolebinding.yaml b/charts/terdut-operator/templates/rbac/leader-election-rolebinding.yaml new file mode 100644 index 0000000..f6a42cd --- /dev/null +++ b/charts/terdut-operator/templates/rbac/leader-election-rolebinding.yaml @@ -0,0 +1,18 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "leader-election-rolebinding" "context" $) }} + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "terdut-operator.resourceName" (dict "suffix" "leader-election-role" "context" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "terdut-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/charts/terdut-operator/templates/rbac/manager-role.yaml b/charts/terdut-operator/templates/rbac/manager-role.yaml new file mode 100644 index 0000000..05c3d14 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/manager-role.yaml @@ -0,0 +1,98 @@ +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "manager-role" "context" $) }} +rules: +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch +- apiGroups: + - "" + 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: + - events.k8s.io + resources: + - events + verbs: + - create + - patch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutalertsources + - terdutdeadmanswitches + - terdutescalationrules + - terdutservers + - terdutteams + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutalertsources/finalizers + - terdutdeadmanswitches/finalizers + - terdutescalationrules/finalizers + - terdutservers/finalizers + - terdutteams/finalizers + verbs: + - update +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutalertsources/status + - terdutdeadmanswitches/status + - terdutescalationrules/status + - terdutservers/status + - terdutteams/status + verbs: + - get + - patch + - update diff --git a/charts/terdut-operator/templates/rbac/manager-rolebinding.yaml b/charts/terdut-operator/templates/rbac/manager-rolebinding.yaml new file mode 100644 index 0000000..a16ed76 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/manager-rolebinding.yaml @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: RoleBinding +{{- else }} +kind: ClusterRoleBinding +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "manager-rolebinding" "context" $) }} +roleRef: + apiGroup: rbac.authorization.k8s.io + {{- if .Values.rbac.namespaced }} + kind: Role + {{- else }} + kind: ClusterRole + {{- end }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "manager-role" "context" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "terdut-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/charts/terdut-operator/templates/rbac/metrics-auth-role.yaml b/charts/terdut-operator/templates/rbac/metrics-auth-role.yaml new file mode 100644 index 0000000..c0e719b --- /dev/null +++ b/charts/terdut-operator/templates/rbac/metrics-auth-role.yaml @@ -0,0 +1,19 @@ +{{- if and .Values.metrics.enabled .Values.metrics.secure }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "terdut-operator.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }} +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/metrics-auth-rolebinding.yaml b/charts/terdut-operator/templates/rbac/metrics-auth-rolebinding.yaml new file mode 100644 index 0000000..faf71ad --- /dev/null +++ b/charts/terdut-operator/templates/rbac/metrics-auth-rolebinding.yaml @@ -0,0 +1,14 @@ +{{- if and .Values.metrics.enabled .Values.metrics.secure }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "terdut-operator.resourceName" (dict "suffix" "metrics-auth-rolebinding" "context" $) }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "terdut-operator.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "terdut-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/metrics-reader.yaml b/charts/terdut-operator/templates/rbac/metrics-reader.yaml new file mode 100644 index 0000000..1184c97 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/metrics-reader.yaml @@ -0,0 +1,11 @@ +{{- if and .Values.metrics.enabled .Values.metrics.secure }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "terdut-operator.resourceName" (dict "suffix" "metrics-reader" "context" $) }} +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutalertsource-admin-role.yaml b/charts/terdut-operator/templates/rbac/terdutalertsource-admin-role.yaml new file mode 100644 index 0000000..3788392 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutalertsource-admin-role.yaml @@ -0,0 +1,31 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutalertsource-admin-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutalertsources + verbs: + - '*' +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutalertsources/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutalertsource-editor-role.yaml b/charts/terdut-operator/templates/rbac/terdutalertsource-editor-role.yaml new file mode 100644 index 0000000..68a25bb --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutalertsource-editor-role.yaml @@ -0,0 +1,37 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutalertsource-editor-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutalertsources + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutalertsources/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutalertsource-viewer-role.yaml b/charts/terdut-operator/templates/rbac/terdutalertsource-viewer-role.yaml new file mode 100644 index 0000000..b7ec8f0 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutalertsource-viewer-role.yaml @@ -0,0 +1,33 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutalertsource-viewer-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutalertsources + verbs: + - get + - list + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutalertsources/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutdeadmanswitch-admin-role.yaml b/charts/terdut-operator/templates/rbac/terdutdeadmanswitch-admin-role.yaml new file mode 100644 index 0000000..0bbae1f --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutdeadmanswitch-admin-role.yaml @@ -0,0 +1,31 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutdeadmanswitch-admin-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutdeadmanswitches + verbs: + - '*' +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutdeadmanswitches/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutdeadmanswitch-editor-role.yaml b/charts/terdut-operator/templates/rbac/terdutdeadmanswitch-editor-role.yaml new file mode 100644 index 0000000..24085cd --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutdeadmanswitch-editor-role.yaml @@ -0,0 +1,37 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutdeadmanswitch-editor-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutdeadmanswitches + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutdeadmanswitches/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutdeadmanswitch-viewer-role.yaml b/charts/terdut-operator/templates/rbac/terdutdeadmanswitch-viewer-role.yaml new file mode 100644 index 0000000..5b6ec2e --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutdeadmanswitch-viewer-role.yaml @@ -0,0 +1,33 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutdeadmanswitch-viewer-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutdeadmanswitches + verbs: + - get + - list + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutdeadmanswitches/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutescalationrule-admin-role.yaml b/charts/terdut-operator/templates/rbac/terdutescalationrule-admin-role.yaml new file mode 100644 index 0000000..a2337f8 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutescalationrule-admin-role.yaml @@ -0,0 +1,31 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutescalationrule-admin-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutescalationrules + verbs: + - '*' +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutescalationrules/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutescalationrule-editor-role.yaml b/charts/terdut-operator/templates/rbac/terdutescalationrule-editor-role.yaml new file mode 100644 index 0000000..c28c4a2 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutescalationrule-editor-role.yaml @@ -0,0 +1,37 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutescalationrule-editor-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutescalationrules + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutescalationrules/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutescalationrule-viewer-role.yaml b/charts/terdut-operator/templates/rbac/terdutescalationrule-viewer-role.yaml new file mode 100644 index 0000000..7635610 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutescalationrule-viewer-role.yaml @@ -0,0 +1,33 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutescalationrule-viewer-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutescalationrules + verbs: + - get + - list + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutescalationrules/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutserver-admin-role.yaml b/charts/terdut-operator/templates/rbac/terdutserver-admin-role.yaml new file mode 100644 index 0000000..16e8d7f --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutserver-admin-role.yaml @@ -0,0 +1,31 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutserver-admin-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutservers + verbs: + - '*' +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutservers/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutserver-editor-role.yaml b/charts/terdut-operator/templates/rbac/terdutserver-editor-role.yaml new file mode 100644 index 0000000..d207787 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutserver-editor-role.yaml @@ -0,0 +1,37 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutserver-editor-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutservers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutservers/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutserver-viewer-role.yaml b/charts/terdut-operator/templates/rbac/terdutserver-viewer-role.yaml new file mode 100644 index 0000000..c7c2d04 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutserver-viewer-role.yaml @@ -0,0 +1,33 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutserver-viewer-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutservers + verbs: + - get + - list + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutservers/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutteam-admin-role.yaml b/charts/terdut-operator/templates/rbac/terdutteam-admin-role.yaml new file mode 100644 index 0000000..418a5b3 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutteam-admin-role.yaml @@ -0,0 +1,31 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutteam-admin-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutteams + verbs: + - '*' +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutteams/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutteam-editor-role.yaml b/charts/terdut-operator/templates/rbac/terdutteam-editor-role.yaml new file mode 100644 index 0000000..60dfbe1 --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutteam-editor-role.yaml @@ -0,0 +1,37 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutteam-editor-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutteams + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutteams/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/rbac/terdutteam-viewer-role.yaml b/charts/terdut-operator/templates/rbac/terdutteam-viewer-role.yaml new file mode 100644 index 0000000..ba7f65c --- /dev/null +++ b/charts/terdut-operator/templates/rbac/terdutteam-viewer-role.yaml @@ -0,0 +1,33 @@ +{{- if .Values.rbac.helpers.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "terdut-operator.resourceName" (dict "suffix" "terdutteam-viewer-role" "context" $) }} +rules: +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutteams + verbs: + - get + - list + - watch +- apiGroups: + - terdut.ryuvia.com + resources: + - terdutteams/status + verbs: + - get +{{- end }} diff --git a/charts/terdut-operator/templates/terdutserver/terdutserver.yaml b/charts/terdut-operator/templates/terdutserver/terdutserver.yaml new file mode 100644 index 0000000..636e6b5 --- /dev/null +++ b/charts/terdut-operator/templates/terdutserver/terdutserver.yaml @@ -0,0 +1,54 @@ +{{/* +Optional: one TerdutServer CR from values.yaml (DESIGN.md §10's "helm +install and get a server" path). Off by default -- see values.yaml's own +terdutServer block for the full shape and api/v1alpha1/terdutserver_types.go +for what each field validates. +*/}} +{{- if .Values.terdutServer.enabled }} +apiVersion: terdut.ryuvia.com/v1alpha1 +kind: TerdutServer +metadata: + name: {{ .Values.terdutServer.name | default (include "terdut-operator.fullname" .) }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "terdut-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + image: + repository: {{ required "terdutServer.image.repository is required when terdutServer.enabled" .Values.terdutServer.image.repository }} + tag: {{ required "terdutServer.image.tag is required when terdutServer.enabled" .Values.terdutServer.image.tag | quote }} + replicas: {{ .Values.terdutServer.replicas }} + networking: + hostname: {{ required "terdutServer.networking.hostname is required when terdutServer.enabled" .Values.terdutServer.networking.hostname | quote }} + servicePort: {{ .Values.terdutServer.networking.servicePort }} + # Not re-validated here as dsn-xor-postgresClusterRef (api/v1alpha1's own + # CEL rule on DatabaseSpec is the authority, same v1 stance as every + # other cross-field check in this operator -- DESIGN.md §13: no + # admission webhooks, CEL-only validation). This chart just has to pass + # the block through faithfully. + database: + {{- required "terdutServer.database (dsn or postgresClusterRef) is required when terdutServer.enabled" .Values.terdutServer.database | toYaml | nindent 4 }} + {{- with .Values.terdutServer.sweeper }} + sweeper: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.terdutServer.deadman }} + deadman: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.terdutServer.notify }} + notify: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.terdutServer.oidc }} + oidc: + {{- toYaml . | nindent 4 }} + {{- end }} + passwordLogin: {{ .Values.terdutServer.passwordLogin }} + {{- with .Values.terdutServer.allowedTeams }} + allowedTeams: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/terdut-operator/values.yaml b/charts/terdut-operator/values.yaml new file mode 100644 index 0000000..0033f56 --- /dev/null +++ b/charts/terdut-operator/values.yaml @@ -0,0 +1,272 @@ +## String to partially override chart.fullname template (will maintain the release name) +## +# nameOverride: "" + +## String to fully override chart.fullname template +## +# fullnameOverride: "" + +## Configure the controller manager deployment +## +manager: + ## Set to false to skip manager installation + ## + enabled: true + + replicas: 1 + + image: + repository: git.ryuvia.com/niklas/terdut-operator + ## Image tag (defaults to Chart.appVersion if not set) -- the release + ## process (`make helm-package`) always passes --app-version from the + ## tag, so leaving this unset here is what tracks a release correctly. + ## + # tag: "" + pullPolicy: IfNotPresent + + ## Arguments + ## + args: + - --leader-elect + + ## Health probes. + ## The manager serves the liveness (/healthz) and readiness (/readyz) endpoints on this port. + ## + healthProbe: + # Health probe server port + port: 8081 + + ## Environment variables + ## + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + + ## Env overrides (--set manager.envOverrides.VAR=value) + ## Same name in env above: this value takes precedence. + ## + envOverrides: {} + + ## Image pull secrets + ## + # imagePullSecrets: + # - name: myregistrykey + + ## Pod-level security settings + ## + podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + + ## Container-level security settings + ## + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + + ## Resource limits and requests + ## + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + + ## Manager pod's affinity + ## + affinity: {} + + ## Manager pod's node selector + ## + nodeSelector: {} + + ## Manager pod's tolerations + ## + tolerations: [] + + ## Deployment strategy + ## + # strategy: + # type: RollingUpdate + # rollingUpdate: + # maxSurge: 25% + # maxUnavailable: 25% + + ## Priority class name + ## + # priorityClassName: "" + + ## Topology spread constraints + ## + # topologySpreadConstraints: [] + + ## Termination grace period seconds + ## + terminationGracePeriodSeconds: 10 + + ## Custom Deployment labels + ## + # labels: {} + + ## Custom Deployment annotations + ## + # annotations: {} + + ## Custom Pod labels and annotations + ## + # pod: + # labels: {} + # annotations: {} + +## RBAC configuration +## +rbac: + ## RBAC resource scope + ## - false (default): ClusterRole/ClusterRoleBinding (all namespaces) + ## - true: Role/RoleBinding (release namespace only) + ## + namespaced: false + + ## Helper roles for CRD management (admin/editor/viewer) + ## + helpers: + ## Install convenience admin/editor/viewer roles for CRDs + ## + enabled: false + +## ServiceAccount configuration +## +serviceAccount: + # Install default ServiceAccount provided + enabled: true + + ## Existing ServiceAccount name (required when enabled=false) + ## Set to "default" to use the namespace default ServiceAccount + ## Note: When enabled=true, respects nameOverride/fullnameOverride + ## + # name: "" + + ## Custom ServiceAccount annotations + ## + # annotations: {} + + ## Custom ServiceAccount labels + ## + # labels: {} + +## Custom Resource Definitions +## +crd: + # Install CRDs with the chart + enabled: true + # Keep CRDs when uninstalling + keep: true + +## Controller metrics endpoint. +## Enable to expose /metrics endpoint +## +metrics: + enabled: true + # Metrics server port + port: 8443 + # Enable secure metrics: HTTPS with certs/auth (true) or HTTP (false). + # Note: Metrics authn/authz needs ClusterRole access. + secure: true + +## Cert-manager integration for TLS certificates. +## Required for webhook certificates and metrics endpoint certificates. +## +certManager: + enabled: false + +## Webhook server configuration +## +webhook: + enabled: false + # Webhook server port + port: 9443 + +## Prometheus ServiceMonitor for metrics scraping. +## Requires prometheus-operator to be installed in the cluster. +## +prometheus: + enabled: false + + ## Custom ServiceMonitor labels + ## + # labels: {} + + ## Custom ServiceMonitor annotations + ## + # annotations: {} + +## Network policies for controlling traffic flow. +## Enable to restrict ingress to the controller manager. +## +networkPolicy: + enabled: false + +## Optionally render one TerdutServer CR from this chart -- "helm install +## and get a server" without hand-writing a CR (DESIGN.md §10). Off by +## default: most installs only want the operator and CRDs here, then apply +## their own TerdutServer (and TerdutTeam, and so on) separately. The shape +## below mirrors api/v1alpha1/terdutserver_types.go's TerdutServerSpec +## directly -- see that file for what each field means and which are +## required. +## +terdutServer: + enabled: false + + ## This CR's own metadata.name. Defaults to the chart's own fullname if unset. + # name: "" + + image: + repository: git.ryuvia.com/niklas/terdut-server + ## Required when terdutServer.enabled. + # tag: "" + + replicas: 1 + + networking: + ## Required when terdutServer.enabled -- the hostname a future + ## HTTPRoute will carry (see NetworkingSpec's own doc comment: creating + ## that HTTPRoute isn't implemented yet). + # hostname: "" + servicePort: 8080 + + ## Exactly one of dsn or postgresClusterRef, matching DatabaseSpec's own + ## CEL rule -- required when terdutServer.enabled. Bring-your-own DSN: + ## database: + ## dsn: "postgres://terdut@terdut-postgres:5432/terdut?sslmode=require" + ## passwordSecretRef: + ## name: terdut-postgres-password + ## key: password + ## Zalando postgres-operator instead: + ## database: + ## postgresClusterRef: + ## name: terdut-postgres + database: {} + + ## All optional -- omit entirely for their terdut-server defaults. + # sweeper: + # staleAfter: 6h + # archiveAfter: 168h + # deadman: + # matchers: "alertname=Watchdog" + # timeout: 15m + # severity: critical + # notify: {} + # oidc: {} + + passwordLogin: true + + # allowedTeams: {} + diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 5c5f0b8..ad13e96 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,2 +1,8 @@ resources: - manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: controller + newTag: latest diff --git a/go.mod b/go.mod index 8ebb6f7..6176069 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - cel.dev/expr v0.25.1 // indirect + cel.dev/expr v0.25.2 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -88,7 +88,7 @@ require ( gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.82.1 // indirect + google.golang.org/grpc v1.83.1 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index a49eda7..905b2df 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= -cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -222,8 +222,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= +google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=