This commit is contained in:
1 parent
984c7083a1
commit
7afdc4be97
29 files changed
+872
-33
No files matched your search
@@ -0,0 +1,5 @@
|
||||
.git
|
||||
.gitignore
|
||||
ci/
|
||||
*.swp
|
||||
*.bak
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: v2
|
||||
name: meguri
|
||||
description: On-call schedules, replacements and escalation policies as a source of truth
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
kubeVersion: ">=1.29.0-0"
|
||||
@@ -0,0 +1,79 @@
|
||||
# Meguri Helm chart
|
||||
|
||||
Deploys the web/worker Deployment, Service, optional Ingress and PodDisruptionBudget, and runs a
|
||||
**pre-install/pre-upgrade migrate Job** that applies Cot migrations and the declarative instance
|
||||
configuration *before* the Deployment is updated. The chart contains no secrets and no Helm-managed
|
||||
database. Requires Kubernetes 1.29+ (native sidecars) and Helm 3.
|
||||
|
||||
## What you provide
|
||||
|
||||
1. An image: `ultradesu/meguri:latest` by default (build your own from the repository `Dockerfile` and set `image.repository` / `image.tag`; pin a tag for reproducible releases).
|
||||
2. A Secret in the release namespace, named by `secretName`. All of its keys become environment
|
||||
variables:
|
||||
|
||||
```sh
|
||||
kubectl -n meguri create secret generic meguri-secrets \
|
||||
--from-literal=DATABASE_URL='postgresql://USER:PERCENT_ENCODED_PASSWORD@HOST:5432/meguri' \
|
||||
--from-literal=MEGURI_OIDC_CLIENT_SECRET='...'
|
||||
```
|
||||
|
||||
Per-tenant provider credentials (`MEGURI_JSM_<TENANT>_EMAIL` / `_TOKEN`) go in the same Secret. `extraSecretNames` lists further Secrets injected the same way, for example one that holds only `MEGURI_OIDC_CLIENT_SECRET`.
|
||||
3. Your values. The two required ones are `publicUrl` (the exact browser origin) and `secretName`;
|
||||
everything about people and access lives under `instanceConfig`.
|
||||
|
||||
## Install and upgrade
|
||||
|
||||
```sh
|
||||
helm upgrade --install meguri ./helm/meguri -n meguri --create-namespace -f my-values.yaml
|
||||
kubectl -n meguri logs job/meguri-migrate-1 # what the sync did; revision number increments
|
||||
```
|
||||
|
||||
`instanceConfig` is rendered verbatim into `/etc/meguri/config.yaml` (field reference:
|
||||
`deploy/instance-config.example.yaml`, behavior: `docs/deployment.md`). The chart schema rejects
|
||||
unknown fields and obviously invalid values at `helm lint`/`install`; the application validates it
|
||||
again, and a rejected configuration fails the migrate Job, so the release stops before any pod
|
||||
changes. A configuration change rolls the Deployment (checksum annotation) after the Job succeeded.
|
||||
|
||||
```yaml
|
||||
publicUrl: https://meguri.example.internal
|
||||
secretName: meguri-secrets
|
||||
image: {repository: ultradesu/meguri, tag: "1.4.0"}
|
||||
instanceConfig:
|
||||
auth: {password_login: false}
|
||||
oidc:
|
||||
issuer: https://sso.example.internal/realms/corp
|
||||
client_id: meguri
|
||||
instance_admin_groups: [/meguri/admins]
|
||||
workspaces:
|
||||
- key: payments
|
||||
name: Payments
|
||||
admin_groups: [/payments/leads]
|
||||
member_groups: [/payments/team]
|
||||
- key: customer-support
|
||||
name: Customer Support
|
||||
admin_groups: [/support/leads]
|
||||
member_groups: [/support/team, /support/contractors]
|
||||
```
|
||||
|
||||
Removing a workspace from `workspaces` **disables** it at the next release (data is kept; an
|
||||
instance administrator can delete it permanently). Giving a new team access is one more list entry.
|
||||
|
||||
## Break-glass administrator
|
||||
|
||||
Add `instanceConfig.auth.bootstrap_admin.usernames: [rescue]` and upgrade. The generated password is
|
||||
printed in the migrate Job log (`kubectl logs job/meguri-migrate-N`) and rotates each UTC hour; anyone who can read
|
||||
that log can sign in as an instance administrator, so remove the block as soon as SSO works again.
|
||||
Finished Jobs are deleted after `migrate.ttlSecondsAfterFinished` (a day).
|
||||
|
||||
## Notable values
|
||||
|
||||
| Value | Purpose |
|
||||
|---|---|
|
||||
| `replicaCount`, `resources`, `podDisruptionBudget` | Sizing and availability (publication is serialized by database leases, so replicas are safe). |
|
||||
| `cloudSqlProxy.*` | Cloud SQL Auth Proxy native sidecar for the app and the Job; point `DATABASE_URL` at `127.0.0.1:5432`. |
|
||||
| `serviceAccount.create` / `.annotations` | Workload Identity account, created once at install (the pre-install hook needs it). Later annotation changes need `kubectl annotate`. |
|
||||
| `ingress.*` | Host and TLS follow `publicUrl`. |
|
||||
| `allowDisableAll` | Permit a release whose `workspaces` list is empty to disable every workspace. |
|
||||
| `extraEnv`, `nodeSelector`, `tolerations`, `affinity`, `topologySpreadConstraints` | Usual scheduling and environment hooks. |
|
||||
|
||||
The demonstration mode is not available in the chart: it only runs on loopback.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Values for `helm lint` and `helm template` in CI; not a deployment example.
|
||||
publicUrl: https://meguri.example.internal
|
||||
secretName: meguri-secrets
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations:
|
||||
iam.gke.io/gcp-service-account: meguri@project.iam.gserviceaccount.com
|
||||
cloudSqlProxy:
|
||||
enabled: true
|
||||
instanceConnectionName: project:region:instance
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
tlsSecretName: meguri-tls
|
||||
@@ -0,0 +1,17 @@
|
||||
Meguri {{ .Chart.AppVersion }} is released as {{ .Release.Name }}.
|
||||
|
||||
Open {{ .Values.publicUrl }} (register {{ .Values.publicUrl | trimSuffix "/" }}/api/oidc/callback as the redirect URI of the OIDC client).
|
||||
|
||||
Workspaces and group mappings come only from `instanceConfig`; the migrate Job applied them
|
||||
before the Deployment was updated. See what it did:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs job/{{ include "meguri.fullname" . }}-migrate-{{ .Release.Revision }}
|
||||
{{- if (.Values.instanceConfig.auth).bootstrap_admin }}
|
||||
|
||||
WARNING: bootstrap_admin is enabled. Its generated password is in that Job log, so anyone who can
|
||||
read the log can sign in as an instance administrator. Remove the block when SSO works again.
|
||||
{{- end }}
|
||||
{{- if not .Values.instanceConfig.workspaces }}
|
||||
|
||||
No workspaces are configured.
|
||||
{{- end }}
|
||||
@@ -0,0 +1,84 @@
|
||||
{{- define "meguri.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 40 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "meguri.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 40 | trimSuffix "-" -}}
|
||||
{{- else if contains .Chart.Name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 40 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 40 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "meguri.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "meguri.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "meguri.labels" -}}
|
||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{ include "meguri.selectorLabels" . }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "meguri.image" -}}
|
||||
{{- printf "%s:%s" .Values.image.repository (default .Chart.AppVersion .Values.image.tag) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "meguri.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
{{- default (include "meguri.fullname" .) .Values.serviceAccount.name -}}
|
||||
{{- else -}}
|
||||
{{- default "default" .Values.serviceAccount.name -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* The configuration file exactly as the application reads it. */}}
|
||||
{{- define "meguri.instanceConfig" -}}
|
||||
{{- toYaml .Values.instanceConfig -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Fails the render early with a readable message. Outputs nothing. */}}
|
||||
{{- define "meguri.requirements" -}}
|
||||
{{- $_ := required "publicUrl is required: the exact browser origin, for example https://meguri.example.internal" .Values.publicUrl -}}
|
||||
{{- $_ := required "secretName is required: a Secret with DATABASE_URL (and MEGURI_OIDC_CLIENT_SECRET when oidc is configured)" .Values.secretName -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "meguri.cloudSqlProxy" -}}
|
||||
- name: cloud-sql-proxy
|
||||
restartPolicy: Always
|
||||
image: {{ .Values.cloudSqlProxy.image | quote }}
|
||||
args:
|
||||
{{- if .Values.cloudSqlProxy.privateIp }}
|
||||
- --private-ip
|
||||
{{- end }}
|
||||
- --structured-logs
|
||||
- --port={{ .Values.cloudSqlProxy.port }}
|
||||
- --health-check
|
||||
- --http-address=0.0.0.0
|
||||
- --http-port=9090
|
||||
- {{ required "cloudSqlProxy.instanceConnectionName is required when the proxy is enabled" .Values.cloudSqlProxy.instanceConnectionName | quote }}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities: {drop: [ALL]}
|
||||
resources:
|
||||
{{- toYaml .Values.cloudSqlProxy.resources | nindent 4 }}
|
||||
startupProbe:
|
||||
httpGet: {path: /startup, port: 9090}
|
||||
periodSeconds: 2
|
||||
failureThreshold: 30
|
||||
{{- end -}}
|
||||
|
||||
{{/* Environment shared by the application and the migrate Job. */}}
|
||||
{{- define "meguri.env" -}}
|
||||
- {name: MEGURI_BIND, value: "0.0.0.0:3000"}
|
||||
- {name: MEGURI_PUBLIC_URL, value: {{ .Values.publicUrl | trimSuffix "/" | quote }}}
|
||||
- {name: MEGURI_CONFIG_FILE, value: /etc/meguri/config.yaml}
|
||||
- {name: RUST_LOG, value: {{ .Values.logLevel | quote }}}
|
||||
{{- with .Values.extraEnv }}
|
||||
{{ toYaml . }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,25 @@
|
||||
{{- include "meguri.requirements" . -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "meguri.fullname" . }}-instance-config
|
||||
labels:
|
||||
{{- include "meguri.labels" . | nindent 4 }}
|
||||
data:
|
||||
config.yaml: |
|
||||
{{- include "meguri.instanceConfig" . | nindent 4 }}
|
||||
---
|
||||
{{- /* The migrate Job runs before ordinary resources are updated, so it reads its own copy. */}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "meguri.fullname" . }}-instance-config-migrate
|
||||
labels:
|
||||
{{- include "meguri.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
helm.sh/hook: pre-install,pre-upgrade
|
||||
helm.sh/hook-weight: "-10"
|
||||
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
|
||||
data:
|
||||
config.yaml: |
|
||||
{{- include "meguri.instanceConfig" . | nindent 4 }}
|
||||
@@ -0,0 +1,89 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "meguri.fullname" . }}
|
||||
labels:
|
||||
{{- include "meguri.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "meguri.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "meguri.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: app
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
checksum/instance-config: {{ include "meguri.instanceConfig" . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
serviceAccountName: {{ include "meguri.serviceAccountName" . }}
|
||||
terminationGracePeriodSeconds: 60
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile: {type: RuntimeDefault}
|
||||
{{- if .Values.cloudSqlProxy.enabled }}
|
||||
initContainers:
|
||||
{{- include "meguri.cloudSqlProxy" . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: instance-config
|
||||
configMap:
|
||||
name: {{ include "meguri.fullname" . }}-instance-config
|
||||
containers:
|
||||
- name: meguri
|
||||
image: {{ include "meguri.image" . | quote }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports: [{name: http, containerPort: 3000}]
|
||||
envFrom:
|
||||
- secretRef: {name: {{ required "secretName is required" .Values.secretName | quote }}}
|
||||
{{- range .Values.extraSecretNames }}
|
||||
- secretRef: {name: {{ . | quote }}}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- include "meguri.env" . | nindent 12 }}
|
||||
- {name: MEGURI_ROLE, value: {{ .Values.role | quote }}}
|
||||
- {name: MEGURI_SKIP_MIGRATIONS, value: "true"}
|
||||
volumeMounts:
|
||||
- {name: instance-config, mountPath: /etc/meguri, readOnly: true}
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities: {drop: [ALL]}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
startupProbe:
|
||||
httpGet: {path: /health/ready, port: http}
|
||||
periodSeconds: 2
|
||||
failureThreshold: 60
|
||||
readinessProbe:
|
||||
httpGet: {path: /health/ready, port: http}
|
||||
livenessProbe:
|
||||
httpGet: {path: /health/live, port: http}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.topologySpreadConstraints }}
|
||||
topologySpreadConstraints:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,32 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- $url := urlParse (required "publicUrl is required" .Values.publicUrl) }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "meguri.fullname" . }}
|
||||
labels:
|
||||
{{- include "meguri.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tlsSecretName }}
|
||||
tls:
|
||||
- hosts: [{{ $url.hostname | quote }}]
|
||||
secretName: {{ .Values.ingress.tlsSecretName }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ $url.hostname | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "meguri.fullname" . }}
|
||||
port: {name: http}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,62 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "meguri.fullname" . }}-migrate-{{ .Release.Revision }}
|
||||
labels:
|
||||
{{- include "meguri.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: migrate
|
||||
annotations:
|
||||
helm.sh/hook: pre-install,pre-upgrade
|
||||
helm.sh/hook-weight: "0"
|
||||
helm.sh/hook-delete-policy: before-hook-creation
|
||||
spec:
|
||||
backoffLimit: 0
|
||||
activeDeadlineSeconds: {{ .Values.migrate.activeDeadlineSeconds }}
|
||||
ttlSecondsAfterFinished: {{ .Values.migrate.ttlSecondsAfterFinished }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "meguri.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: migrate
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
serviceAccountName: {{ include "meguri.serviceAccountName" . }}
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile: {type: RuntimeDefault}
|
||||
{{- if .Values.cloudSqlProxy.enabled }}
|
||||
initContainers:
|
||||
{{- include "meguri.cloudSqlProxy" . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: instance-config
|
||||
configMap:
|
||||
name: {{ include "meguri.fullname" . }}-instance-config-migrate
|
||||
containers:
|
||||
- name: migrate
|
||||
image: {{ include "meguri.image" . | quote }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
envFrom:
|
||||
- secretRef: {name: {{ required "secretName is required" .Values.secretName | quote }}}
|
||||
{{- range .Values.extraSecretNames }}
|
||||
- secretRef: {name: {{ . | quote }}}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- include "meguri.env" . | nindent 12 }}
|
||||
- {name: MEGURI_ROLE, value: migrate}
|
||||
- {name: MEGURI_SKIP_MIGRATIONS, value: "false"}
|
||||
{{- if .Values.allowDisableAll }}
|
||||
- {name: MEGURI_ALLOW_DISABLE_ALL, value: "true"}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- {name: instance-config, mountPath: /etc/meguri, readOnly: true}
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities: {drop: [ALL]}
|
||||
resources:
|
||||
{{- toYaml .Values.migrate.resources | nindent 12 }}
|
||||
@@ -0,0 +1,14 @@
|
||||
{{- if and .Values.podDisruptionBudget.enabled (gt (int .Values.replicaCount) 1) }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "meguri.fullname" . }}
|
||||
labels:
|
||||
{{- include "meguri.labels" . | nindent 4 }}
|
||||
spec:
|
||||
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "meguri.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: app
|
||||
{{- end }}
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "meguri.fullname" . }}
|
||||
labels:
|
||||
{{- include "meguri.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "meguri.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: app
|
||||
ports:
|
||||
- {name: http, port: {{ .Values.service.port }}, targetPort: http}
|
||||
@@ -0,0 +1,16 @@
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- /* A hook, because the pre-install migrate Job needs it before ordinary resources exist. */}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "meguri.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "meguri.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
helm.sh/hook: pre-install
|
||||
helm.sh/hook-weight: "-20"
|
||||
helm.sh/hook-delete-policy: before-hook-creation
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["publicUrl", "secretName", "instanceConfig"],
|
||||
"properties": {
|
||||
"image": {
|
||||
"type": "object",
|
||||
"required": ["repository"],
|
||||
"properties": {
|
||||
"repository": {"type": "string", "minLength": 1},
|
||||
"tag": {"type": "string"},
|
||||
"pullPolicy": {"enum": ["Always", "IfNotPresent", "Never"]}
|
||||
}
|
||||
},
|
||||
"publicUrl": {"type": "string", "pattern": "^https?://[^/\\s]+/?$"},
|
||||
"secretName": {"type": "string", "minLength": 1},
|
||||
"extraSecretNames": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
||||
"replicaCount": {"type": "integer", "minimum": 1},
|
||||
"role": {"enum": ["all", "web", "worker"]},
|
||||
"logLevel": {"type": "string"},
|
||||
"allowDisableAll": {"type": "boolean"},
|
||||
"cloudSqlProxy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"instanceConnectionName": {"type": "string"},
|
||||
"port": {"type": "integer", "minimum": 1, "maximum": 65535}
|
||||
}
|
||||
},
|
||||
"instanceConfig": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"password_login": {"type": "boolean"},
|
||||
"bootstrap_admin": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["usernames"],
|
||||
"properties": {
|
||||
"usernames": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {"type": "string", "pattern": "^[a-z0-9._-]{3,64}$"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["issuer", "client_id"],
|
||||
"properties": {
|
||||
"issuer": {"type": "string", "pattern": "^https://[^\\s]+$"},
|
||||
"client_id": {"type": "string", "minLength": 1},
|
||||
"scopes": {"type": "array", "items": {"type": "string"}, "contains": {"const": "openid"}},
|
||||
"groups_claim": {"type": "string", "minLength": 1},
|
||||
"groups_sources": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"enum": ["id_token", "userinfo"]}
|
||||
}
|
||||
}
|
||||
},
|
||||
"instance_admin_groups": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["key", "name", "admin_groups"],
|
||||
"properties": {
|
||||
"key": {"type": "string", "pattern": "^[a-z0-9]([a-z0-9-]{1,62})[a-z0-9]$"},
|
||||
"name": {"type": "string", "minLength": 1, "maxLength": 120},
|
||||
"admin_groups": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
|
||||
"member_groups": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
# Meguri Helm chart. Secrets are never part of the chart: create the Secret named by
|
||||
# `secretName` first (see README.md). Everything under `instanceConfig` is rendered verbatim into
|
||||
# the instance configuration file; the chart schema and the application validate it strictly.
|
||||
|
||||
image:
|
||||
repository: ultradesu/meguri
|
||||
# Pin an immutable tag or digest for reproducible releases; `latest` is re-pulled on every restart.
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
imagePullSecrets: []
|
||||
|
||||
# Exact browser origin (scheme, host, port) of the service. Cookies, CSRF checks and the OIDC
|
||||
# redirect URI ({publicUrl}/api/oidc/callback) depend on it. Required.
|
||||
publicUrl: ""
|
||||
|
||||
# Name of a Secret holding at least DATABASE_URL (postgresql://USER:PERCENT_ENCODED_PASSWORD@HOST:5432/DB)
|
||||
# and, when instanceConfig.oidc is set, MEGURI_OIDC_CLIENT_SECRET. All of its keys become environment
|
||||
# variables, so per-tenant provider credentials (MEGURI_JSM_<TENANT>_EMAIL / _TOKEN) belong there too.
|
||||
# Required.
|
||||
secretName: ""
|
||||
# Further Secrets whose keys are also injected as environment variables (for example a separate
|
||||
# MEGURI_OIDC_CLIENT_SECRET kept apart from the database credentials).
|
||||
extraSecretNames: []
|
||||
|
||||
replicaCount: 2
|
||||
# all = web and publication worker in each replica; web = HTTP only (add a separate release
|
||||
# with role worker if you split them).
|
||||
role: all
|
||||
logLevel: meguri=info,cot=warn
|
||||
|
||||
# Disables every existing workspace when instanceConfig.workspaces is empty. Leave false.
|
||||
allowDisableAll: false
|
||||
|
||||
# The instance configuration. Written to /etc/meguri/config.yaml. The migrate hook applies it at
|
||||
# every install and upgrade: workspaces are created, adopted, renamed, enabled or DISABLED (never
|
||||
# deleted) so that the database matches this list. Field reference: deploy/instance-config.example.yaml.
|
||||
instanceConfig:
|
||||
auth:
|
||||
# Lets ordinary password accounts (created by an instance administrator) sign in.
|
||||
password_login: false
|
||||
# Break-glass instance administrators. Remove in production. While present, their generated
|
||||
# password is written to the migrate Job log.
|
||||
# bootstrap_admin:
|
||||
# usernames: [rescue]
|
||||
oidc:
|
||||
issuer: https://sso.example.internal/realms/corp
|
||||
client_id: meguri
|
||||
scopes: [openid, profile, email]
|
||||
groups_claim: groups
|
||||
groups_sources: [id_token, userinfo]
|
||||
instance_admin_groups:
|
||||
- /meguri/admins
|
||||
workspaces:
|
||||
- key: payments
|
||||
name: Payments
|
||||
admin_groups:
|
||||
- /payments/leads
|
||||
member_groups:
|
||||
- /payments/team
|
||||
|
||||
migrate:
|
||||
# Runs the Cot migrations and the configuration sync before the Deployment is touched.
|
||||
activeDeadlineSeconds: 300
|
||||
# Finished Jobs (and the log that may contain bootstrap passwords) are removed after this long.
|
||||
ttlSecondsAfterFinished: 86400
|
||||
resources:
|
||||
requests: {cpu: 250m, memory: 128Mi}
|
||||
limits: {memory: 256Mi}
|
||||
|
||||
resources:
|
||||
requests: {cpu: 250m, memory: 256Mi}
|
||||
limits: {memory: 512Mi}
|
||||
|
||||
# Optional Cloud SQL Auth Proxy as a native sidecar (Kubernetes 1.29+), for the application and
|
||||
# the migrate Job. Point DATABASE_URL at 127.0.0.1:<port>.
|
||||
cloudSqlProxy:
|
||||
enabled: false
|
||||
image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.1
|
||||
instanceConnectionName: "" # PROJECT:REGION:INSTANCE
|
||||
privateIp: true
|
||||
port: 5432
|
||||
resources:
|
||||
requests: {cpu: 100m, memory: 128Mi}
|
||||
limits: {memory: 256Mi}
|
||||
|
||||
serviceAccount:
|
||||
# Created once, at install, because the migrate hook runs before ordinary resources exist.
|
||||
# Later annotation changes need `kubectl annotate`. Leave create false to use an existing
|
||||
# account named `name` (or the namespace default).
|
||||
create: false
|
||||
name: ""
|
||||
annotations: {}
|
||||
# iam.gke.io/gcp-service-account: meguri@PROJECT.iam.gserviceaccount.com
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# Host and TLS follow publicUrl. Set tlsSecretName for an https publicUrl terminated at the ingress.
|
||||
tlsSecretName: ""
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: true
|
||||
minAvailable: 1
|
||||
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
extraEnv: []
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
topologySpreadConstraints: []
|
||||
Reference in new issue
Block a user