initial scaffold: emdash catalog, helm chart, woodpecker pipeline, ddev
- app/: Emdash scaffold (Astro 6, node target) with cmses/plugins/pages collections - app/seed/seed.json: WordPress→Emdash parity for kotkanagrilli.fi (~30 entries) - Dockerfile + docker/entrypoint.sh: multi-stage build, single PVC at /app/state - deploy/helm/: chart mirroring emdash-kotkanagrilli (single-replica, sqlite, kotkan) - deploy/fleet-overlay/: HelmRelease/source/image-automation templates for anton-helm-workloads (staging + production) - .woodpecker/container.yaml: arm64 build, three OCI tags per push (immutable 0.1.<pipeline> + floating <branch> + <branch>-latest) - .ddev/: local dev with nginx proxy to emdash on :4321 - README/DEPLOYMENT/ARCHITECTURE/CLAUDE: docs covering the three-repo pipeline (cms-plugins + anton-helm-workloads + Gitea OCI registry)
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# deploy/
|
||||
|
||||
Two sibling directories with very different lifecycles:
|
||||
|
||||
- **`helm/`** — the Helm chart that runs the pod. FluxCD pulls it
|
||||
directly from this repo on the branch matching each environment
|
||||
(no `helm push` step). Edit this in lockstep with the app code that
|
||||
depends on it.
|
||||
- **`fleet-overlay/`** — templates for the FluxCD manifests that live in
|
||||
the `anton-helm-workloads` repo. Not consumed from here — they're
|
||||
versioned alongside the chart so the chart's contract with Flux stays
|
||||
legible.
|
||||
|
||||
See `../DEPLOYMENT.md` for the end-to-end pipeline.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Fleet overlay templates
|
||||
|
||||
The YAMLs under `cms-plugins-staging/` and `cms-plugins-production/` are the
|
||||
FluxCD manifests that drive each environment. They are **not** consumed from
|
||||
this repo — they live here as a versioned blueprint, intended to be copied
|
||||
into the workloads repo that Flux watches:
|
||||
|
||||
```
|
||||
git.oleks.space/anton/helm-workloads
|
||||
├─ cms-plugins-staging/ ← copy from deploy/fleet-overlay/cms-plugins-staging/
|
||||
├─ cms-plugins-production/ ← copy from deploy/fleet-overlay/cms-plugins-production/
|
||||
└─ kustomization.yaml ← add both directories to `resources:`
|
||||
```
|
||||
|
||||
See `../../DEPLOYMENT.md` for the full pipeline and the first-time setup
|
||||
checklist (deploy keys, sops secrets, Woodpecker secrets, DNS).
|
||||
|
||||
## Shape
|
||||
|
||||
Each env directory contains five files, mirroring the emdash-kotkanagrilli
|
||||
layout in `~/projects/servers/fleet/apps/base/`:
|
||||
|
||||
- `source.yaml` — `GitRepository` pointing at this repo on the matching
|
||||
branch (`staging` / `production`), restricted to `/deploy/helm` via the
|
||||
`ignore` rule so Flux only pulls the chart.
|
||||
- `helmrelease.yaml` — `HelmRelease` consuming the chart from `./deploy/helm`
|
||||
in that `GitRepository`. Pinned by digest (see image-automation.yaml).
|
||||
- `image-automation.yaml` — `ImageRepository` + `ImagePolicy` +
|
||||
`ImageUpdateAutomation`. Watches the floating `staging` / `production`
|
||||
tag in the Gitea OCI registry, resolves the current digest, and rewrites
|
||||
the digest setter in `helmrelease.yaml` (which is what actually makes
|
||||
`helm upgrade` see a change when CI retags the image).
|
||||
- `secrets.yaml` — two Secrets per env: the SSH deploy key Flux uses to
|
||||
clone this repo (`cms-plugins-deploy-key`, in `flux-system`), and the
|
||||
pod's env-var secret (`cms-plugins-{staging,production}-secrets`, in
|
||||
`kotkan`). **Templates here are NOT encrypted** — sops-encrypt them
|
||||
before pushing to anton-helm-workloads.
|
||||
- `kustomization.yaml` — bundles the above.
|
||||
|
||||
## Why this lives in two repos
|
||||
|
||||
The chart (`deploy/helm/`) ships with the app — that way a chart change
|
||||
is reviewed and tagged alongside the code that depends on it. The
|
||||
HelmRelease references the chart as a path inside a `GitRepository`,
|
||||
not as an OCI artifact, so there's no "publish chart" step in CI.
|
||||
|
||||
The HelmRelease itself lives in the workloads repo because that repo is
|
||||
the source of truth for what runs on the kotkanagrilli.fi subdomain
|
||||
pool. Same convention as the existing `kotkanagrilli/` (legacy WP) and
|
||||
`hello-kotkan/` entries there.
|
||||
@@ -0,0 +1,48 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: cms-plugins-production
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 5m
|
||||
chart:
|
||||
spec:
|
||||
chart: ./deploy/helm
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: cms-plugins-production
|
||||
namespace: flux-system
|
||||
reconcileStrategy: Revision
|
||||
releaseName: cms-plugins-production
|
||||
targetNamespace: kotkan
|
||||
install:
|
||||
disableWait: true
|
||||
remediation:
|
||||
retries: 3
|
||||
upgrade:
|
||||
disableWait: true
|
||||
remediation:
|
||||
retries: 3
|
||||
values:
|
||||
existingSecret: cms-plugins-production-secrets
|
||||
image:
|
||||
# `tag` stays human-readable. The chart prefers `digest` when set
|
||||
# and renders `repository@<digest>` — that's what actually pins
|
||||
# the pod. Without digest pinning, helm upgrade would see no spec
|
||||
# change when CI retags the floating `production` tag.
|
||||
tag: production
|
||||
digest: "" # {"$imagepolicy": "kotkan:cms-plugins-production:digest"}
|
||||
pullPolicy: Always
|
||||
ingress:
|
||||
host: cms-plugins-production.kotkanagrilli.fi
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: kotkan
|
||||
persistence:
|
||||
size: 10Gi
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 384Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
# Watch the Gitea OCI registry for the floating `production` tag. Every
|
||||
# push to the production branch retags the new image as `production`,
|
||||
# overwriting the previous binding (OCI tag→manifest is single-valued).
|
||||
# The image's immutable `0.1.<N>` tag stays in the registry as audit.
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
kind: ImageRepository
|
||||
metadata:
|
||||
name: cms-plugins-production
|
||||
namespace: kotkan
|
||||
spec:
|
||||
image: git.oleks.space/oleks/cms-plugins
|
||||
interval: 1m
|
||||
secretRef:
|
||||
name: gitea-registry-creds
|
||||
---
|
||||
# Only the `production` floating tag is in scope. There's at most one
|
||||
# match at a time, so alphabetical ordering is a no-op — the policy
|
||||
# just resolves to that single tag's current digest.
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: cms-plugins-production
|
||||
namespace: kotkan
|
||||
spec:
|
||||
interval: 1m
|
||||
imageRepositoryRef:
|
||||
name: cms-plugins-production
|
||||
filterTags:
|
||||
pattern: '^production$'
|
||||
# Extract and reflect the resolved digest into helmrelease.yaml.
|
||||
# This enables IUA to pin by digest, which makes helm upgrade detect
|
||||
# changes when the floating tag is reassigned.
|
||||
digestReflectionPolicy: Always
|
||||
policy:
|
||||
alphabetical:
|
||||
order: asc
|
||||
---
|
||||
# IUA writes the resolved digest into helmrelease.yaml — pinning by
|
||||
# digest is what makes `helm upgrade` see a change when the floating
|
||||
# tag is reassigned (without digest, tag stays `production` literal and
|
||||
# helm upgrade is a no-op).
|
||||
#
|
||||
# NOTE: `sourceRef` must reference a GitRepository that points at
|
||||
# THIS workloads repo (anton-helm-workloads) with write access. If it
|
||||
# doesn't exist yet, create one alongside this manifest. The
|
||||
# emdash-kotkanagrilli equivalent uses `oleks-fleet-image-automation`
|
||||
# because its HelmReleases live in the fleet repo.
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
kind: ImageUpdateAutomation
|
||||
metadata:
|
||||
name: cms-plugins-production
|
||||
namespace: kotkan
|
||||
spec:
|
||||
interval: 1m
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: anton-workloads-image-automation
|
||||
namespace: flux-system
|
||||
git:
|
||||
checkout:
|
||||
ref:
|
||||
branch: main
|
||||
commit:
|
||||
author:
|
||||
email: flux-bot@oleks.space
|
||||
name: flux-bot
|
||||
messageTemplate: |
|
||||
chore(cms-plugins-production): pin new digest
|
||||
Files:
|
||||
{{ range $filename, $_ := .Changed.FileChanges -}}
|
||||
- {{ $filename }}
|
||||
{{ end -}}
|
||||
push:
|
||||
branch: main
|
||||
update:
|
||||
path: ./cms-plugins-production
|
||||
strategy: Setters
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- source.yaml
|
||||
- helmrelease.yaml
|
||||
- secrets.yaml
|
||||
- image-automation.yaml
|
||||
@@ -0,0 +1,46 @@
|
||||
# Two secrets per environment:
|
||||
# 1. cms-plugins-deploy-key — Flux's SSH key for cloning the production branch
|
||||
# of cms-plugins (only `read` on this Gitea repo).
|
||||
# One pair is shared between production + production;
|
||||
# commit it under whichever env directory is
|
||||
# applied first.
|
||||
# 2. cms-plugins-production-secrets — env vars consumed by the pod via the
|
||||
# chart's `existingSecret`. EMDASH_ENCRYPTION_KEY
|
||||
# is required; everything else is optional.
|
||||
#
|
||||
# These are TEMPLATES — encrypt them with sops before committing to the
|
||||
# anton-helm-workloads repo:
|
||||
#
|
||||
# sops --encrypt --age <recipient-key> secrets.yaml > secrets.enc.yaml
|
||||
# mv secrets.enc.yaml secrets.yaml
|
||||
#
|
||||
# Generation:
|
||||
# ssh-keygen -t ed25519 -f /tmp/cms-plugins-deploy -N ""
|
||||
# → upload /tmp/cms-plugins-deploy.pub to Gitea: Repo Settings → Deploy
|
||||
# Keys → "cms-plugins Flux deploy", read-only.
|
||||
# openssl rand -hex 32 → EMDASH_ENCRYPTION_KEY (one per env, do not reuse).
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: cms-plugins-deploy-key
|
||||
namespace: flux-system
|
||||
type: Opaque
|
||||
stringData:
|
||||
identity: |
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
REPLACE_WITH_PRIVATE_KEY
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
identity.pub: |
|
||||
ssh-ed25519 REPLACE_WITH_PUBLIC_KEY flux@cms-plugins
|
||||
known_hosts: |
|
||||
git.oleks.space REPLACE_WITH_HOST_KEY
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: cms-plugins-production-secrets
|
||||
namespace: kotkan
|
||||
type: Opaque
|
||||
stringData:
|
||||
EMDASH_ENCRYPTION_KEY: REPLACE_WITH_RANDOM_HEX_32
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
# Flux pulls the chart from the `production` branch of cms-plugins on Gitea.
|
||||
# The `ignore` rule restricts reconciliation to /deploy/helm so app-code
|
||||
# pushes don't trigger chart re-reconcile.
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: GitRepository
|
||||
metadata:
|
||||
name: cms-plugins-production
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1m0s
|
||||
url: ssh://git@git.oleks.space/oleks/cms-plugins.git
|
||||
ref:
|
||||
branch: production
|
||||
secretRef:
|
||||
name: cms-plugins-deploy-key
|
||||
ignore: |
|
||||
/*
|
||||
!/deploy/helm
|
||||
@@ -0,0 +1,46 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: cms-plugins-staging
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 5m
|
||||
chart:
|
||||
spec:
|
||||
chart: ./deploy/helm
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: cms-plugins-staging
|
||||
namespace: flux-system
|
||||
reconcileStrategy: Revision
|
||||
releaseName: cms-plugins-staging
|
||||
targetNamespace: kotkan
|
||||
install:
|
||||
disableWait: true
|
||||
remediation:
|
||||
retries: 3
|
||||
upgrade:
|
||||
disableWait: true
|
||||
remediation:
|
||||
retries: 3
|
||||
values:
|
||||
existingSecret: cms-plugins-staging-secrets
|
||||
image:
|
||||
# `tag` stays human-readable. The chart prefers `digest` when set
|
||||
# and renders `repository@<digest>` — that's what actually pins
|
||||
# the pod. Without digest pinning, helm upgrade would see no spec
|
||||
# change when CI retags the floating `staging` tag.
|
||||
tag: staging
|
||||
digest: "" # {"$imagepolicy": "kotkan:cms-plugins-staging:digest"}
|
||||
pullPolicy: Always
|
||||
ingress:
|
||||
host: cms-plugins-staging.kotkanagrilli.fi
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: kotkan
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 192Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 768Mi
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
# Watch the Gitea OCI registry for the floating `staging` tag. Every
|
||||
# push to the staging branch retags the new image as `staging`,
|
||||
# overwriting the previous binding (OCI tag→manifest is single-valued).
|
||||
# The image's immutable `0.1.<N>` tag stays in the registry as audit.
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
kind: ImageRepository
|
||||
metadata:
|
||||
name: cms-plugins-staging
|
||||
namespace: kotkan
|
||||
spec:
|
||||
image: git.oleks.space/oleks/cms-plugins
|
||||
interval: 1m
|
||||
secretRef:
|
||||
name: gitea-registry-creds
|
||||
---
|
||||
# Only the `staging` floating tag is in scope. There's at most one
|
||||
# match at a time, so alphabetical ordering is a no-op — the policy
|
||||
# just resolves to that single tag's current digest.
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
kind: ImagePolicy
|
||||
metadata:
|
||||
name: cms-plugins-staging
|
||||
namespace: kotkan
|
||||
spec:
|
||||
interval: 1m
|
||||
imageRepositoryRef:
|
||||
name: cms-plugins-staging
|
||||
filterTags:
|
||||
pattern: '^staging$'
|
||||
# Extract and reflect the resolved digest into helmrelease.yaml.
|
||||
# This enables IUA to pin by digest, which makes helm upgrade detect
|
||||
# changes when the floating tag is reassigned.
|
||||
digestReflectionPolicy: Always
|
||||
policy:
|
||||
alphabetical:
|
||||
order: asc
|
||||
---
|
||||
# IUA writes the resolved digest into helmrelease.yaml — pinning by
|
||||
# digest is what makes `helm upgrade` see a change when the floating
|
||||
# tag is reassigned (without digest, tag stays `staging` literal and
|
||||
# helm upgrade is a no-op).
|
||||
#
|
||||
# NOTE: `sourceRef` must reference a GitRepository that points at
|
||||
# THIS workloads repo (anton-helm-workloads) with write access. If it
|
||||
# doesn't exist yet, create one alongside this manifest. The
|
||||
# emdash-kotkanagrilli equivalent uses `oleks-fleet-image-automation`
|
||||
# because its HelmReleases live in the fleet repo.
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
kind: ImageUpdateAutomation
|
||||
metadata:
|
||||
name: cms-plugins-staging
|
||||
namespace: kotkan
|
||||
spec:
|
||||
interval: 1m
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: anton-workloads-image-automation
|
||||
namespace: flux-system
|
||||
git:
|
||||
checkout:
|
||||
ref:
|
||||
branch: main
|
||||
commit:
|
||||
author:
|
||||
email: flux-bot@oleks.space
|
||||
name: flux-bot
|
||||
messageTemplate: |
|
||||
chore(cms-plugins-staging): pin new digest
|
||||
Files:
|
||||
{{ range $filename, $_ := .Changed.FileChanges -}}
|
||||
- {{ $filename }}
|
||||
{{ end -}}
|
||||
push:
|
||||
branch: main
|
||||
update:
|
||||
path: ./cms-plugins-staging
|
||||
strategy: Setters
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- source.yaml
|
||||
- helmrelease.yaml
|
||||
- secrets.yaml
|
||||
- image-automation.yaml
|
||||
@@ -0,0 +1,46 @@
|
||||
# Two secrets per environment:
|
||||
# 1. cms-plugins-deploy-key — Flux's SSH key for cloning the staging branch
|
||||
# of cms-plugins (only `read` on this Gitea repo).
|
||||
# One pair is shared between staging + production;
|
||||
# commit it under whichever env directory is
|
||||
# applied first.
|
||||
# 2. cms-plugins-staging-secrets — env vars consumed by the pod via the
|
||||
# chart's `existingSecret`. EMDASH_ENCRYPTION_KEY
|
||||
# is required; everything else is optional.
|
||||
#
|
||||
# These are TEMPLATES — encrypt them with sops before committing to the
|
||||
# anton-helm-workloads repo:
|
||||
#
|
||||
# sops --encrypt --age <recipient-key> secrets.yaml > secrets.enc.yaml
|
||||
# mv secrets.enc.yaml secrets.yaml
|
||||
#
|
||||
# Generation:
|
||||
# ssh-keygen -t ed25519 -f /tmp/cms-plugins-deploy -N ""
|
||||
# → upload /tmp/cms-plugins-deploy.pub to Gitea: Repo Settings → Deploy
|
||||
# Keys → "cms-plugins Flux deploy", read-only.
|
||||
# openssl rand -hex 32 → EMDASH_ENCRYPTION_KEY (one per env, do not reuse).
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: cms-plugins-deploy-key
|
||||
namespace: flux-system
|
||||
type: Opaque
|
||||
stringData:
|
||||
identity: |
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
REPLACE_WITH_PRIVATE_KEY
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
identity.pub: |
|
||||
ssh-ed25519 REPLACE_WITH_PUBLIC_KEY flux@cms-plugins
|
||||
known_hosts: |
|
||||
git.oleks.space REPLACE_WITH_HOST_KEY
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: cms-plugins-staging-secrets
|
||||
namespace: kotkan
|
||||
type: Opaque
|
||||
stringData:
|
||||
EMDASH_ENCRYPTION_KEY: REPLACE_WITH_RANDOM_HEX_32
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
# Flux pulls the chart from the `staging` branch of cms-plugins on Gitea.
|
||||
# The `ignore` rule restricts reconciliation to /deploy/helm so app-code
|
||||
# pushes don't trigger chart re-reconcile.
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: GitRepository
|
||||
metadata:
|
||||
name: cms-plugins-staging
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1m0s
|
||||
url: ssh://git@git.oleks.space/oleks/cms-plugins.git
|
||||
ref:
|
||||
branch: staging
|
||||
secretRef:
|
||||
name: cms-plugins-deploy-key
|
||||
ignore: |
|
||||
/*
|
||||
!/deploy/helm
|
||||
@@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
*.swp
|
||||
*.tmp
|
||||
*.bak
|
||||
*.orig
|
||||
README.md
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v2
|
||||
name: cms-plugins
|
||||
description: CMS plugins catalog — Emdash-based catalog of WordPress→Emdash plugin parity
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
keywords:
|
||||
- emdash
|
||||
- cms
|
||||
- astro
|
||||
- catalog
|
||||
home: https://git.oleks.space/oleks/cms-plugins
|
||||
maintainers:
|
||||
- name: oleks
|
||||
@@ -0,0 +1,34 @@
|
||||
{{/* Expand the name of the chart. */}}
|
||||
{{- define "cms-plugins.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Create a default fully qualified app name. */}}
|
||||
{{- define "cms-plugins.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 }}
|
||||
|
||||
{{/* Common labels. Flux appends `+<sha>` to chart versions for GitRepository
|
||||
sources; `+` is illegal in k8s labels, so we replace it with `_`. */}}
|
||||
{{- define "cms-plugins.labels" -}}
|
||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
app.kubernetes.io/name: {{ include "cms-plugins.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Selector labels */}}
|
||||
{{- define "cms-plugins.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "cms-plugins.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,100 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "cms-plugins.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "cms-plugins.labels" . | nindent 4 }}
|
||||
spec:
|
||||
# SQLite is single-writer; do not scale beyond 1.
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "cms-plugins.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "cms-plugins.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/version: {{ .Values.image.tag | quote }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: cms-plugins
|
||||
# When `image.digest` is provided, pin by digest so a floating
|
||||
# tag (staging, production) doesn't confuse Helm into a no-op
|
||||
# upgrade when the underlying image changes. Tag stays as a
|
||||
# human-readable hint via the imagePullPolicy fallback path.
|
||||
image: "{{ .Values.image.repository }}{{- if .Values.image.digest -}}@{{ .Values.image.digest }}{{- else -}}:{{ .Values.image.tag }}{{- end }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
{{- with .Values.containerSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
env:
|
||||
{{- range $key, $val := .Values.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $val | quote }}
|
||||
{{- end }}
|
||||
# EMDASH_SITE_URL gates the CSRF check on plugin POST routes.
|
||||
# Astro inside the pod sees http://localhost:4321/, so without
|
||||
# this any browser request from https://<ingress.host>/ trips
|
||||
# the same-origin check. Derived from the ingress host so we
|
||||
# don't need to set it per-environment.
|
||||
{{- if and .Values.ingress.enabled .Values.ingress.host }}
|
||||
- name: EMDASH_SITE_URL
|
||||
value: "https://{{ .Values.ingress.host }}"
|
||||
{{- end }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: {{ .Values.existingSecret | default (printf "%s-secrets" (include "cms-plugins.fullname" .)) }}
|
||||
volumeMounts:
|
||||
- name: state
|
||||
mountPath: {{ .Values.persistence.mountPath }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.probes.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumes:
|
||||
- name: state
|
||||
{{- if .Values.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "cms-plugins.fullname" . }}-state
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,27 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "cms-plugins.fullname" . -}}
|
||||
# Plain Ingress object (no TLS) — TLS terminates at the Caddy reverse-proxy
|
||||
# at the cluster edge (matches the woodpecker / emdash-kotkanagrilli pattern).
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
annotations:
|
||||
caddy.oleks.space/ingress: "true"
|
||||
labels:
|
||||
{{- include "cms-plugins.labels" . | nindent 4 }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className | default "kube-system-traefik" }}
|
||||
rules:
|
||||
- host: {{ .Values.ingress.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ .Values.service.port }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,21 @@
|
||||
{{- if .Values.persistence.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "cms-plugins.fullname" . }}-state
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "cms-plugins.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
# Holds the single-writer SQLite DB and uploaded media. Keep it on
|
||||
# `helm uninstall` / chart-name changes — losing it is unrecoverable
|
||||
# data loss, not a redeployable artifact.
|
||||
helm.sh/resource-policy: keep
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: {{ .Values.persistence.storageClass | quote }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size | quote }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "cms-plugins.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "cms-plugins.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
selector:
|
||||
{{- include "cms-plugins.selectorLabels" . | nindent 4 }}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Production overrides — applied via the FluxCD HelmRelease (or directly with
|
||||
# `helm upgrade -f values-production.yaml`).
|
||||
|
||||
image:
|
||||
tag: production-latest
|
||||
|
||||
ingress:
|
||||
host: cms-plugins-production.kotkanagrilli.fi
|
||||
|
||||
persistence:
|
||||
size: 10Gi
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 384Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
@@ -0,0 +1,17 @@
|
||||
# Staging overrides — applied via the FluxCD HelmRelease (or directly with
|
||||
# `helm upgrade -f values-staging.yaml`).
|
||||
|
||||
image:
|
||||
tag: staging-latest
|
||||
|
||||
ingress:
|
||||
host: cms-plugins-staging.kotkanagrilli.fi
|
||||
|
||||
# Slimmer staging — non-critical workload, can run lean.
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 192Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 768Mi
|
||||
@@ -0,0 +1,88 @@
|
||||
# Defaults for the cms-plugins chart.
|
||||
# Per-env overrides come from values-staging.yaml / values-production.yaml
|
||||
# and from the FluxCD HelmRelease's `values:` block.
|
||||
|
||||
image:
|
||||
repository: git.oleks.space/oleks/cms-plugins
|
||||
tag: develop-latest
|
||||
# The tag is a mutable floating pointer (CI retags <branch>-latest onto
|
||||
# each new build), so kubelet must always re-pull — IfNotPresent would
|
||||
# pin the node to whatever digest it cached first and never roll.
|
||||
pullPolicy: Always
|
||||
|
||||
service:
|
||||
port: 4321
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
host: cms-plugins.kotkanagrilli.fi
|
||||
# TLS terminates at the Caddy reverse-proxy at the cluster edge
|
||||
# (matches the woodpecker / emdash-kotkanagrilli pattern). The
|
||||
# Ingress object is plain — no inline TLS, no cert-manager Certificate.
|
||||
className: kube-system-traefik
|
||||
|
||||
# SQLite is single-writer — pin to one node so the local-path PV is sticky.
|
||||
# kotkan hosts the kotkanagrilli subdomain pool, matching the
|
||||
# anton-helm-workloads convention (hello-kotkan, kotkanagrilli, etc.).
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: kotkan
|
||||
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClass: local-path
|
||||
size: 5Gi
|
||||
# Mounted at /app/state. The image symlinks /app/data.db and /app/uploads
|
||||
# into this volume, so a single PVC covers SQLite + uploaded media.
|
||||
mountPath: /app/state
|
||||
|
||||
# Plain env values (non-secret).
|
||||
env:
|
||||
HOST: "0.0.0.0"
|
||||
PORT: "4321"
|
||||
NODE_ENV: production
|
||||
DEPLOY_TARGET: node
|
||||
STATE_DIR: /app/state
|
||||
EMDASH_ALLOWED_ORIGINS: ""
|
||||
|
||||
# All secrets project from one Secret. Keys expected:
|
||||
# - EMDASH_ENCRYPTION_KEY (required)
|
||||
existingSecret: cms-plugins-secrets
|
||||
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry-creds
|
||||
|
||||
probes:
|
||||
liveness:
|
||||
# /_emdash/api/health requires auth (401 to unauthenticated requests),
|
||||
# so kubelet probes fail and the pod gets killed. The site root is
|
||||
# public and a 200 from it is a reasonable proxy for "the server is up".
|
||||
path: /
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
readiness:
|
||||
path: /
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
|
||||
podSecurityContext:
|
||||
fsGroup: 1001
|
||||
containerSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
Reference in New Issue
Block a user