feat(deploy): add Helm chart for k3s and production Kubernetes

Include values profiles, secrets handling, TLS hooks, HPA/PDB, and in-cluster data services.
This commit is contained in:
ahp
2026-10-07 19:40:26 +03:30
parent dbd9a4ddb0
commit dc1b9e9cc0
25 changed files with 1983 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
.DS_Store
.git
*.md
*.tgz
secrets*.yaml
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v2
name: pargar
description: Pargar gated LMS - Go API, Next.js frontend, Postgres, Redis, S3-compatible storage
type: application
version: 0.2.0
appVersion: "0.1.0"
keywords:
- lms
- pargar
- traefik
maintainers:
- name: Pargar
home: https://github.com/example/pargar
sources:
- https://github.com/example/pargar
+26
View File
@@ -0,0 +1,26 @@
Pargar {{ .Chart.AppVersion }} installed as release "{{ .Release.Name }}" in namespace "{{ .Release.Namespace }}".
Public URL: {{ .Values.global.publicUrl }}
Domain: {{ .Values.global.domain }}
1) Ensure DNS /hosts points to your Traefik entrypoint:
{{ .Values.global.domain }} → Traefik LoadBalancer / node IP
2) Images must exist on the cluster nodes (k3s local):
docker compose -f deploy/compose/compose.yaml -f deploy/compose/compose.dev.yaml build
docker save pargar-backend:local pargar-frontend:local | sudo k3s ctr images import -
3) Upgrade / install:
helm upgrade --install {{ .Release.Name }} ./deploy/helm/pargar \
-n {{ .Release.Namespace }} --create-namespace \
-f deploy/helm/pargar/values-k3s.yaml
4) Health:
kubectl -n {{ .Release.Namespace }} get pods,ingressroute,svc
curl -k {{ .Values.global.publicUrl }}/api/health
{{- if not .Values.ingress.tls.existingSecret }}
TLS: chart-managed self-signed certificate (browser warning expected).
For production set ingress.tls.existingSecret (cert-manager).
{{- end }}
+110
View File
@@ -0,0 +1,110 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "pargar.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "pargar.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 }}
{{- define "pargar.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "pargar.labels" -}}
helm.sh/chart: {{ include "pargar.chart" . }}
{{ include "pargar.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "pargar.selectorLabels" -}}
app.kubernetes.io/name: {{ include "pargar.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- define "pargar.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "pargar.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{- define "pargar.appSecretName" -}}
{{- if .Values.backend.existingSecret }}
{{- .Values.backend.existingSecret }}
{{- else }}
{{- printf "%s-app" (include "pargar.fullname" .) }}
{{- end }}
{{- end }}
{{- define "pargar.postgresSecretName" -}}
{{- if .Values.postgres.existingSecret }}
{{- .Values.postgres.existingSecret }}
{{- else }}
{{- printf "%s-postgres" (include "pargar.fullname" .) }}
{{- end }}
{{- end }}
{{- define "pargar.redisSecretName" -}}
{{- if .Values.redis.existingSecret }}
{{- .Values.redis.existingSecret }}
{{- else }}
{{- printf "%s-redis" (include "pargar.fullname" .) }}
{{- end }}
{{- end }}
{{- define "pargar.rustfsSecretName" -}}
{{- if .Values.rustfs.existingSecret }}
{{- .Values.rustfs.existingSecret }}
{{- else }}
{{- printf "%s-rustfs" (include "pargar.fullname" .) }}
{{- end }}
{{- end }}
{{- define "pargar.tlsSecretName" -}}
{{- if .Values.ingress.tls.existingSecret }}
{{- .Values.ingress.tls.existingSecret }}
{{- else }}
{{- printf "%s-tls" (include "pargar.fullname" .) }}
{{- end }}
{{- end }}
{{- define "pargar.postgresHost" -}}
{{- printf "%s-postgres" (include "pargar.fullname" .) }}
{{- end }}
{{- define "pargar.redisHost" -}}
{{- printf "%s-redis" (include "pargar.fullname" .) }}
{{- end }}
{{- define "pargar.rustfsHost" -}}
{{- printf "%s-rustfs" (include "pargar.fullname" .) }}
{{- end }}
{{/*
Resolve container image: optional global.imageRegistry + repository:tag
Usage: {{ include "pargar.image" (dict "root" . "image" .Values.backend.image) }}
*/}}
{{- define "pargar.image" -}}
{{- $registry := .root.Values.global.imageRegistry | default "" -}}
{{- $repo := .image.repository -}}
{{- $tag := .image.tag | default .root.Chart.AppVersion | toString -}}
{{- if $registry -}}
{{- printf "%s/%s:%s" (trimSuffix "/" $registry) $repo $tag -}}
{{- else -}}
{{- printf "%s:%s" $repo $tag -}}
{{- end -}}
{{- end }}
+204
View File
@@ -0,0 +1,204 @@
{{- if .Values.backend.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "pargar.fullname" . }}-backend
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
type: ClusterIP
ports:
- name: http
port: {{ .Values.backend.service.port }}
targetPort: http
selector:
{{- include "pargar.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "pargar.fullname" . }}-backend
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
{{- if not .Values.backend.autoscaling.enabled }}
replicas: {{ .Values.backend.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: backend
template:
metadata:
labels:
{{- include "pargar.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: backend
spec:
serviceAccountName: {{ include "pargar.serviceAccountName" . }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: backend
image: {{ include "pargar.image" (dict "root" . "image" .Values.backend.image) | quote }}
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
ports:
- name: http
containerPort: {{ .Values.backend.service.port }}
env:
- name: PORT
value: {{ .Values.backend.service.port | quote }}
- name: PUBLIC_URL
value: {{ .Values.global.publicUrl | quote }}
- name: FRONTEND_ORIGINS
value: {{ .Values.global.frontendOrigins | quote }}
- name: TRUSTED_PROXY_CIDRS
value: {{ .Values.global.trustedProxyCIDRs | quote }}
- name: DEV_MODE
value: {{ .Values.backend.env.devMode | quote }}
- name: SEED_CURRICULUM
value: {{ .Values.backend.env.seedCurriculum | quote }}
- name: ADMIN_NAME
value: {{ .Values.backend.admin.name | quote }}
- name: ADMIN_USERNAME
value: {{ .Values.backend.admin.username | quote }}
- name: ADMIN_EMAIL
value: {{ .Values.backend.admin.email | quote }}
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "pargar.appSecretName" . }}
key: jwt-secret
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "pargar.appSecretName" . }}
key: admin-password
- name: METRICS_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "pargar.appSecretName" . }}
key: metrics-token
optional: true
- name: DONATION_ENABLED
value: {{ .Values.backend.env.donationEnabled | quote }}
- name: PHYSICAL_CERT_ENABLED
value: {{ .Values.backend.env.physicalCertEnabled | quote }}
- name: PHYSICAL_CERT_PRICE_IRR
value: {{ .Values.backend.env.physicalCertPriceIrr | quote }}
- name: PHYSICAL_CERT_WINDOW_DAYS
value: {{ .Values.backend.env.physicalCertWindowDays | quote }}
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: {{ .Values.backend.env.otelEndpoint | quote }}
- name: OTEL_SERVICE_NAME
value: {{ .Values.backend.env.otelServiceName | quote }}
- name: STORAGE_DRIVER
value: s3
- name: STORAGE_REGION
value: {{ .Values.rustfs.region | quote }}
- name: STORAGE_BUCKET
value: {{ .Values.rustfs.bucket | quote }}
{{- if .Values.postgres.enabled }}
- name: POSTGRES_USER
value: {{ .Values.postgres.auth.username | quote }}
- name: POSTGRES_DB
value: {{ .Values.postgres.auth.database | quote }}
- name: POSTGRES_HOST
value: {{ include "pargar.postgresHost" . | quote }}
- name: POSTGRES_PORT
value: "5432"
- name: POSTGRES_SSLMODE
value: "disable"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "pargar.postgresSecretName" . }}
key: postgres-password
{{- else }}
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "pargar.appSecretName" . }}
key: database-url
{{- end }}
{{- if .Values.redis.enabled }}
- name: REDIS_HOST
value: {{ include "pargar.redisHost" . | quote }}
- name: REDIS_PORT
value: "6379"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "pargar.redisSecretName" . }}
key: redis-password
{{- else }}
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: {{ include "pargar.appSecretName" . }}
key: redis-url
{{- end }}
{{- if .Values.rustfs.enabled }}
- name: STORAGE_ENDPOINT
value: {{ printf "http://%s:9000" (include "pargar.rustfsHost" .) | quote }}
- name: STORAGE_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ include "pargar.rustfsSecretName" . }}
key: access-key
- name: STORAGE_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "pargar.rustfsSecretName" . }}
key: secret-key
{{- else }}
- name: STORAGE_ENDPOINT
value: {{ required "external.storageEndpoint required when rustfs.enabled=false" .Values.external.storageEndpoint | quote }}
- name: STORAGE_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ include "pargar.appSecretName" . }}
key: storage-access-key
- name: STORAGE_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "pargar.appSecretName" . }}
key: storage-secret-key
{{- end }}
readinessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 15
periodSeconds: 10
{{- with .Values.backend.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
@@ -0,0 +1,79 @@
{{- if and .Values.postgres.enabled .Values.backup.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "pargar.fullname" . }}-backups
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: backup
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.backup.persistence.storageClass }}
storageClassName: {{ .Values.backup.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.backup.persistence.size | quote }}
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "pargar.fullname" . }}-pg-backup
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: backup
spec:
schedule: {{ .Values.backup.schedule | quote }}
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
metadata:
labels:
{{- include "pargar.selectorLabels" . | nindent 12 }}
app.kubernetes.io/component: backup
spec:
restartPolicy: OnFailure
securityContext:
fsGroup: 999
containers:
- name: pg-dump
image: {{ .Values.backup.image | quote }}
imagePullPolicy: IfNotPresent
env:
- name: PGHOST
value: {{ include "pargar.postgresHost" . | quote }}
- name: PGPORT
value: "5432"
- name: PGUSER
value: {{ .Values.postgres.auth.username | quote }}
- name: PGDATABASE
value: {{ .Values.postgres.auth.database | quote }}
- name: RETENTION_DAYS
value: {{ .Values.backup.retentionDays | quote }}
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: {{ include "pargar.postgresSecretName" . }}
key: postgres-password
command:
- /bin/sh
- -ec
- |
set -euo pipefail
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
OUT="/backups/pargar-${STAMP}.sql.gz"
echo "Writing ${OUT}"
pg_dump --no-owner --no-acl | gzip -c > "${OUT}"
find /backups -type f -name 'pargar-*.sql.gz' -mtime +"${RETENTION_DAYS}" -print -delete || true
ls -lh /backups || true
volumeMounts:
- name: backups
mountPath: /backups
volumes:
- name: backups
persistentVolumeClaim:
claimName: {{ include "pargar.fullname" . }}-backups
{{- end }}
@@ -0,0 +1,23 @@
{{- if and .Values.ingress.enabled .Values.ingress.tls.enabled .Values.certManager.enabled }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "pargar.fullname" . }}-tls
labels:
{{- include "pargar.labels" . | nindent 4 }}
spec:
secretName: {{ include "pargar.tlsSecretName" . }}
duration: {{ .Values.certManager.duration | default "2160h" | quote }}
renewBefore: {{ .Values.certManager.renewBefore | default "360h" | quote }}
issuerRef:
name: {{ required "certManager.issuerRef.name required when certManager.enabled" .Values.certManager.issuerRef.name | quote }}
kind: {{ .Values.certManager.issuerRef.kind | default "ClusterIssuer" | quote }}
{{- if .Values.certManager.issuerRef.group }}
group: {{ .Values.certManager.issuerRef.group | quote }}
{{- end }}
dnsNames:
- {{ .Values.global.domain | quote }}
{{- range .Values.certManager.extraDnsNames }}
- {{ . | quote }}
{{- end }}
{{- end }}
@@ -0,0 +1,83 @@
{{- if .Values.frontend.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "pargar.fullname" . }}-frontend
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
type: ClusterIP
ports:
- name: http
port: {{ .Values.frontend.service.port }}
targetPort: http
selector:
{{- include "pargar.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: frontend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "pargar.fullname" . }}-frontend
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
{{- if not .Values.frontend.autoscaling.enabled }}
replicas: {{ .Values.frontend.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: frontend
template:
metadata:
labels:
{{- include "pargar.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: frontend
spec:
serviceAccountName: {{ include "pargar.serviceAccountName" . }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: frontend
image: {{ include "pargar.image" (dict "root" . "image" .Values.frontend.image) | quote }}
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
env:
- name: NODE_ENV
value: production
ports:
- name: http
containerPort: {{ .Values.frontend.service.port }}
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 15
periodSeconds: 10
{{- with .Values.frontend.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
+67
View File
@@ -0,0 +1,67 @@
{{- if and .Values.backend.enabled .Values.backend.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "pargar.fullname" . }}-backend
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "pargar.fullname" . }}-backend
minReplicas: {{ .Values.backend.autoscaling.minReplicas }}
maxReplicas: {{ .Values.backend.autoscaling.maxReplicas }}
metrics:
{{- if .Values.backend.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.backend.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.backend.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.backend.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
---
{{- end }}
{{- if and .Values.frontend.enabled .Values.frontend.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "pargar.fullname" . }}-frontend
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "pargar.fullname" . }}-frontend
minReplicas: {{ .Values.frontend.autoscaling.minReplicas }}
maxReplicas: {{ .Values.frontend.autoscaling.maxReplicas }}
metrics:
{{- if .Values.frontend.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.frontend.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.frontend.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.frontend.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
+50
View File
@@ -0,0 +1,50 @@
{{- if and .Values.ingress.enabled (not .Values.ingress.useIngressRoute) }}
{{- $fullName := include "pargar.fullname" . }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "pargar.labels" . | nindent 4 }}
annotations:
traefik.ingress.kubernetes.io/router.middlewares: {{ .Release.Namespace }}-{{ $fullName }}-security-headers@kubernetescrd,{{ .Release.Namespace }}-{{ $fullName }}-compress@kubernetescrd
{{- if .Values.ingress.tls.clusterIssuer }}
cert-manager.io/cluster-issuer: {{ .Values.ingress.tls.clusterIssuer | quote }}
{{- end }}
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className | quote }}
{{- if .Values.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.global.domain | quote }}
secretName: {{ include "pargar.tlsSecretName" . }}
{{- end }}
rules:
- host: {{ .Values.global.domain | quote }}
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: {{ $fullName }}-backend
port:
number: {{ .Values.backend.service.port }}
- path: /media
pathType: Prefix
backend:
service:
name: {{ $fullName }}-backend
port:
number: {{ .Values.backend.service.port }}
- path: /
pathType: Prefix
backend:
service:
name: {{ $fullName }}-frontend
port:
number: {{ .Values.frontend.service.port }}
{{- end }}
@@ -0,0 +1,97 @@
{{- if and .Values.ingress.enabled .Values.ingress.useIngressRoute }}
{{- $fullName := include "pargar.fullname" . }}
{{- $domain := .Values.global.domain }}
{{- $tlsSecret := include "pargar.tlsSecretName" . }}
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: {{ $fullName }}-http
labels:
{{- include "pargar.labels" . | nindent 4 }}
spec:
entryPoints:
- web
routes:
- match: Host(`{{ $domain }}`)
kind: Rule
{{- if .Values.middlewares.redirectHttps }}
middlewares:
- name: {{ $fullName }}-redirect-https
{{- end }}
services:
- name: {{ $fullName }}-frontend
port: {{ .Values.frontend.service.port }}
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: {{ $fullName }}-https
labels:
{{- include "pargar.labels" . | nindent 4 }}
spec:
entryPoints:
- websecure
routes:
# Certificate public API — highest priority + noindex
- match: Host(`{{ $domain }}`) && PathPrefix(`/api/certificates`)
kind: Rule
priority: 110
middlewares:
{{- if .Values.middlewares.securityHeaders }}
- name: {{ $fullName }}-security-headers
{{- end }}
{{- if .Values.middlewares.compress }}
- name: {{ $fullName }}-compress
{{- end }}
- name: {{ $fullName }}-noindex
services:
- name: {{ $fullName }}-backend
port: {{ .Values.backend.service.port }}
# API + media (+ WebSocket /api/ws)
- match: Host(`{{ $domain }}`) && (PathPrefix(`/api`) || PathPrefix(`/media`))
kind: Rule
priority: 100
middlewares:
{{- if .Values.middlewares.securityHeaders }}
- name: {{ $fullName }}-security-headers
{{- end }}
{{- if .Values.middlewares.compress }}
- name: {{ $fullName }}-compress
{{- end }}
services:
- name: {{ $fullName }}-backend
port: {{ .Values.backend.service.port }}
# Unlisted certificate pages
- match: Host(`{{ $domain }}`) && PathPrefix(`/c`)
kind: Rule
priority: 50
middlewares:
{{- if .Values.middlewares.securityHeaders }}
- name: {{ $fullName }}-security-headers
{{- end }}
{{- if .Values.middlewares.compress }}
- name: {{ $fullName }}-compress
{{- end }}
- name: {{ $fullName }}-noindex
services:
- name: {{ $fullName }}-frontend
port: {{ .Values.frontend.service.port }}
# Frontend catch-all
- match: Host(`{{ $domain }}`)
kind: Rule
priority: 1
middlewares:
{{- if .Values.middlewares.securityHeaders }}
- name: {{ $fullName }}-security-headers
{{- end }}
{{- if .Values.middlewares.compress }}
- name: {{ $fullName }}-compress
{{- end }}
services:
- name: {{ $fullName }}-frontend
port: {{ .Values.frontend.service.port }}
{{- if .Values.ingress.tls.enabled }}
tls:
secretName: {{ $tlsSecret }}
{{- end }}
{{- end }}
@@ -0,0 +1,63 @@
{{- if .Values.ingress.enabled }}
{{- if .Values.middlewares.securityHeaders }}
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: {{ include "pargar.fullname" . }}-security-headers
labels:
{{- include "pargar.labels" . | nindent 4 }}
spec:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
contentTypeNosniff: true
browserXssFilter: true
frameDeny: true
referrerPolicy: strict-origin-when-cross-origin
permissionsPolicy: "camera=(), microphone=(), geolocation=()"
customResponseHeaders:
Server: ""
Content-Security-Policy: >-
default-src 'self'; base-uri 'self'; form-action 'self';
frame-ancestors 'none'; object-src 'none';
script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:; font-src 'self' data:;
media-src 'self' blob:; connect-src 'self' ws: wss:;
worker-src 'self' blob:
---
{{- end }}
{{- if .Values.middlewares.compress }}
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: {{ include "pargar.fullname" . }}-compress
labels:
{{- include "pargar.labels" . | nindent 4 }}
spec:
compress: {}
---
{{- end }}
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: {{ include "pargar.fullname" . }}-noindex
labels:
{{- include "pargar.labels" . | nindent 4 }}
spec:
headers:
customResponseHeaders:
X-Robots-Tag: "noindex, nofollow, noarchive"
---
{{- if and .Values.ingress.useIngressRoute .Values.middlewares.redirectHttps }}
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: {{ include "pargar.fullname" . }}-redirect-https
labels:
{{- include "pargar.labels" . | nindent 4 }}
spec:
redirectScheme:
scheme: https
permanent: true
{{- end }}
{{- end }}
@@ -0,0 +1,229 @@
{{- if .Values.networkPolicy.enabled }}
{{- $fullName := include "pargar.fullname" . }}
# Default deny all ingress/egress in the release namespace, then allow required paths.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ $fullName }}-default-deny
labels:
{{- include "pargar.labels" . | nindent 4 }}
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
{{- if .Values.backend.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ $fullName }}-backend
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: backend
policyTypes:
- Ingress
- Egress
ingress:
- from:
{{- range .Values.networkPolicy.ingressNamespaces }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
podSelector:
matchLabels:
{{- toYaml $.Values.networkPolicy.ingressPodLabels | nindent 14 }}
{{- end }}
ports:
- protocol: TCP
port: {{ .Values.backend.service.port }}
egress:
# DNS
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- if .Values.postgres.enabled }}
- to:
- podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 14 }}
app.kubernetes.io/component: postgres
ports:
- protocol: TCP
port: 5432
{{- end }}
{{- if .Values.redis.enabled }}
- to:
- podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 14 }}
app.kubernetes.io/component: redis
ports:
- protocol: TCP
port: 6379
{{- end }}
{{- if .Values.rustfs.enabled }}
- to:
- podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 14 }}
app.kubernetes.io/component: rustfs
ports:
- protocol: TCP
port: 9000
{{- else }}
# External S3 / managed data when in-cluster rustfs is off
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 169.254.0.0/16
ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 5432
- protocol: TCP
port: 6379
{{- end }}
---
{{- end }}
{{- if .Values.frontend.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ $fullName }}-frontend
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: frontend
policyTypes:
- Ingress
- Egress
ingress:
- from:
{{- range .Values.networkPolicy.ingressNamespaces }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
podSelector:
matchLabels:
{{- toYaml $.Values.networkPolicy.ingressPodLabels | nindent 14 }}
{{- end }}
ports:
- protocol: TCP
port: {{ .Values.frontend.service.port }}
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
{{- end }}
{{- if .Values.postgres.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ $fullName }}-postgres
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: postgres
spec:
podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: postgres
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 14 }}
app.kubernetes.io/component: backend
ports:
- protocol: TCP
port: 5432
egress: []
---
{{- end }}
{{- if .Values.redis.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ $fullName }}-redis
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: redis
spec:
podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: redis
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 14 }}
app.kubernetes.io/component: backend
ports:
- protocol: TCP
port: 6379
egress: []
---
{{- end }}
{{- if .Values.rustfs.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ $fullName }}-rustfs
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: rustfs
spec:
podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: rustfs
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 14 }}
app.kubernetes.io/component: backend
ports:
- protocol: TCP
port: 9000
egress: []
{{- end }}
{{- end }}
+41
View File
@@ -0,0 +1,41 @@
{{- if and .Values.backend.enabled .Values.backend.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "pargar.fullname" . }}-backend
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
{{- if .Values.backend.podDisruptionBudget.minAvailable }}
minAvailable: {{ .Values.backend.podDisruptionBudget.minAvailable }}
{{- end }}
{{- if .Values.backend.podDisruptionBudget.maxUnavailable }}
maxUnavailable: {{ .Values.backend.podDisruptionBudget.maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: backend
---
{{- end }}
{{- if and .Values.frontend.enabled .Values.frontend.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "pargar.fullname" . }}-frontend
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
{{- if .Values.frontend.podDisruptionBudget.minAvailable }}
minAvailable: {{ .Values.frontend.podDisruptionBudget.minAvailable }}
{{- end }}
{{- if .Values.frontend.podDisruptionBudget.maxUnavailable }}
maxUnavailable: {{ .Values.frontend.podDisruptionBudget.maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: frontend
{{- end }}
@@ -0,0 +1,93 @@
{{- if .Values.postgres.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "pargar.postgresHost" . }}
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: postgres
spec:
type: ClusterIP
ports:
- name: postgres
port: 5432
targetPort: postgres
selector:
{{- include "pargar.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "pargar.fullname" . }}-postgres
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: postgres
spec:
serviceName: {{ include "pargar.postgresHost" . }}
replicas: 1
selector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: postgres
template:
metadata:
labels:
{{- include "pargar.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: postgres
spec:
containers:
- name: postgres
image: {{ .Values.postgres.image }}
imagePullPolicy: IfNotPresent
ports:
- name: postgres
containerPort: 5432
env:
- name: POSTGRES_USER
value: {{ .Values.postgres.auth.username | quote }}
- name: POSTGRES_DB
value: {{ .Values.postgres.auth.database | quote }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "pargar.postgresSecretName" . }}
key: postgres-password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
readinessProbe:
exec:
command: ["pg_isready", "-U", {{ .Values.postgres.auth.username | quote }}, "-d", {{ .Values.postgres.auth.database | quote }}]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-U", {{ .Values.postgres.auth.username | quote }}, "-d", {{ .Values.postgres.auth.database | quote }}]
initialDelaySeconds: 15
periodSeconds: 10
{{- with .Values.postgres.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
{{- if not .Values.postgres.persistence.enabled }}
volumes:
- name: data
emptyDir: {}
{{- end }}
{{- if .Values.postgres.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.postgres.persistence.storageClass }}
storageClassName: {{ .Values.postgres.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.postgres.persistence.size }}
{{- end }}
{{- end }}
+88
View File
@@ -0,0 +1,88 @@
{{- if .Values.redis.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "pargar.redisHost" . }}
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: redis
spec:
type: ClusterIP
ports:
- name: redis
port: 6379
targetPort: redis
selector:
{{- include "pargar.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: redis
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "pargar.fullname" . }}-redis
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: redis
spec:
serviceName: {{ include "pargar.redisHost" . }}
replicas: 1
selector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: redis
template:
metadata:
labels:
{{- include "pargar.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: redis
spec:
containers:
- name: redis
image: {{ .Values.redis.image }}
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "exec redis-server --requirepass \"$REDIS_PASSWORD\" --appendonly yes"]
ports:
- name: redis
containerPort: 6379
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "pargar.redisSecretName" . }}
key: redis-password
readinessProbe:
exec:
command: ["sh", "-c", "redis-cli -a \"$REDIS_PASSWORD\" ping | grep -q PONG"]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: ["sh", "-c", "redis-cli -a \"$REDIS_PASSWORD\" ping | grep -q PONG"]
initialDelaySeconds: 15
periodSeconds: 10
{{- with .Values.redis.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: data
mountPath: /data
{{- if not .Values.redis.persistence.enabled }}
volumes:
- name: data
emptyDir: {}
{{- end }}
{{- if .Values.redis.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.redis.persistence.storageClass }}
storageClassName: {{ .Values.redis.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.redis.persistence.size }}
{{- end }}
{{- end }}
+101
View File
@@ -0,0 +1,101 @@
{{- if .Values.rustfs.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "pargar.rustfsHost" . }}
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: rustfs
spec:
type: ClusterIP
ports:
- name: s3
port: 9000
targetPort: s3
selector:
{{- include "pargar.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: rustfs
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "pargar.fullname" . }}-rustfs
labels:
{{- include "pargar.labels" . | nindent 4 }}
app.kubernetes.io/component: rustfs
spec:
serviceName: {{ include "pargar.rustfsHost" . }}
replicas: 1
selector:
matchLabels:
{{- include "pargar.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: rustfs
template:
metadata:
labels:
{{- include "pargar.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: rustfs
spec:
containers:
- name: rustfs
image: {{ .Values.rustfs.image }}
imagePullPolicy: IfNotPresent
args: ["/data"]
ports:
- name: s3
containerPort: 9000
env:
- name: RUSTFS_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ include "pargar.rustfsSecretName" . }}
key: access-key
- name: RUSTFS_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "pargar.rustfsSecretName" . }}
key: secret-key
- name: RUSTFS_ADDRESS
value: ":9000"
- name: RUSTFS_CONSOLE_ADDRESS
value: ":9001"
- name: RUSTFS_CONSOLE_ENABLE
value: {{ .Values.rustfs.consoleEnable | ternary "true" "false" | quote }}
readinessProbe:
httpGet:
path: /health
port: s3
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: s3
initialDelaySeconds: 20
periodSeconds: 10
{{- with .Values.rustfs.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: data
mountPath: /data
{{- if not .Values.rustfs.persistence.enabled }}
volumes:
- name: data
emptyDir: {}
{{- end }}
{{- if .Values.rustfs.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.rustfs.persistence.storageClass }}
storageClassName: {{ .Values.rustfs.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.rustfs.persistence.size }}
{{- end }}
{{- end }}
@@ -0,0 +1,56 @@
{{- if and .Values.backend.enabled (not .Values.backend.existingSecret) }}
{{- $name := include "pargar.appSecretName" . }}
{{- $existing := lookup "v1" "Secret" .Release.Namespace $name }}
{{- $jwt := "" }}
{{- $admin := "" }}
{{- $metrics := .Values.backend.secrets.metricsToken | default "" }}
{{- if and $existing $existing.data }}
{{- /* Reuse existing secrets so upgrades do not invalidate sessions / bootstrap. */}}
{{- if index $existing.data "jwt-secret" }}
{{- $jwt = index $existing.data "jwt-secret" | b64dec }}
{{- end }}
{{- if index $existing.data "admin-password" }}
{{- $admin = index $existing.data "admin-password" | b64dec }}
{{- end }}
{{- if and (not $metrics) (index $existing.data "metrics-token") }}
{{- $metrics = index $existing.data "metrics-token" | b64dec }}
{{- end }}
{{- end }}
{{- if not $jwt }}
{{- if and .Values.backend.secrets.jwtSecret (not (hasPrefix "change-me" .Values.backend.secrets.jwtSecret)) (ne .Values.backend.secrets.jwtSecret "") }}
{{- $jwt = .Values.backend.secrets.jwtSecret }}
{{- else }}
{{- $jwt = randAlphaNum 48 }}
{{- end }}
{{- end }}
{{- if not $admin }}
{{- if and .Values.backend.secrets.adminPassword (not (hasPrefix "change-me" .Values.backend.secrets.adminPassword)) (ne .Values.backend.secrets.adminPassword "") }}
{{- $admin = .Values.backend.secrets.adminPassword }}
{{- else }}
{{- $admin = randAlphaNum 24 }}
{{- end }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
name: {{ $name }}
labels:
{{- include "pargar.labels" . | nindent 4 }}
annotations:
pargar.io/secret-source: generated-or-values
type: Opaque
stringData:
jwt-secret: {{ $jwt | quote }}
admin-password: {{ $admin | quote }}
metrics-token: {{ $metrics | quote }}
{{- if not .Values.postgres.enabled }}
database-url: {{ required "external.databaseUrl required when postgres.enabled=false and existingSecret is empty" .Values.external.databaseUrl | quote }}
{{- end }}
{{- if not .Values.redis.enabled }}
redis-url: {{ required "external.redisUrl required when redis.enabled=false and existingSecret is empty" .Values.external.redisUrl | quote }}
{{- end }}
{{- if not .Values.rustfs.enabled }}
storage-access-key: {{ required "external.storageAccessKey required when rustfs.enabled=false and existingSecret is empty" .Values.external.storageAccessKey | quote }}
storage-secret-key: {{ required "external.storageSecretKey required when rustfs.enabled=false and existingSecret is empty" .Values.external.storageSecretKey | quote }}
{{- end }}
{{- end }}
@@ -0,0 +1,71 @@
{{- if and .Values.postgres.enabled (not .Values.postgres.existingSecret) }}
{{- $pgName := include "pargar.postgresSecretName" . }}
{{- $pgExisting := lookup "v1" "Secret" .Release.Namespace $pgName }}
{{- $pgPass := "" }}
{{- if and $pgExisting $pgExisting.data (index $pgExisting.data "postgres-password") }}
{{- /* Never rotate an existing DB password — Postgres data dir is bound to it. */}}
{{- $pgPass = index $pgExisting.data "postgres-password" | b64dec }}
{{- else if and .Values.postgres.auth.password (not (hasPrefix "change-me" .Values.postgres.auth.password)) (ne .Values.postgres.auth.password "") }}
{{- $pgPass = .Values.postgres.auth.password }}
{{- else }}
{{- $pgPass = randAlphaNum 32 }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
name: {{ $pgName }}
labels:
{{- include "pargar.labels" . | nindent 4 }}
type: Opaque
stringData:
postgres-password: {{ $pgPass | quote }}
---
{{- end }}
{{- if and .Values.redis.enabled (not .Values.redis.existingSecret) }}
{{- $rdName := include "pargar.redisSecretName" . }}
{{- $rdExisting := lookup "v1" "Secret" .Release.Namespace $rdName }}
{{- $rdPass := "" }}
{{- if and $rdExisting $rdExisting.data (index $rdExisting.data "redis-password") }}
{{- $rdPass = index $rdExisting.data "redis-password" | b64dec }}
{{- else if and .Values.redis.auth.password (not (hasPrefix "change-me" .Values.redis.auth.password)) (ne .Values.redis.auth.password "") }}
{{- $rdPass = .Values.redis.auth.password }}
{{- else }}
{{- $rdPass = randAlphaNum 32 }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
name: {{ $rdName }}
labels:
{{- include "pargar.labels" . | nindent 4 }}
type: Opaque
stringData:
redis-password: {{ $rdPass | quote }}
---
{{- end }}
{{- if and .Values.rustfs.enabled (not .Values.rustfs.existingSecret) }}
{{- $rsName := include "pargar.rustfsSecretName" . }}
{{- $rsExisting := lookup "v1" "Secret" .Release.Namespace $rsName }}
{{- $access := .Values.rustfs.auth.accessKey | default "pargar" }}
{{- $secret := "" }}
{{- if and $rsExisting $rsExisting.data (index $rsExisting.data "secret-key") }}
{{- $secret = index $rsExisting.data "secret-key" | b64dec }}
{{- if index $rsExisting.data "access-key" }}
{{- $access = index $rsExisting.data "access-key" | b64dec }}
{{- end }}
{{- else if and .Values.rustfs.auth.secretKey (not (hasPrefix "change-me" .Values.rustfs.auth.secretKey)) (ne .Values.rustfs.auth.secretKey "") }}
{{- $secret = .Values.rustfs.auth.secretKey }}
{{- else }}
{{- $secret = randAlphaNum 40 }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
name: {{ $rsName }}
labels:
{{- include "pargar.labels" . | nindent 4 }}
type: Opaque
stringData:
access-key: {{ $access | quote }}
secret-key: {{ $secret | quote }}
{{- end }}
@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "pargar.serviceAccountName" . }}
labels:
{{- include "pargar.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,21 @@
{{- if and .Values.ingress.enabled .Values.ingress.tls.enabled (not .Values.ingress.tls.existingSecret) (not .Values.certManager.enabled) }}
{{- $secretName := include "pargar.tlsSecretName" . }}
{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName }}
{{- $cn := .Values.global.domain }}
apiVersion: v1
kind: Secret
metadata:
name: {{ $secretName }}
labels:
{{- include "pargar.labels" . | nindent 4 }}
type: kubernetes.tls
data:
{{- if $existing }}
tls.crt: {{ index $existing.data "tls.crt" }}
tls.key: {{ index $existing.data "tls.key" }}
{{- else }}
{{- $cert := genSelfSignedCert $cn nil (list $cn) 3650 }}
tls.crt: {{ $cert.Cert | b64enc }}
tls.key: {{ $cert.Key | b64enc }}
{{- end }}
{{- end }}
+55
View File
@@ -0,0 +1,55 @@
# Local k3s profile — self-signed TLS, in-cluster data, single replicas.
# helm upgrade --install pargar ./deploy/helm/pargar -n pargar --create-namespace -f deploy/helm/pargar/values-k3s.yaml
#
# After install, add to /etc/hosts: <NODE_IP> pargar.local
global:
domain: pargar.local
publicUrl: https://pargar.local
frontendOrigins: "https://pargar.local"
backend:
replicaCount: 1
image:
repository: pargar-backend
tag: local
pullPolicy: IfNotPresent
env:
seedCurriculum: "true"
frontend:
replicaCount: 1
image:
repository: pargar-frontend
tag: local
pullPolicy: IfNotPresent
postgres:
enabled: true
persistence:
size: 5Gi
redis:
enabled: true
persistence:
size: 1Gi
rustfs:
enabled: true
persistence:
size: 10Gi
consoleEnable: false
backup:
enabled: true
schedule: "0 3 * * *"
retentionDays: 7
persistence:
size: 2Gi
ingress:
enabled: true
useIngressRoute: true
tls:
enabled: true
existingSecret: "" # chart generates self-signed for pargar.local
+103
View File
@@ -0,0 +1,103 @@
# Production profile — external data plane, registry images, HPA/PDB/NetworkPolicy, cert-manager.
# helm upgrade --install pargar ./deploy/helm/pargar -n pargar --create-namespace \
# -f deploy/helm/pargar/values-prod.yaml
#
# Prerequisites:
# - images pushed to ghcr.io/pargar/{backend,frontend}:<tag>
# - Secret pargar-app (jwt-secret, admin-password, metrics-token, database-url, redis-url,
# storage-access-key, storage-secret-key)
# - cert-manager ClusterIssuer letsencrypt-prod
# - imagePullSecrets if the registry is private
global:
domain: app.example.com
publicUrl: https://app.example.com
frontendOrigins: "https://app.example.com"
imageRegistry: ghcr.io/pargar
imagePullSecrets:
- name: ghcr-pull
backend:
replicaCount: 2
image:
repository: backend
tag: "0.1.0"
pullPolicy: IfNotPresent
existingSecret: pargar-app
env:
devMode: "false"
seedCurriculum: "false"
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "2"
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 8
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
podDisruptionBudget:
enabled: true
minAvailable: 1
frontend:
replicaCount: 2
image:
repository: frontend
tag: "0.1.0"
pullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "2"
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 8
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
podDisruptionBudget:
enabled: true
minAvailable: 1
# Prefer managed Postgres / Redis / object storage in real prod.
postgres:
enabled: false
redis:
enabled: false
rustfs:
enabled: false
external:
databaseUrl: "" # prefer secret key database-url on pargar-app
redisUrl: ""
storageEndpoint: "https://s3.example.com"
ingress:
enabled: true
useIngressRoute: true
tls:
enabled: true
existingSecret: "" # Certificate CR owns the secret below
clusterIssuer: letsencrypt-prod
certManager:
enabled: true
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
networkPolicy:
enabled: true
ingressNamespaces:
- kube-system
ingressPodLabels:
app.kubernetes.io/name: traefik
+229
View File
@@ -0,0 +1,229 @@
# Default values — safe for local k3s. Override with values-k3s.yaml / values-prod.yaml.
nameOverride: ""
fullnameOverride: ""
global:
# Public hostname (Ingress / TLS / PUBLIC_URL)
domain: pargar.local
# Full public origin browsers use (must match Ingress TLS host)
publicUrl: https://pargar.local
frontendOrigins: "https://pargar.local"
# Docker/k8s pod+service CIDRs — Traefik peer is trusted for X-Forwarded-*
trustedProxyCIDRs: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8,::1/128"
# Optional registry prefix, e.g. ghcr.io/pargar → images become ghcr.io/pargar/<repository>:<tag>
imageRegistry: ""
imagePullSecrets: []
backend:
enabled: true
replicaCount: 1
image:
repository: pargar-backend
tag: local
pullPolicy: IfNotPresent
service:
port: 8080
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 6
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
podDisruptionBudget:
enabled: false
minAvailable: 1
env:
devMode: "false"
seedCurriculum: "false"
donationEnabled: "true"
# Offline payment only — leave off until ops process is ready; enable in admin settings.
physicalCertEnabled: "false"
physicalCertPriceIrr: "2500000"
physicalCertWindowDays: "30"
otelEndpoint: ""
otelServiceName: pargar-api
# Created only when admin user is missing (see seed.ensureBootstrapAdmin)
admin:
name: Admin
username: admin
email: admin@example.com
# Prefer existingSecret in production. When empty, chart generate-once (lookup + rand).
existingSecret: ""
# Optional overrides; leave empty to auto-generate strong values (placeholders like change-me-* are ignored).
secrets:
jwtSecret: ""
adminPassword: ""
metricsToken: ""
frontend:
enabled: true
replicaCount: 1
image:
repository: pargar-frontend
tag: local
pullPolicy: IfNotPresent
service:
port: 3000
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 6
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
podDisruptionBudget:
enabled: false
minAvailable: 1
# In-cluster data plane (k3s / small clusters). Disable and point to managed services in prod.
postgres:
enabled: true
image: postgres:16.4-alpine
persistence:
enabled: true
size: 10Gi
storageClass: ""
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
auth:
username: pargar
database: pargar
# Empty → chart generate-once; placeholders change-me-* are ignored.
password: ""
existingSecret: ""
redis:
enabled: true
image: redis@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499
persistence:
enabled: true
size: 2Gi
storageClass: ""
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
memory: 256Mi
auth:
password: ""
existingSecret: ""
rustfs:
enabled: true
image: rustfs/rustfs@sha256:1803faef57627e2d9c2e7d89d655d712ddded5389040054987163043fecb6a3c
persistence:
enabled: true
size: 20Gi
storageClass: ""
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
auth:
accessKey: "pargar-rustfs"
secretKey: ""
bucket: pargar
region: us-east-1
consoleEnable: false
existingSecret: ""
# Postgres dump CronJob (in-cluster postgres only).
backup:
enabled: false
schedule: "0 3 * * *"
retentionDays: 7
persistence:
size: 5Gi
storageClass: ""
image: postgres:16.4-alpine
# External endpoints when in-cluster postgres/redis/rustfs are disabled.
# Prefer putting these in backend.existingSecret in production.
external:
databaseUrl: ""
redisUrl: ""
storageEndpoint: ""
storageAccessKey: ""
storageSecretKey: ""
ingress:
enabled: true
# k3s ships Traefik; use IngressRoute CRDs (recommended) or standard Ingress
className: traefik
# Use Traefik IngressRoute + Middleware CRDs (true) vs networking.k8s.io/Ingress (false)
useIngressRoute: true
annotations: {}
tls:
enabled: true
# When empty and certManager.enabled=false, chart creates a self-signed Secret for k3s/dev.
# Prefer certManager.enabled=true in prod, or set existingSecret to a pre-created TLS secret.
existingSecret: ""
# cert-manager ClusterIssuer name (annotation helper when useIngressRoute=false)
clusterIssuer: ""
# cert-manager Certificate CR (writes TLS secret used by IngressRoute)
certManager:
enabled: false
duration: 2160h
renewBefore: 360h
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
group: ""
extraDnsNames: []
# Restrict pod traffic (off for local k3s; on in values-prod.yaml)
networkPolicy:
enabled: false
# Namespaces whose pods may reach frontend/backend (Traefik)
ingressNamespaces:
- kube-system
ingressPodLabels:
app.kubernetes.io/name: traefik
middlewares:
securityHeaders: true
compress: true
redirectHttps: true
serviceAccount:
create: true
name: ""
annotations: {}
podSecurityContext:
fsGroup: 10001
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop: ["ALL"]
nodeSelector: {}
tolerations: []
affinity: {}