From 3364eacbccd02a1347c6238c35adf72d38174cd2 Mon Sep 17 00:00:00 2001 From: Daniel Noulet Date: Sun, 20 Sep 2026 10:49:09 +0200 Subject: [PATCH] feat: add Rancher Helm deployments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/deploy-rancher.yml | 153 ++++++++++++++++++ Makefile | 27 +++- README.md | 52 ++++++ deploy/helm/go-guess/.helmignore | 5 + deploy/helm/go-guess/Chart.yaml | 6 + deploy/helm/go-guess/templates/NOTES.txt | 11 ++ deploy/helm/go-guess/templates/_helpers.tpl | 41 +++++ .../go-guess/templates/app-deployment.yaml | 105 ++++++++++++ .../helm/go-guess/templates/app-service.yaml | 19 +++ deploy/helm/go-guess/templates/ingress.yaml | 35 ++++ .../templates/postgresql-service.yaml | 18 +++ .../templates/postgresql-statefulset.yaml | 92 +++++++++++ .../go-guess/templates/rabbitmq-service.yaml | 18 +++ .../templates/rabbitmq-statefulset.yaml | 90 +++++++++++ deploy/helm/go-guess/templates/secret.yaml | 34 ++++ .../templates/tests/test-connection.yaml | 20 +++ deploy/helm/go-guess/values-development.yaml | 44 +++++ deploy/helm/go-guess/values-production.yaml | 41 +++++ deploy/helm/go-guess/values.yaml | 112 +++++++++++++ docker/entrypoint.sh | 2 + 20 files changed, 924 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy-rancher.yml create mode 100644 deploy/helm/go-guess/.helmignore create mode 100644 deploy/helm/go-guess/Chart.yaml create mode 100644 deploy/helm/go-guess/templates/NOTES.txt create mode 100644 deploy/helm/go-guess/templates/_helpers.tpl create mode 100644 deploy/helm/go-guess/templates/app-deployment.yaml create mode 100644 deploy/helm/go-guess/templates/app-service.yaml create mode 100644 deploy/helm/go-guess/templates/ingress.yaml create mode 100644 deploy/helm/go-guess/templates/postgresql-service.yaml create mode 100644 deploy/helm/go-guess/templates/postgresql-statefulset.yaml create mode 100644 deploy/helm/go-guess/templates/rabbitmq-service.yaml create mode 100644 deploy/helm/go-guess/templates/rabbitmq-statefulset.yaml create mode 100644 deploy/helm/go-guess/templates/secret.yaml create mode 100644 deploy/helm/go-guess/templates/tests/test-connection.yaml create mode 100644 deploy/helm/go-guess/values-development.yaml create mode 100644 deploy/helm/go-guess/values-production.yaml create mode 100644 deploy/helm/go-guess/values.yaml diff --git a/.github/workflows/deploy-rancher.yml b/.github/workflows/deploy-rancher.yml new file mode 100644 index 0000000..6df6a0b --- /dev/null +++ b/.github/workflows/deploy-rancher.yml @@ -0,0 +1,153 @@ +name: Deploy to Rancher + +on: + workflow_run: + workflows: + - Publish Docker image + types: + - completed + workflow_dispatch: + inputs: + environment: + description: Deployment environment + required: true + default: development + type: choice + options: + - development + - production + image_tag: + description: Image tag (defaults to latest) + required: false + default: latest + type: string + +permissions: + contents: read + +concurrency: + group: go-guess-${{ github.event_name == 'workflow_dispatch' && inputs.environment || 'production' }} + cancel-in-progress: false + +jobs: + deploy: + if: >- + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + name: Deploy ${{ github.event_name == 'workflow_dispatch' && inputs.environment || 'production' }} + runs-on: ubuntu-latest + environment: ${{ github.event_name == 'workflow_dispatch' && inputs.environment || 'production' }} + env: + TARGET_ENVIRONMENT: ${{ github.event_name == 'workflow_dispatch' && inputs.environment || 'production' }} + KUBE_CONFIG_BASE64: ${{ secrets.RANCHER_KUBE_CONFIG_BASE64 }} + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + JWT_SECRET: ${{ secrets.GO_GUESS_JWT_SECRET }} + GO_GUESS_HOST: ${{ vars.GO_GUESS_HOST }} + GO_GUESS_FRONTEND_URL: ${{ vars.GO_GUESS_FRONTEND_URL }} + GO_GUESS_TLS_SECRET: ${{ vars.GO_GUESS_TLS_SECRET }} + RANCHER_NAMESPACE: ${{ vars.RANCHER_NAMESPACE }} + + steps: + - name: Check out source + uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.18.6 + + - name: Set up kubectl + uses: azure/setup-kubectl@v4 + with: + version: v1.34.1 + + - name: Resolve deployment settings + env: + REQUESTED_TAG: ${{ inputs.image_tag }} + PUBLISHED_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + set -eu + if [ -z "$KUBE_CONFIG_BASE64" ]; then + echo "RANCHER_KUBE_CONFIG_BASE64 is required" >&2 + exit 1 + fi + if [ -z "$REGISTRY_USERNAME" ] || [ -z "$REGISTRY_PASSWORD" ]; then + echo "REGISTRY_USERNAME and REGISTRY_PASSWORD are required" >&2 + exit 1 + fi + if [ -z "$JWT_SECRET" ] || [ "${#JWT_SECRET}" -lt 32 ]; then + echo "GO_GUESS_JWT_SECRET must contain at least 32 characters" >&2 + exit 1 + fi + if [ "$TARGET_ENVIRONMENT" = "production" ] && [ -z "$GO_GUESS_HOST" ]; then + echo "GO_GUESS_HOST is required for production" >&2 + exit 1 + fi + namespace="${RANCHER_NAMESPACE:-go-guess-$TARGET_ENVIRONMENT}" + if [ -n "$PUBLISHED_SHA" ]; then + image_tag="$PUBLISHED_SHA" + else + image_tag="${REQUESTED_TAG:-latest}" + fi + frontend_url="$GO_GUESS_FRONTEND_URL" + if [ "$TARGET_ENVIRONMENT" = "production" ] && [ -z "$frontend_url" ]; then + frontend_url="https://$GO_GUESS_HOST" + fi + { + echo "DEPLOY_NAMESPACE=$namespace" + echo "IMAGE_TAG=$image_tag" + echo "RESOLVED_FRONTEND_URL=$frontend_url" + } >> "$GITHUB_ENV" + + - name: Configure Rancher cluster access + run: | + mkdir -p "$HOME/.kube" + printf '%s' "$KUBE_CONFIG_BASE64" | base64 --decode > "$HOME/.kube/config" + chmod 600 "$HOME/.kube/config" + kubectl cluster-info + + - name: Configure registry credentials + run: | + kubectl create namespace "$DEPLOY_NAMESPACE" \ + --dry-run=client -o yaml | kubectl apply -f - + kubectl -n "$DEPLOY_NAMESPACE" create secret docker-registry go-guess-registry \ + --docker-server=batty1039.startdedicated.net:5000 \ + --docker-username="$REGISTRY_USERNAME" \ + --docker-password="$REGISTRY_PASSWORD" \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Deploy Helm release + run: | + set -eu + values_file="deploy/helm/go-guess/values-$TARGET_ENVIRONMENT.yaml" + set -- \ + --set-string "image.tag=$IMAGE_TAG" \ + --set "imagePullSecrets[0].name=go-guess-registry" \ + --set-string "secrets.jwtSecret=$JWT_SECRET" + if [ -n "$RESOLVED_FRONTEND_URL" ]; then + set -- "$@" --set-string "app.frontendURL=$RESOLVED_FRONTEND_URL" + fi + if [ "$TARGET_ENVIRONMENT" = "production" ]; then + set -- "$@" --set-string "ingress.hosts[0].host=$GO_GUESS_HOST" + if [ -n "$GO_GUESS_TLS_SECRET" ]; then + set -- "$@" \ + --set-string "ingress.tls[0].secretName=$GO_GUESS_TLS_SECRET" \ + --set-string "ingress.tls[0].hosts[0]=$GO_GUESS_HOST" + fi + fi + helm upgrade --install go-guess deploy/helm/go-guess \ + --namespace "$DEPLOY_NAMESPACE" \ + --values "$values_file" \ + --atomic \ + --wait \ + --timeout 10m \ + "$@" + + - name: Verify deployment + run: | + kubectl -n "$DEPLOY_NAMESPACE" rollout status deployment/go-guess \ + --timeout=5m + helm test go-guess --namespace "$DEPLOY_NAMESPACE" --logs diff --git a/Makefile b/Makefile index 793ac58..c26976c 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,12 @@ DATABASE_URL ?= postgres://go_guess:go_guess@localhost:5432/go_guess?sslmode=dis JWT_SECRET ?= local-development-secret-change-me-12345 GOOSE := cd backend && go run github.com/pressly/goose/v3/cmd/goose@v3.26.0 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 -.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 +.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 helm-lint helm-deploy-development helm-deploy-production help: @echo "make dev builds the demo docker image" @@ -27,6 +31,9 @@ help: @echo "make test-env-down stops the test image" @echo "make lint runs lint on the frontend" @echo "make build builds the entire project" + @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" dev: docker compose up --build @@ -89,3 +96,21 @@ lint: build: cd backend && go build ./... cd frontend && npm run build + +helm-lint: + $(HELM) lint $(HELM_CHART) -f $(HELM_CHART)/values-development.yaml + $(HELM) lint $(HELM_CHART) -f $(HELM_CHART)/values-production.yaml + $(HELM) template $(HELM_RELEASE) $(HELM_CHART) \ + -f $(HELM_CHART)/values-development.yaml >/dev/null + $(HELM) template $(HELM_RELEASE) $(HELM_CHART) \ + -f $(HELM_CHART)/values-production.yaml >/dev/null + +helm-deploy-development: + $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART) \ + --namespace $(HELM_NAMESPACE)-development --create-namespace \ + -f $(HELM_CHART)/values-development.yaml + +helm-deploy-production: + $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART) \ + --namespace $(HELM_NAMESPACE)-production --create-namespace \ + -f $(HELM_CHART)/values-production.yaml diff --git a/README.md b/README.md index 9ab82fb..d0b9fbe 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,58 @@ Pushes to `master` publish `go-guess:latest` and `go-guess:` 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. + +Validate or deploy one of the environment profiles: + +```bash +make helm-lint +make helm-deploy-development +make helm-deploy-production +``` + +Development uses demo fixtures, ephemeral database and message-broker storage, +and NodePort `30080`. Production uses production fixtures, persistent volumes, +an Nginx ingress, generated secrets that are preserved across upgrades, and +larger resource limits. Before production deployment, replace +`go-guess.example.com` in `values-production.yaml` or override +`app.frontendURL` and `ingress.hosts[0].host`. Configure a TLS entry under +`ingress.tls` when the Rancher cluster does not provide TLS separately. + +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 \ + -f deploy/helm/go-guess/values-production.yaml \ + --set postgresql.enabled=false \ + --set rabbitmq.enabled=false \ + --set-string secrets.databaseURL="$DATABASE_URL" \ + --set-string secrets.rabbitmqURL="$RABBITMQ_URL" +``` + +`.github/workflows/deploy-rancher.yml` supports manual development or production +deployment. It also deploys production automatically after the image publishing +workflow succeeds. Create GitHub environments named `development` and +`production`, then configure: + +- Secret `RANCHER_KUBE_CONFIG_BASE64`: base64-encoded kubeconfig downloaded from + Rancher. +- Secrets `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and + `GO_GUESS_JWT_SECRET`. +- Variable `RANCHER_NAMESPACE` for the target namespace. +- Production variable `GO_GUESS_HOST`, plus optional + `GO_GUESS_FRONTEND_URL` and `GO_GUESS_TLS_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 +the registry in production when possible. + ## Architecture Requests enter the Chi router and web handlers under `backend/internal/web`. diff --git a/deploy/helm/go-guess/.helmignore b/deploy/helm/go-guess/.helmignore new file mode 100644 index 0000000..cb173e2 --- /dev/null +++ b/deploy/helm/go-guess/.helmignore @@ -0,0 +1,5 @@ +.git/ +.github/ +.idea/ +.vscode/ +*.tgz diff --git a/deploy/helm/go-guess/Chart.yaml b/deploy/helm/go-guess/Chart.yaml new file mode 100644 index 0000000..22e298e --- /dev/null +++ b/deploy/helm/go-guess/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: go-guess +description: Go Guess recruitment workspace +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/deploy/helm/go-guess/templates/NOTES.txt b/deploy/helm/go-guess/templates/NOTES.txt new file mode 100644 index 0000000..678da57 --- /dev/null +++ b/deploy/helm/go-guess/templates/NOTES.txt @@ -0,0 +1,11 @@ +Go Guess has been installed in namespace {{ .Release.Namespace }}. + +{{- if .Values.ingress.enabled }} +Open {{ .Values.app.frontendURL }} +{{- else if eq .Values.service.type "NodePort" }} +Open a cluster node on port {{ .Values.service.nodePort }}. +{{- 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. +{{- end }} diff --git a/deploy/helm/go-guess/templates/_helpers.tpl b/deploy/helm/go-guess/templates/_helpers.tpl new file mode 100644 index 0000000..50fe08c --- /dev/null +++ b/deploy/helm/go-guess/templates/_helpers.tpl @@ -0,0 +1,41 @@ +{{- define "go-guess.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "go-guess.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 "go-guess.labels" -}} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} +app.kubernetes.io/name: {{ include "go-guess.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "go-guess.selectorLabels" -}} +app.kubernetes.io/name: {{ include "go-guess.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "go-guess.secretName" -}} +{{- printf "%s-secrets" (include "go-guess.fullname" .) }} +{{- end }} + +{{- define "go-guess.postgresqlName" -}} +{{- printf "%s-postgresql" (include "go-guess.fullname" .) }} +{{- end }} + +{{- define "go-guess.rabbitmqName" -}} +{{- printf "%s-rabbitmq" (include "go-guess.fullname" .) }} +{{- end }} diff --git a/deploy/helm/go-guess/templates/app-deployment.yaml b/deploy/helm/go-guess/templates/app-deployment.yaml new file mode 100644 index 0000000..d271706 --- /dev/null +++ b/deploy/helm/go-guess/templates/app-deployment.yaml @@ -0,0 +1,105 @@ +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 }} diff --git a/deploy/helm/go-guess/templates/app-service.yaml b/deploy/helm/go-guess/templates/app-service.yaml new file mode 100644 index 0000000..1d5e71a --- /dev/null +++ b/deploy/helm/go-guess/templates/app-service.yaml @@ -0,0 +1,19 @@ +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 }} diff --git a/deploy/helm/go-guess/templates/ingress.yaml b/deploy/helm/go-guess/templates/ingress.yaml new file mode 100644 index 0000000..9b7db99 --- /dev/null +++ b/deploy/helm/go-guess/templates/ingress.yaml @@ -0,0 +1,35 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "go-guess.fullname" . }} + labels: + {{- include "go-guess.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "go-guess.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/go-guess/templates/postgresql-service.yaml b/deploy/helm/go-guess/templates/postgresql-service.yaml new file mode 100644 index 0000000..16ec84b --- /dev/null +++ b/deploy/helm/go-guess/templates/postgresql-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.postgresql.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "go-guess.postgresqlName" . }} + labels: + {{- include "go-guess.labels" . | nindent 4 }} + app.kubernetes.io/component: postgresql +spec: + clusterIP: None + selector: + {{- include "go-guess.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: postgresql + ports: + - name: postgresql + port: 5432 + targetPort: postgresql +{{- end }} diff --git a/deploy/helm/go-guess/templates/postgresql-statefulset.yaml b/deploy/helm/go-guess/templates/postgresql-statefulset.yaml new file mode 100644 index 0000000..376629d --- /dev/null +++ b/deploy/helm/go-guess/templates/postgresql-statefulset.yaml @@ -0,0 +1,92 @@ +{{- if .Values.postgresql.enabled }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "go-guess.postgresqlName" . }} + labels: + {{- include "go-guess.labels" . | nindent 4 }} + app.kubernetes.io/component: postgresql +spec: + serviceName: {{ include "go-guess.postgresqlName" . }} + replicas: 1 + selector: + matchLabels: + {{- include "go-guess.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: postgresql + template: + metadata: + labels: + {{- include "go-guess.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: postgresql + spec: + automountServiceAccountToken: false + securityContext: + fsGroup: 70 + fsGroupChangePolicy: OnRootMismatch + containers: + - name: postgresql + image: "{{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}" + imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: 70 + env: + - name: POSTGRES_DB + value: {{ .Values.postgresql.auth.database | quote }} + - name: POSTGRES_USER + value: {{ .Values.postgresql.auth.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "go-guess.secretName" . }} + key: postgresql-password + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - name: postgresql + containerPort: 5432 + readinessProbe: + exec: + command: + - sh + - -ec + - pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + exec: + command: + - sh + - -ec + - pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" + initialDelaySeconds: 20 + periodSeconds: 10 + resources: + {{- toYaml .Values.postgresql.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + {{- if not .Values.postgresql.persistence.enabled }} + volumes: + - name: data + emptyDir: {} + {{- end }} + {{- if .Values.postgresql.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: + - ReadWriteOnce + {{- with .Values.postgresql.persistence.storageClass }} + storageClassName: {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.postgresql.persistence.size }} + {{- end }} +{{- end }} diff --git a/deploy/helm/go-guess/templates/rabbitmq-service.yaml b/deploy/helm/go-guess/templates/rabbitmq-service.yaml new file mode 100644 index 0000000..2783262 --- /dev/null +++ b/deploy/helm/go-guess/templates/rabbitmq-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.rabbitmq.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "go-guess.rabbitmqName" . }} + labels: + {{- include "go-guess.labels" . | nindent 4 }} + app.kubernetes.io/component: rabbitmq +spec: + clusterIP: None + selector: + {{- include "go-guess.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: rabbitmq + ports: + - name: amqp + port: 5672 + targetPort: amqp +{{- end }} diff --git a/deploy/helm/go-guess/templates/rabbitmq-statefulset.yaml b/deploy/helm/go-guess/templates/rabbitmq-statefulset.yaml new file mode 100644 index 0000000..256331b --- /dev/null +++ b/deploy/helm/go-guess/templates/rabbitmq-statefulset.yaml @@ -0,0 +1,90 @@ +{{- if .Values.rabbitmq.enabled }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "go-guess.rabbitmqName" . }} + labels: + {{- include "go-guess.labels" . | nindent 4 }} + app.kubernetes.io/component: rabbitmq +spec: + serviceName: {{ include "go-guess.rabbitmqName" . }} + replicas: 1 + selector: + matchLabels: + {{- include "go-guess.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: rabbitmq + template: + metadata: + labels: + {{- include "go-guess.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: rabbitmq + spec: + automountServiceAccountToken: false + securityContext: + fsGroup: 100 + fsGroupChangePolicy: OnRootMismatch + containers: + - name: rabbitmq + image: "{{ .Values.rabbitmq.image.repository }}:{{ .Values.rabbitmq.image.tag }}" + imagePullPolicy: {{ .Values.rabbitmq.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: 100 + env: + - name: RABBITMQ_DEFAULT_USER + value: {{ .Values.rabbitmq.auth.username | quote }} + - name: RABBITMQ_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: {{ include "go-guess.secretName" . }} + key: rabbitmq-password + ports: + - name: amqp + containerPort: 5672 + readinessProbe: + exec: + command: + - rabbitmq-diagnostics + - -q + - ping + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + livenessProbe: + exec: + command: + - rabbitmq-diagnostics + - -q + - ping + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + resources: + {{- toYaml .Values.rabbitmq.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/rabbitmq + {{- if not .Values.rabbitmq.persistence.enabled }} + volumes: + - name: data + emptyDir: {} + {{- end }} + {{- if .Values.rabbitmq.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: + - ReadWriteOnce + {{- with .Values.rabbitmq.persistence.storageClass }} + storageClassName: {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.rabbitmq.persistence.size }} + {{- end }} +{{- end }} diff --git a/deploy/helm/go-guess/templates/secret.yaml b/deploy/helm/go-guess/templates/secret.yaml new file mode 100644 index 0000000..f6489ec --- /dev/null +++ b/deploy/helm/go-guess/templates/secret.yaml @@ -0,0 +1,34 @@ +{{- $name := include "go-guess.secretName" . -}} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $name -}} +{{- $jwt := .Values.secrets.jwtSecret -}} +{{- $postgresPassword := .Values.secrets.postgresqlPassword -}} +{{- $rabbitmqPassword := .Values.secrets.rabbitmqPassword -}} +{{- if and (not $jwt) $existing }}{{ $jwt = index $existing.data "jwt-secret" | b64dec }}{{ end -}} +{{- if and (not $postgresPassword) $existing }}{{ $postgresPassword = index $existing.data "postgresql-password" | b64dec }}{{ end -}} +{{- if and (not $rabbitmqPassword) $existing }}{{ $rabbitmqPassword = index $existing.data "rabbitmq-password" | b64dec }}{{ end -}} +{{- if not $jwt }}{{ $jwt = randAlphaNum 48 }}{{ end -}} +{{- if not $postgresPassword }}{{ $postgresPassword = randAlphaNum 32 }}{{ end -}} +{{- if not $rabbitmqPassword }}{{ $rabbitmqPassword = randAlphaNum 32 }}{{ end -}} +{{- $databaseURL := .Values.secrets.databaseURL -}} +{{- if and (not $databaseURL) .Values.postgresql.enabled -}} +{{- $databaseURL = printf "postgres://%s:%s@%s:5432/%s?sslmode=disable" .Values.postgresql.auth.username ($postgresPassword | urlquery) (include "go-guess.postgresqlName" .) .Values.postgresql.auth.database -}} +{{- end -}} +{{- if not $databaseURL }}{{ fail "secrets.databaseURL is required when postgresql.enabled is false" }}{{ end -}} +{{- $rabbitmqURL := .Values.secrets.rabbitmqURL -}} +{{- if and (not $rabbitmqURL) .Values.rabbitmq.enabled -}} +{{- $rabbitmqURL = printf "amqp://%s:%s@%s:5672/" .Values.rabbitmq.auth.username ($rabbitmqPassword | urlquery) (include "go-guess.rabbitmqName" .) -}} +{{- end -}} +{{- if not $rabbitmqURL }}{{ fail "secrets.rabbitmqURL is required when rabbitmq.enabled is false" }}{{ end -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: + {{- include "go-guess.labels" . | nindent 4 }} +type: Opaque +data: + jwt-secret: {{ $jwt | b64enc | quote }} + database-url: {{ $databaseURL | b64enc | quote }} + rabbitmq-url: {{ $rabbitmqURL | b64enc | quote }} + postgresql-password: {{ $postgresPassword | b64enc | quote }} + rabbitmq-password: {{ $rabbitmqPassword | b64enc | quote }} diff --git a/deploy/helm/go-guess/templates/tests/test-connection.yaml b/deploy/helm/go-guess/templates/tests/test-connection.yaml new file mode 100644 index 0000000..ea6d661 --- /dev/null +++ b/deploy/helm/go-guess/templates/tests/test-connection.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "go-guess.fullname" . }}-test-connection" + labels: + {{- include "go-guess.labels" . | nindent 4 }} + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation +spec: + automountServiceAccountToken: false + restartPolicy: Never + containers: + - name: wget + image: busybox:1.37 + command: + - wget + args: + - -qO- + - "http://{{ include "go-guess.fullname" . }}:{{ .Values.service.port }}/api/health" diff --git a/deploy/helm/go-guess/values-development.yaml b/deploy/helm/go-guess/values-development.yaml new file mode 100644 index 0000000..ff0d4f4 --- /dev/null +++ b/deploy/helm/go-guess/values-development.yaml @@ -0,0 +1,44 @@ +image: + tag: latest + pullPolicy: Always + +app: + environment: development + frontendURL: http://localhost:30080 + +service: + type: NodePort + nodePort: 30080 + +ingress: + enabled: false + +resources: + requests: + cpu: 50m + memory: 96Mi + limits: + cpu: 300m + memory: 384Mi + +postgresql: + persistence: + enabled: false + resources: + requests: + cpu: 50m + memory: 96Mi + limits: + cpu: 300m + memory: 384Mi + +rabbitmq: + persistence: + enabled: false + resources: + requests: + cpu: 50m + memory: 96Mi + limits: + cpu: 300m + memory: 384Mi diff --git a/deploy/helm/go-guess/values-production.yaml b/deploy/helm/go-guess/values-production.yaml new file mode 100644 index 0000000..3ecb0d9 --- /dev/null +++ b/deploy/helm/go-guess/values-production.yaml @@ -0,0 +1,41 @@ +image: + tag: latest + pullPolicy: IfNotPresent + +app: + environment: production + frontendURL: https://go-guess.example.com + +service: + type: ClusterIP + +ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: 10m + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + hosts: + - host: go-guess.example.com + paths: + - path: / + pathType: Prefix + tls: [] + +resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: "1" + memory: 768Mi + +postgresql: + persistence: + enabled: true + size: 20Gi + +rabbitmq: + persistence: + enabled: true + size: 8Gi diff --git a/deploy/helm/go-guess/values.yaml b/deploy/helm/go-guess/values.yaml new file mode 100644 index 0000000..6537aa2 --- /dev/null +++ b/deploy/helm/go-guess/values.yaml @@ -0,0 +1,112 @@ +nameOverride: "" +fullnameOverride: "" + +replicaCount: 1 + +image: + repository: batty1039.startdedicated.net:5000/go-guess + tag: latest + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +app: + environment: production + frontendURL: http://localhost:8080 + tokenTTL: 8h + maxUploadMB: 10 + +secrets: + jwtSecret: "" + databaseURL: "" + rabbitmqURL: "" + 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: + repository: postgres + tag: 18-alpine + pullPolicy: IfNotPresent + auth: + username: go_guess + database: go_guess + persistence: + enabled: true + size: 8Gi + storageClass: "" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +rabbitmq: + enabled: true + image: + repository: rabbitmq + tag: 4-alpine + pullPolicy: IfNotPresent + auth: + username: go_guess + persistence: + enabled: true + size: 4Gi + storageClass: "" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +nodeSelector: {} +tolerations: [] +affinity: {} diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 0de1df0..64acaab 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -11,6 +11,8 @@ shutdown() { goose -dir /app/migrations postgres "$DATABASE_URL" up seed +mkdir -p /tmp/nginx + api & api_pid=$! nginx -e /dev/stderr -g 'daemon off;' &