feat: deploy

This commit is contained in:
Daniel Noulet 2026-10-03 04:39:14 +02:00
parent 560014d0b8
commit 9bbf8a96aa
16 changed files with 542 additions and 292 deletions

View file

@ -1,6 +1,6 @@
apiVersion: v2
name: go-guess
description: Go Guess recruitment workspace
description: Go Guess recruitment workspace with per-tenant web hosts
type: application
version: 0.2.0
appVersion: "0.2.0"
version: 0.3.0
appVersion: "0.3.0"

View file

@ -1,17 +1,48 @@
Go Guess has been installed in namespace {{ .Release.Namespace }}.
{{- if .Values.goLoose.enabled }}
Browser sign-in uses Go Loose at https://<tenant>.{{ .Values.goLoose.authDomain }}.
{{- else }}
Browser sign-in uses the application password login. Enable goLoose to redirect tenant hosts.
Go Guess is installed in namespace {{ .Release.Namespace }}.
Components:
API service/{{ include "go-guess.apiServiceName" . }}:{{ .Values.api.service.port }} (cluster-internal)
Web service/{{ include "go-guess.webDeploymentName" . }}:{{ .Values.web.service.port }}
Ingress {{ if .Values.ingress.enabled }}class {{ if .Values.ingress.className }}{{ .Values.ingress.className }}{{ else }}default{{ end }}{{ else }}disabled{{ end }}
{{ if .Values.ingress.enabled }}
Tenant URLs:
{{- range .Values.tenants }}
{{ if $.Values.ingress.tls.enabled }}https{{ else }}http{{ end }}://{{ printf "%s.%s" . $.Values.domain }}
{{- end }}
{{- if .Values.ingress.enabled }}
Open {{ .Values.app.frontendURL }}
{{- else if eq .Values.service.type "NodePort" }}
The application is exposed on cluster node port {{ .Values.service.nodePort }}.
Open {{ .Values.app.frontendURL }} through the external reverse proxy.
{{- else }}
Run:
kubectl -n {{ .Release.Namespace }} port-forward service/{{ include "go-guess.fullname" . }} 8080:{{ .Values.service.port }}
Then open http://127.0.0.1:8080.
Point every tenant host at the ingress controller address, for example in
/etc/hosts:
{{- range .Values.tenants }}
<ingress-controller-ip> {{ printf "%s.%s" . $.Values.domain }}
{{- end }}
{{- if and .Values.ingress.tls.enabled .Values.certManager.enabled (not .Values.ingress.tls.existingSecret) }}
The certificate is self-signed, so trust the chart CA in every browser or client
that opens these hosts:
kubectl -n {{ .Release.Namespace }} get secret {{ include "go-guess.certManagerCASecretName" . }} \
-o jsonpath='{.data.ca\.crt}' | base64 -d > go-guess-ca.crt
{{- end }}
{{- if .Values.api.goLoose.enabled }}
Browser sign-in redirects to https://<tenant>.{{ .Values.api.goLoose.authDomain }}:
{{- range $tenant, $credentials := .Values.api.goLoose.tenants }}
{{- if and $credentials.clientID $credentials.clientSecret }}
{{ $tenant }}: Go Loose browser sign-in
{{- else }}
{{ $tenant }}: no client credentials configured, password login only
{{- end }}
{{- end }}
{{- else }}
Browser sign-in uses the application password login.
{{- end }}
{{- else if and (eq .Values.web.service.type "NodePort") .Values.web.service.nodePort }}
The web Service is exposed on cluster node port {{ .Values.web.service.nodePort }}.
{{- else }}
Run:
kubectl -n {{ .Release.Namespace }} port-forward service/{{ include "go-guess.webDeploymentName" . }} 8080:{{ .Values.web.service.port }}
Then open http://127.0.0.1:8080.
{{- end }}

View file

@ -39,3 +39,72 @@ app.kubernetes.io/instance: {{ .Release.Name }}
{{- define "go-guess.rabbitmqName" -}}
{{- printf "%s-rabbitmq" (include "go-guess.fullname" .) }}
{{- end }}
{{- define "go-guess.apiDeploymentName" -}}
{{- printf "%s-api" (include "go-guess.fullname" .) | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "go-guess.webDeploymentName" -}}
{{- printf "%s-web" (include "go-guess.fullname" .) | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/* DNS name the API is reachable under. The web image hardcodes "api:8080". */}}
{{- define "go-guess.apiServiceName" -}}
{{- default "api" .Values.api.service.name }}
{{- end }}
{{/* Host serving one tenant, for example nmbs.guess.local. */}}
{{- define "go-guess.tenantHost" -}}
{{- printf "%s.%s" (index . 0) (index . 1) }}
{{- end }}
{{/* Fallback public URL for invitation and meeting links. */}}
{{- define "go-guess.frontendURL" -}}
{{- if .Values.api.frontendURL }}
{{- .Values.api.frontendURL }}
{{- else if .Values.tenants }}
{{- printf "https://%s.%s" (first .Values.tenants) .Values.domain }}
{{- end }}
{{- end }}
{{/* Secret holding the wildcard-per-tenant web certificate. */}}
{{- define "go-guess.tlsSecretName" -}}
{{- if .Values.ingress.tls.existingSecret }}
{{- .Values.ingress.tls.existingSecret }}
{{- else if .Values.ingress.tls.secretName }}
{{- .Values.ingress.tls.secretName }}
{{- else }}
{{- printf "%s-web-tls" (include "go-guess.fullname" .) }}
{{- end }}
{{- end }}
{{/* Cluster-scoped by default so one issuer signs every tenant release. */}}
{{- define "go-guess.certManagerIssuerRef" -}}
{{- if .Values.certManager.issuerRef }}
toYaml .Values.certManager.issuerRef
{{- else }}
name: selfsigned
kind: Issuer
{{- end }}
{{- end }}
{{- define "go-guess.validate" -}}
{{- if not .Values.tenants }}
{{- fail "tenants must list at least one tenant slug" }}
{{- end }}
{{- if not .Values.domain }}
{{- fail "domain is required" }}
{{- end }}
{{- if and .Values.ingress.enabled .Values.ingress.tls.enabled (not .Values.certManager.enabled) (not .Values.ingress.tls.existingSecret) }}
{{- fail "ingress.tls.enabled requires certManager.enabled or ingress.tls.existingSecret" }}
{{- end }}
{{- end }}
{{/* Secret holding the CA that signs the leaf web certificate. */}}
{{- define "go-guess.certManagerCASecretName" -}}
{{- if .Values.certManager.caSecretName }}
{{- .Values.certManager.caSecretName }}
{{- else }}
{{- printf "%s-web-ca" (include "go-guess.fullname" .) }}
{{- end }}
{{- end }}

View file

@ -1,105 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "go-guess.fullname" . }}
labels:
{{- include "go-guess.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: {{ .Values.strategy.type }}
selector:
matchLabels:
{{- include "go-guess.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: application
template:
metadata:
annotations:
checksum/secrets: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "go-guess.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: application
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
automountServiceAccountToken: false
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: go-guess
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
env:
- name: APP_ENV
value: {{ .Values.app.environment | quote }}
- name: FRONTEND_URL
value: {{ .Values.app.frontendURL | quote }}
- name: TOKEN_TTL
value: {{ .Values.app.tokenTTL | quote }}
- name: MAX_UPLOAD_MB
value: {{ .Values.app.maxUploadMB | quote }}
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "go-guess.secretName" . }}
key: database-url
- name: RABBITMQ_URL
valueFrom:
secretKeyRef:
name: {{ include "go-guess.secretName" . }}
key: rabbitmq-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "go-guess.secretName" . }}
key: jwt-secret
ports:
- name: http
containerPort: 8080
protocol: TCP
readinessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 12
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: temporary-files
mountPath: /tmp
volumes:
- name: temporary-files
emptyDir: {}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View file

@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "go-guess.fullname" . }}
labels:
{{- include "go-guess.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "go-guess.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: application
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
{{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}

View file

@ -1,4 +1,5 @@
{{- if .Values.ingress.enabled }}
{{- $webService := include "go-guess.webDeploymentName" . }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@ -13,23 +14,25 @@ spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- with .Values.ingress.tls }}
{{- if .Values.ingress.tls.enabled }}
tls:
{{- toYaml . | nindent 4 }}
- hosts:
{{- range .Values.tenants }}
- {{ printf "%s.%s" . $.Values.domain | quote }}
{{- end }}
secretName: {{ include "go-guess.tlsSecretName" . }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
{{- range .Values.tenants }}
- host: {{ printf "%s.%s" . $.Values.domain | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
- path: {{ $.Values.ingress.path }}
pathType: {{ $.Values.ingress.pathType }}
backend:
service:
name: {{ include "go-guess.fullname" $ }}
name: {{ $webService }}
port:
number: {{ $.Values.service.port }}
{{- end }}
number: {{ $.Values.web.service.port }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -32,3 +32,14 @@ data:
rabbitmq-url: {{ $rabbitmqURL | b64enc | quote }}
postgresql-password: {{ $postgresPassword | b64enc | quote }}
rabbitmq-password: {{ $rabbitmqPassword | b64enc | quote }}
{{- if .Values.api.goLoose.enabled }}
{{- range $tenant, $credentials := .Values.api.goLoose.tenants }}
{{- $prefix := printf "go-loose-%s" (lower $tenant) }}
{{- if $credentials.clientID }}
{{ printf "%s-client-id" $prefix }}: {{ $credentials.clientID | b64enc | quote }}
{{- end }}
{{- if $credentials.clientSecret }}
{{ printf "%s-client-secret" $prefix }}: {{ $credentials.clientSecret | b64enc | quote }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -17,4 +17,11 @@ spec:
- wget
args:
- -qO-
- "http://{{ include "go-guess.fullname" . }}:{{ .Values.service.port }}/api/health"
- "http://{{ include "go-guess.apiServiceName" . }}:{{ .Values.api.service.port }}/api/health"
- name: wget-web
image: busybox:1.37
command:
- wget
args:
- -qO-
- "http://{{ include "go-guess.webDeploymentName" . }}:{{ .Values.web.service.port }}/"

View file

@ -1,25 +1,43 @@
image:
tag: latest
pullPolicy: Always
# Development profile: demo fixtures, ephemeral database and broker storage, and
# `latest` image tags. TLS still uses the chart's self-signed certificate so
# https://<tenant>.guess.local behaves like the production profile.
domain: guess.local
app:
tenants:
- nmbs
- ypto
api:
environment: development
frontendURL: http://localhost:30080
image:
tag: latest
pullPolicy: Always
goLoose:
enabled: true
authDomain: auth.dev
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
cpu: 300m
memory: 384Mi
service:
type: NodePort
nodePort: 30080
web:
replicaCount: 1
image:
tag: latest
pullPolicy: Always
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 200m
memory: 96Mi
ingress:
enabled: false
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
cpu: 300m
memory: 384Mi
enabled: true
postgresql:
persistence:
@ -41,4 +59,4 @@ rabbitmq:
memory: 96Mi
limits:
cpu: 300m
memory: 384Mi
memory: 384Mi

View file

@ -1,25 +1,45 @@
image:
tag: latest
pullPolicy: IfNotPresent
# Production profile: production fixtures, persistent volumes, released image
# tags, and self-signed certificates served for https://<tenant>.guess.local.
domain: guess.local
app:
tenants:
- nmbs
- ypto
api:
environment: production
frontendURL: https://goguess.urpi.be
image:
tag: "0.0.1"
pullPolicy: IfNotPresent
goLoose:
enabled: true
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 768Mi
service:
type: NodePort
nodePort: 31374
web:
replicaCount: 2
image:
tag: "0.0.1"
pullPolicy: IfNotPresent
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 300m
memory: 128Mi
ingress:
enabled: false
enabled: true
className: nginx
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 768Mi
certManager:
enabled: true
postgresql:
persistence:
@ -29,4 +49,4 @@ postgresql:
rabbitmq:
persistence:
enabled: true
size: 8Gi
size: 8Gi

View file

@ -1,30 +1,142 @@
nameOverride: ""
fullnameOverride: ""
replicaCount: 1
# Public application domain. Every tenant is served from https://<tenant>.<domain>.
domain: guess.local
image:
repository: batty1039.startdedicated.net:5000/go-guess
tag: latest
pullPolicy: IfNotPresent
# Tenant slugs. Each one gets its own ingress host and TLS SAN.
tenants:
- nmbs
- ypto
imagePullSecrets: []
app:
environment: production
frontendURL: http://localhost:8080
api:
replicaCount: 1
image:
repository: batty1039.startdedicated.net:5000/go-guess-api
tag: "0.0.1"
pullPolicy: IfNotPresent
environment: development
# Public base URL used when building invitation and meeting links. Defaults to
# https://<first tenant>.<domain> when empty.
frontendURL: ""
tokenTTL: 8h
maxUploadMB: 10
service:
# The web image proxies /api/ to the literal upstream name "api:8080", so this
# Service name has to stay "api" unless the web image is rebuilt.
name: api
type: ClusterIP
port: 8080
goLoose:
enabled: false
authDomain: auth.dev
ca:
# Mount an internal CA bundle and point GO_LOOSE_CA_FILE at it.
existingSecret: ""
key: ca.crt
mountPath: /certs/local-ca.crt
# Per-tenant browser authentication credentials, written to the release secret.
tenants:
nmbs:
clientID: ""
clientSecret: ""
ypto:
clientID: ""
clientSecret: ""
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
podAnnotations: {}
podLabels: {}
# The image runs as its own "app" user (uid 100), so no uid is forced here:
# runAsNonRoot then validates whatever the image declares.
podSecurityContext: {}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
# The API container applies migrations and seeds before serving, so old pods must
# be gone before the new one starts.
strategy:
type: Recreate
goLoose:
enabled: false
authDomain: auth.dev
appDomain: guess.dev
ca:
web:
replicaCount: 2
image:
repository: batty1039.startdedicated.net:5000/go-guess-web
tag: "0.0.1"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
nodePort: null
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 300m
memory: 128Mi
podAnnotations: {}
podLabels: {}
# nginx workers run as uid 101, which the image already declares.
podSecurityContext: {}
# nginx binds the privileged port 80 and forks workers as the nginx user, so it
# needs the root user plus a small capability set and writable scratch paths.
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
add:
- CHOWN
- SETGID
- SETUID
- NET_BIND_SERVICE
readOnlyRootFilesystem: false
runAsNonRoot: false
runAsUser: 0
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 10m
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
path: /
pathType: Prefix
tls:
enabled: true
# Use a certificate you already manage instead of the chart-issued one.
existingSecret: ""
key: ca.crt
mountPath: /certs/local-ca.crt
tenants: []
# Defaults to <fullname>-web-tls when empty and certManager.enabled is true.
secretName: ""
certManager:
# Requires cert-manager in the cluster: make cert-manager-install.
enabled: true
# Reuse an existing cert-manager issuer instead of creating a self-signed one.
issuerRef: {}
duration: 8760h
renewBefore: 720h
caSecretName: ""
secrets:
jwtSecret: ""
@ -33,49 +145,6 @@ secrets:
postgresqlPassword: ""
rabbitmqPassword: ""
service:
type: ClusterIP
port: 80
nodePort: null
ingress:
enabled: false
className: nginx
annotations: {}
hosts:
- host: go-guess.local
paths:
- path: /
pathType: Prefix
tls: []
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
podAnnotations: {}
podLabels: {}
podSecurityContext:
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
strategy:
type: Recreate
postgresql:
enabled: true
image:
@ -119,4 +188,4 @@ rabbitmq:
nodeSelector: {}
tolerations: []
affinity: {}
affinity: {}