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,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