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

@ -7,10 +7,15 @@ BDD_COMPOSE := docker compose -f docker-compose.bdd.yml
HELM ?= helm
HELM_CHART := deploy/helm/go-guess
HELM_RELEASE ?= go-guess
HELM_NAMESPACE ?= go-guess
KUBECONFIG ?= $(HOME)/.config/kubectl/rancher.urpi.local.yaml
HELM_NAMESPACE ?= go-insane
KUBECONFIG ?= $(HOME)/.kube/config
HELM_KUBECTL := KUBECONFIG="$(KUBECONFIG)" $(HELM)
GO_LOOSE_SET ?= --set-string api.goLoose.tenants.nmbs.clientID="$${GO_LOOSE_NMBS_CLIENT_ID:-}" \
--set-string api.goLoose.tenants.nmbs.clientSecret="$${GO_LOOSE_NMBS_CLIENT_SECRET:-}" \
--set-string api.goLoose.tenants.ypto.clientID="$${GO_LOOSE_YPTO_CLIENT_ID:-}" \
--set-string api.goLoose.tenants.ypto.clientSecret="$${GO_LOOSE_YPTO_CLIENT_SECRET:-}"
.PHONY: help dev db-up db-down migrate migrate-down seed backend frontend frontend-install test test-backend test-frontend test-integration test-bdd test-bdd-down test-env test-env-down lint build rancher-storage helm-lint helm-deploy-development helm-deploy-production helm-down-production helm-down-development
.PHONY: help dev db-up db-down migrate migrate-down seed backend frontend frontend-install test test-backend test-frontend test-integration test-bdd test-bdd-down test-env test-env-down lint build rancher-storage ingress-nginx-install cert-manager-install cluster-prepare helm-lint helm-deploy-development helm-deploy-production helm-down-production helm-down-development
help:
@echo "make dev builds the demo docker image"
@ -33,6 +38,9 @@ help:
@echo "make lint runs lint on the frontend"
@echo "make build builds the entire project"
@echo "make rancher-storage installs the persistent local-path StorageClass"
@echo "make ingress-nginx-install installs the ingress-nginx controller serving 80/443"
@echo "make cert-manager-install installs cert-manager for chart-issued TLS certificates"
@echo "make cluster-prepare runs every cluster prerequisite in order"
@echo "make helm-lint lints development and production Helm configurations"
@echo "make helm-deploy-development deploys the development Helm release"
@echo "make helm-deploy-production deploys the production Helm release"
@ -104,6 +112,27 @@ build:
rancher-storage:
KUBECONFIG="$(KUBECONFIG)" kubectl apply -f deploy/rancher/local-path.yaml
ingress-nginx-install:
$(HELM_KUBECTL) repo add ingress-nginx https://kubernetes.github.io/ingress-nginx --force-update
$(HELM_KUBECTL) repo update
$(HELM_KUBECTL) upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=NodePort \
--set controller.hostPort.enabled=true \
--set controller.kind=DaemonSet \
--set controller.admissionWebhooks.enabled=false \
--wait --timeout 10m
cert-manager-install:
$(HELM_KUBECTL) repo add jetstack https://charts.jetstack.io --force-update
$(HELM_KUBECTL) repo update
$(HELM_KUBECTL) upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set crds.enabled=true \
--wait --timeout 10m
cluster-prepare: rancher-storage ingress-nginx-install cert-manager-install
helm-lint:
KUBECONFIG="$(KUBECONFIG)" $(HELM) lint $(HELM_CHART) -f $(HELM_CHART)/values-development.yaml
KUBECONFIG="$(KUBECONFIG)" $(HELM) lint $(HELM_CHART) -f $(HELM_CHART)/values-production.yaml
@ -113,17 +142,25 @@ helm-lint:
-f $(HELM_CHART)/values-production.yaml >/dev/null
helm-deploy-development:
KUBECONFIG="$(KUBECONFIG)" $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART) --namespace $(HELM_NAMESPACE)-development --create-namespace -f $(HELM_CHART)/values-development.yaml --rollback-on-failure --wait --timeout 10m
$(HELM_KUBECTL) upgrade --install $(HELM_RELEASE) $(HELM_CHART) \
--namespace $(HELM_NAMESPACE)-development --create-namespace \
-f $(HELM_CHART)/values-development.yaml \
--set secrets.postgresqlPassword="go_guess" \
--set secrets.rabbitmqPassword="guest" \
$(GO_LOOSE_SET) \
--rollback-on-failure --wait --timeout 10m
helm-deploy-production:
KUBECONFIG="$(KUBECONFIG)" $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART) \
$(HELM_KUBECTL) upgrade --install $(HELM_RELEASE) $(HELM_CHART) \
--namespace $(HELM_NAMESPACE)-production --create-namespace \
-f $(HELM_CHART)/values-production.yaml --wait --timeout 10m\
-f $(HELM_CHART)/values-production.yaml \
--set secrets.postgresqlPassword="go_guess" \
--set secrets.rabbitmqPassword="guest"
--set secrets.rabbitmqPassword="guest" \
$(GO_LOOSE_SET) \
--wait --timeout 10m
helm-down-production:
KUBECONFIG="$(KUBECONFIG)" $(HELM) uninstall $(HELM_RELEASE) -n $(HELM_NAMESPACE)-production
$(HELM_KUBECTL) uninstall $(HELM_RELEASE) -n $(HELM_NAMESPACE)-production
helm-down-development:
KUBECONFIG="$(KUBECONFIG)" $(HELM) uninstall $(HELM_RELEASE) -n $(HELM_NAMESPACE)-development
$(HELM_KUBECTL) uninstall $(HELM_RELEASE) -n $(HELM_NAMESPACE)-development

View file

@ -156,36 +156,56 @@ make test-env-down
## Production image
Build the complete application as a single image:
Two images are published to the private registry: `go-guess-api:0.0.1` for the Go
service and `go-guess-web:0.0.1` for the React front end behind nginx.
```bash
docker build -t go-guess .
docker build -t batty1039.startdedicated.net:5000/go-guess-api:0.0.1 ./backend
docker build -t batty1039.startdedicated.net:5000/go-guess-web:0.0.1 ./frontend
```
The image serves the frontend and API on port `8080`. It applies database
migrations and shared production fixtures before startup. PostgreSQL, RabbitMQ,
and these environment variables must be provided:
The API image listens on `8080` and applies database migrations and shared
fixtures before serving. The web image listens on `80`, serves the built assets,
and proxies `/api/` to `http://api:8080`, so both containers must share a
network with the API reachable under the DNS name `api`. PostgreSQL, RabbitMQ,
and these environment variables must be provided to the API:
```bash
docker run --rm -p 8080:8080 \
-e DATABASE_URL='postgres://user:password@postgres:5432/go_guess?sslmode=disable' \
-e RABBITMQ_URL='amqp://user:password@rabbitmq:5672/' \
-e JWT_SECRET='replace-with-at-least-32-characters' \
-e FRONTEND_URL='https://go-guess.example.com' \
go-guess
-e FRONTEND_URL='https://nmbs.guess.local' \
batty1039.startdedicated.net:5000/go-guess-api:0.0.1
```
The `Dockerfile` at the repository root still builds one combined image that
serves both halves on port `8080`. It is convenient for local runs and browser
BDD tests, while Kubernetes deploys the two images separately.
Pushes to `master` publish `go-guess:latest` and `go-guess:<commit-sha>` to the
private registry configured in `.github/workflows/publish-image.yml`. The
repository must provide `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` secrets.
## Rancher and Helm
The chart under `deploy/helm/go-guess` deploys the application, PostgreSQL, and
RabbitMQ. It works with any Kubernetes cluster managed by Rancher; Rancher's free
edition does not restrict Helm or GitHub Actions deployments.
The chart under `deploy/helm/go-guess` deploys the API image, the web image, and
their PostgreSQL and RabbitMQ. Every tenant gets its own HTTPS host at
`https://<tenant>.guess.local`. The chart values are documented in
[deploy/helm/go-guess/README.md](deploy/helm/go-guess/README.md).
Validate or deploy one of the environment profiles:
One web deployment serves every tenant host because the API resolves the tenant
from the request `Host` header, so a new tenant is one entry in `tenants`.
Install the cluster prerequisites once per cluster. Storage for persistent
volumes, an ingress-nginx controller for the `nginx` ingress class, and
cert-manager for the chart-issued certificate:
```bash
make cluster-prepare
```
Then validate or deploy one of the environment profiles:
```bash
make helm-lint
@ -193,25 +213,41 @@ make helm-deploy-development
make helm-deploy-production
```
For the repository's single-node Docker Rancher setup, run
`make rancher-storage` once before the first production deployment. This
installs the default persistent StorageClass documented in
`rancher-install.txt`.
Development uses `latest` images, development fixtures, and ephemeral database
and broker storage in namespace `go-insane-development`. Production uses the
released `0.0.1` tags, production fixtures, and persistent volumes in namespace
`go-insane-production`.
Development uses demo fixtures, ephemeral database and message-broker storage,
and NodePort `30080`. Production uses production fixtures, persistent volumes,
generated secrets that are preserved across upgrades, and larger resource
limits. It exposes the application at NodePort `31374` for the dedicated-server
Nginx proxy while PostgreSQL and RabbitMQ remain cluster-internal. The public
URL is `https://goguess.urpi.be`; one.com DNS, Nginx, Certbot, firewall, and
verification instructions are in `deploy/nginx/README.md`.
Certificates are self-signed and issued by the chart, so every browser or client
that opens a tenant host needs the CA from the release:
```bash
kubectl -n go-insane-production get secret go-guess-web-ca \
-o jsonpath='{.data.ca\.crt}' | base64 -d > go-guess-ca.crt
```
Point each tenant host at the ingress controller address, then open
`https://nmbs.guess.local` and `https://ypto.guess.local`. Browser sign-in
through Go Loose needs the per-tenant client credentials; pass them from the
environment to `make helm-deploy-production`:
```bash
export GO_LOOSE_NMBS_CLIENT_ID=... GO_LOOSE_NMBS_CLIENT_SECRET=...
export GO_LOOSE_YPTO_CLIENT_ID=... GO_LOOSE_YPTO_CLIENT_SECRET=...
```
Without credentials for a tenant, that host keeps password login. The cluster
served by `~/.kube/config` is managed by Rancher at `rancher.urpi.be`, which is
the management interface and not the deployment target. One.com DNS, Nginx,
Certbot, and firewall instructions for the single-node dedicated server are in
`deploy/nginx/README.md`.
To use external managed services, disable the bundled StatefulSets and provide
full connection URLs:
```bash
helm upgrade --install go-guess deploy/helm/go-guess \
--namespace go-guess-production --create-namespace \
--namespace go-insane-production --create-namespace \
-f deploy/helm/go-guess/values-production.yaml \
--set postgresql.enabled=false \
--set rabbitmq.enabled=false \
@ -229,8 +265,8 @@ workflow succeeds. Create GitHub environments named `development` and
- Secrets `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and
`GO_GUESS_JWT_SECRET`.
- Variable `RANCHER_NAMESPACE` for the target namespace.
- Optional variable `GO_GUESS_FRONTEND_URL` when overriding an environment's
configured public URL.
- Optional secrets `GO_LOOSE_NMBS_CLIENT_ID`, `GO_LOOSE_NMBS_CLIENT_SECRET`,
`GO_LOOSE_YPTO_CLIENT_ID`, and `GO_LOOSE_YPTO_CLIENT_SECRET`.
Because the configured image registry uses HTTP, every Rancher cluster node must
trust `batty1039.startdedicated.net:5000` as an insecure registry. Use HTTPS for

View file

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"strconv"
"strings"
"time"
)
@ -30,6 +31,13 @@ type GoLooseTenant struct {
ClientSecret string
}
// GoLooseCredentialEnv returns the environment variable names holding the Go
// Loose client credentials of a tenant.
func GoLooseCredentialEnv(slug string) (idEnv, secretEnv string) {
prefix := "GO_LOOSE_" + strings.ToUpper(slug)
return prefix + "_CLIENT_ID", prefix + "_CLIENT_SECRET"
}
func Load() (Config, error) {
ttl, err := time.ParseDuration(env("TOKEN_TTL", "8h"))
if err != nil {
@ -62,15 +70,16 @@ func loadGoLoose() GoLooseConfig {
CAFile: os.Getenv("GO_LOOSE_CA_FILE"),
Tenants: map[string]GoLooseTenant{},
}
add := func(slug, idEnv, secretEnv string) {
add := func(slug string) {
idEnv, secretEnv := GoLooseCredentialEnv(slug)
id, secret := os.Getenv(idEnv), os.Getenv(secretEnv)
if id == "" || secret == "" {
return
}
cfg.Tenants[slug] = GoLooseTenant{ClientID: id, ClientSecret: secret}
}
add("nmbs", "GO_LOOSE_NMBS_CLIENT_ID", "GO_LOOSE_NMBS_CLIENT_SECRET")
add("ypto", "GO_LOOSE_YPTO_CLIENT_ID", "GO_LOOSE_YPTO_CLIENT_SECRET")
add("nmbs")
add("ypto")
return cfg
}

View file

@ -2,10 +2,12 @@ package handler
import (
"errors"
"fmt"
"net/http"
goloose "github.com/danyel/go-loose/client"
"github.com/danyel/go-guess/backend/internal/config"
"github.com/danyel/go-guess/backend/internal/golooseauth"
webmapper "github.com/danyel/go-guess/backend/internal/web/mapper"
webmodel "github.com/danyel/go-guess/backend/internal/web/model"
@ -14,6 +16,8 @@ import (
var (
errMissingSession = errors.New("missing Go Loose session")
errTenantMismatch = errors.New("Go Loose session does not match this tenant")
errLoginDisabled = errors.New("Go Loose browser login is not enabled")
errNoTenantHost = errors.New("this host does not identify a configured tenant")
)
func (h *Handler) SetBrowserAuth(auth *golooseauth.Registry) {
@ -28,27 +32,27 @@ func (h *Handler) AppDomain() string {
}
func (h *Handler) BeginExternalLogin(w http.ResponseWriter, r *http.Request) {
auth, ok := h.externalAuth(r)
auth, slug, ok := h.externalAuthWithSlug(r)
if !ok {
http.NotFound(w, r)
writeExternalAuthError(w, h, slug)
return
}
auth.LoginHandler(w, r)
}
func (h *Handler) CompleteExternalLogin(w http.ResponseWriter, r *http.Request) {
auth, ok := h.externalAuth(r)
auth, slug, ok := h.externalAuthWithSlug(r)
if !ok {
http.NotFound(w, r)
writeExternalAuthError(w, h, slug)
return
}
auth.CallbackHandler(w, r)
}
func (h *Handler) ExternalLogout(w http.ResponseWriter, r *http.Request) {
auth, ok := h.externalAuth(r)
auth, slug, ok := h.externalAuthWithSlug(r)
if !ok {
http.NotFound(w, r)
writeExternalAuthError(w, h, slug)
return
}
auth.LogoutHandler(w, r)
@ -57,7 +61,7 @@ func (h *Handler) ExternalLogout(w http.ResponseWriter, r *http.Request) {
func (h *Handler) ExternalSession(w http.ResponseWriter, r *http.Request) {
auth, slug, ok := h.externalAuthWithSlug(r)
if !ok {
http.NotFound(w, r)
writeExternalAuthError(w, h, slug)
return
}
if _, err := r.Cookie(golooseauth.SessionCookie); err != nil {
@ -90,3 +94,17 @@ func (h *Handler) externalAuthWithSlug(r *http.Request) (*goloose.BrowserAuth, s
}
return h.browser.ForHost(r.Host)
}
func writeExternalAuthError(w http.ResponseWriter, h *Handler, slug string) {
if h.browser == nil {
writeError(w, http.StatusNotFound, errLoginDisabled)
return
}
if slug == "" {
writeError(w, http.StatusNotFound, errNoTenantHost)
return
}
idEnv, secretEnv := config.GoLooseCredentialEnv(slug)
writeError(w, http.StatusServiceUnavailable,
fmt.Errorf("Go Loose login is not configured for tenant %q: set %s and %s, then restart the API", slug, idEnv, secretEnv))
}

View file

@ -341,6 +341,52 @@ func TestTenantHostLoginRedirectsToMatchingGoLooseClient(t *testing.T) {
}
}
func TestTenantHostLoginExplainsUnconfiguredGoLooseTenant(t *testing.T) {
h := newTestHandler(t, nil)
browser, err := golooseauth.New(config.GoLooseConfig{
AuthDomain: "auth.dev",
AppDomain: "guess.dev",
Tenants: map[string]config.GoLooseTenant{"nmbs": {ClientID: "glc_nmbs", ClientSecret: "secret"}},
})
if err != nil {
t.Fatal(err)
}
h.SetBrowserAuth(browser)
api := New(h, security.NewTokenManager("01234567890123456789012345678901", time.Hour), "http://localhost:5173")
request := httptest.NewRequest(http.MethodGet, "/api/auth/login", nil)
request.Host = "ypto.guess.dev"
response := httptest.NewRecorder()
api.ServeHTTP(response, request)
if response.Code != http.StatusServiceUnavailable ||
!strings.Contains(response.Body.String(), "GO_LOOSE_YPTO_CLIENT_ID") {
t.Fatalf("ypto login = %d %s", response.Code, response.Body.String())
}
request = httptest.NewRequest(http.MethodGet, "/api/auth/login", nil)
request.Host = "guess.dev"
response = httptest.NewRecorder()
api.ServeHTTP(response, request)
if response.Code != http.StatusNotFound || !strings.Contains(response.Body.String(), "tenant") {
t.Fatalf("apex login = %d %s", response.Code, response.Body.String())
}
}
func TestLoginWithoutGoLooseRegistryIsNotFound(t *testing.T) {
api := New(
newTestHandler(t, nil),
security.NewTokenManager("01234567890123456789012345678901", time.Hour),
"http://localhost:5173",
)
request := httptest.NewRequest(http.MethodGet, "/api/auth/login", nil)
request.Host = "nmbs.guess.dev"
response := httptest.NewRecorder()
api.ServeHTTP(response, request)
if response.Code != http.StatusNotFound || !strings.Contains(response.Body.String(), "not enabled") {
t.Fatalf("login without registry = %d %s", response.Code, response.Body.String())
}
}
type stubDriver struct{}
func (stubDriver) Open(string) (driver.Conn, error) { return stubConn{}, nil }

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.
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.fullname" . }} 8080:{{ .Values.service.port }}
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 }}

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,19 +1,21 @@
image:
# 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
tenants:
- nmbs
- ypto
api:
environment: development
image:
tag: latest
pullPolicy: Always
app:
environment: development
frontendURL: http://localhost:30080
service:
type: NodePort
nodePort: 30080
ingress:
enabled: false
resources:
goLoose:
enabled: true
authDomain: auth.dev
resources:
requests:
cpu: 50m
memory: 96Mi
@ -21,6 +23,22 @@ resources:
cpu: 300m
memory: 384Mi
web:
replicaCount: 1
image:
tag: latest
pullPolicy: Always
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 200m
memory: 96Mi
ingress:
enabled: true
postgresql:
persistence:
enabled: false

View file

@ -1,19 +1,19 @@
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
service:
type: NodePort
nodePort: 31374
ingress:
enabled: false
resources:
image:
tag: "0.0.1"
pullPolicy: IfNotPresent
goLoose:
enabled: true
resources:
requests:
cpu: 200m
memory: 256Mi
@ -21,6 +21,26 @@ resources:
cpu: "1"
memory: 768Mi
web:
replicaCount: 2
image:
tag: "0.0.1"
pullPolicy: IfNotPresent
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 300m
memory: 128Mi
ingress:
enabled: true
className: nginx
certManager:
enabled: true
postgresql:
persistence:
enabled: true

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
goLoose:
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
appDomain: guess.dev
ca:
# Mount an internal CA bundle and point GO_LOOSE_CA_FILE at it.
existingSecret: ""
key: ca.crt
mountPath: /certs/local-ca.crt
tenants: []
# 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
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: ""
# 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: