diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml new file mode 100644 index 0000000..ef75f10 --- /dev/null +++ b/.github/workflows/helm-release.yml @@ -0,0 +1,59 @@ +name: Helm Release + +# Publishes the vesting-backend Helm chart to GitHub Pages. +# Triggers on tags matching helm-v* (e.g. helm-v0.1.0). +on: + push: + tags: + - 'helm-v*' + # Allow manual trigger for initial publish + workflow_dispatch: + +jobs: + release: + name: Package & Publish Helm Chart + runs-on: ubuntu-latest + permissions: + contents: write # push to gh-pages branch + pages: write + id-token: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: '3.14.0' + + - name: Package chart + run: | + mkdir -p docs/helm-charts + helm package helm/vesting-backend --destination docs/helm-charts/ + + - name: Update Helm repo index + run: | + helm repo index docs/helm-charts/ \ + --url https://faveteamz.github.io/workload-governor/helm-charts \ + --merge docs/helm-charts/index.yaml + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Commit and push updated index + run: | + git add docs/helm-charts/ + git commit -m "chore: publish Helm chart ${{ github.ref_name }}" || echo "Nothing to commit" + git push origin HEAD:main + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/helm-charts + destination_dir: helm-charts + keep_files: true diff --git a/docs/helm-charts/index.yaml b/docs/helm-charts/index.yaml new file mode 100644 index 0000000..1f71b5d --- /dev/null +++ b/docs/helm-charts/index.yaml @@ -0,0 +1,3 @@ +apiVersion: v1 +entries: {} +generated: "2026-07-01T00:00:00Z" diff --git a/helm/vesting-backend/Chart.yaml b/helm/vesting-backend/Chart.yaml new file mode 100644 index 0000000..d0e8fd7 --- /dev/null +++ b/helm/vesting-backend/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: vesting-backend +description: | + Helm chart for the WorkloadGovernor vesting-backend service. + Deploys the Node.js API server that proxies Soroban contract calls, + alongside an HPA, Ingress, ConfigMap, and ExternalSecret integration. +type: application +version: 0.1.0 +appVersion: "1.0.0" +keywords: + - workload-governor + - stellar + - soroban + - vesting +home: https://github.com/FaveTeamz/workload-governor +sources: + - https://github.com/FaveTeamz/workload-governor +maintainers: + - name: FaveTeamz + url: https://github.com/FaveTeamz diff --git a/helm/vesting-backend/README.md b/helm/vesting-backend/README.md new file mode 100644 index 0000000..d44ab56 --- /dev/null +++ b/helm/vesting-backend/README.md @@ -0,0 +1,204 @@ +# vesting-backend Helm Chart + +Deploys the **WorkloadGovernor vesting-backend** service on any Kubernetes cluster. +The chart includes a `Deployment`, `Service`, `Ingress`, `HPA`, `ConfigMap`, and +an `ExternalSecret` (via the [External Secrets Operator](https://external-secrets.io/)). + +## Prerequisites + +| Requirement | Version | +|---|---| +| Kubernetes | ≥ 1.25 | +| Helm | ≥ 3.10 | +| [External Secrets Operator](https://external-secrets.io/) | ≥ 0.9 (optional, disable with `externalSecrets.enabled=false`) | +| metrics-server | ≥ 0.6 (required for memory-based HPA) | + +--- + +## Install + +```bash +# Add the Helm repo (GitHub Pages) +helm repo add workload-governor https://faveteamz.github.io/workload-governor +helm repo update + +# Dry-run first +helm install vesting-backend workload-governor/vesting-backend \ + --dry-run --debug \ + --set config.CONTRACT_ID= + +# Install +helm install vesting-backend workload-governor/vesting-backend \ + --namespace vesting \ + --create-namespace \ + --set config.CONTRACT_ID= +``` + +### Local (kind) install + +```bash +# Create a local cluster +kind create cluster --name wg-dev + +# Install from local chart directory +helm install vesting-backend ./helm/vesting-backend \ + --namespace vesting \ + --create-namespace \ + --set externalSecrets.enabled=false \ + --set config.CONTRACT_ID= \ + --set ingress.enabled=false + +# Verify +kubectl get all -n vesting +helm test vesting-backend -n vesting +``` + +--- + +## Upgrade + +```bash +helm upgrade vesting-backend workload-governor/vesting-backend \ + --namespace vesting \ + --reuse-values \ + --set image.tag= +``` + +--- + +## Uninstall + +```bash +helm uninstall vesting-backend --namespace vesting +``` + +--- + +## Configuration + +All values can be overridden with `--set key=value` or a custom `-f values.yaml` file. + +### Core + +| Key | Default | Description | +|---|---|---| +| `replicaCount` | `2` | Number of pod replicas (ignored when HPA enabled) | +| `image.repository` | `ghcr.io/faveteamz/workload-governor` | Container image | +| `image.tag` | `""` (uses `appVersion`) | Image tag | +| `image.pullPolicy` | `IfNotPresent` | Image pull policy | +| `nameOverride` | `""` | Partial chart name override | +| `fullnameOverride` | `""` | Full release name override | + +### Service + +| Key | Default | Description | +|---|---|---| +| `service.type` | `ClusterIP` | Service type | +| `service.port` | `3000` | Service port | +| `service.targetPort` | `3000` | Container port | + +### Ingress + +| Key | Default | Description | +|---|---|---| +| `ingress.enabled` | `true` | Create Ingress resource | +| `ingress.className` | `nginx` | Ingress class | +| `ingress.annotations` | See values.yaml | Custom annotations | +| `ingress.hosts` | `vesting-backend.example.com` | Hostname rules | +| `ingress.tls` | See values.yaml | TLS secret mappings | + +### HPA + +| Key | Default | Description | +|---|---|---| +| `autoscaling.enabled` | `true` | Enable HPA | +| `autoscaling.minReplicas` | `2` | Minimum replicas | +| `autoscaling.maxReplicas` | `10` | Maximum replicas | +| `autoscaling.targetCPUUtilizationPercentage` | `70` | Target CPU % | +| `autoscaling.targetMemoryUtilizationPercentage` | `80` | Target memory % | + +### Application Config (ConfigMap) + +| Key | Default | Description | +|---|---|---| +| `config.STELLAR_NETWORK` | `testnet` | Stellar network | +| `config.SOROBAN_RPC_URL` | `https://soroban-testnet.stellar.org` | RPC endpoint | +| `config.HORIZON_URL` | `https://horizon-testnet.stellar.org` | Horizon endpoint | +| `config.CONTRACT_ID` | `""` | Deployed contract ID — **must be set** | +| `config.NODE_ENV` | `production` | Node environment | +| `config.PORT` | `3000` | Listening port | +| `config.LOG_LEVEL` | `info` | Log verbosity | + +### External Secrets + +| Key | Default | Description | +|---|---|---| +| `externalSecrets.enabled` | `true` | Enable ExternalSecret resource | +| `externalSecrets.secretStoreName` | `aws-secrets-manager` | SecretStore name | +| `externalSecrets.secretStoreKind` | `ClusterSecretStore` | Store kind | +| `externalSecrets.refreshInterval` | `1h` | Refresh interval | +| `externalSecrets.secrets` | See values.yaml | Secret key mappings | + +Secrets synced by default: + +| Env Var | AWS Secrets Manager Path | +|---|---| +| `ADMIN_TOKEN` | `workload-governor/admin-token` | +| `STELLAR_SECRET_KEY` | `workload-governor/stellar-secret-key` | +| `DATABASE_URL` | `workload-governor/database-url` | +| `REDIS_URL` | `workload-governor/redis-url` | + +To disable and use a plain Kubernetes Secret instead: + +```bash +helm install vesting-backend ./helm/vesting-backend \ + --set externalSecrets.enabled=false +``` + +Then create the secret manually: + +```bash +kubectl create secret generic vesting-backend-secrets \ + --from-literal=ADMIN_TOKEN= \ + --from-literal=STELLAR_SECRET_KEY= \ + --from-literal=DATABASE_URL= \ + --from-literal=REDIS_URL= \ + --namespace vesting +``` + +### Resources + +| Key | Default | +|---|---| +| `resources.requests.cpu` | `100m` | +| `resources.requests.memory` | `128Mi` | +| `resources.limits.cpu` | `500m` | +| `resources.limits.memory` | `512Mi` | + +--- + +## Publishing to GitHub Pages + +The chart is published as a Helm repository on GitHub Pages. + +```bash +# From repo root — package and index +helm package helm/vesting-backend --destination docs/helm-charts/ +helm repo index docs/helm-charts/ --url https://faveteamz.github.io/workload-governor/helm-charts + +# Push and the gh-pages workflow will deploy automatically +git add docs/helm-charts/ +git commit -m "chore: publish vesting-backend chart" +git push +``` + +The GitHub Actions workflow at `.github/workflows/helm-release.yml` automates this on every tag push matching `helm-v*`. + +--- + +## Runbooks + +- [Contract upgrade](../../docs/runbooks/contract-upgrade.md) +- [Admin key rotation](../../docs/runbooks/admin-key-rotation.md) +- [Cap emergency increase](../../docs/runbooks/cap-emergency-increase.md) +- [Incident response](../../docs/runbooks/incident-response.md) diff --git a/helm/vesting-backend/templates/NOTES.txt b/helm/vesting-backend/templates/NOTES.txt new file mode 100644 index 0000000..1f26371 --- /dev/null +++ b/helm/vesting-backend/templates/NOTES.txt @@ -0,0 +1,32 @@ +🚀 vesting-backend deployed successfully! + +Release: {{ .Release.Name }} +Namespace: {{ .Release.Namespace }} +Version: {{ include "vesting-backend.imageTag" . }} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ACCESS +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + https://{{ .host }}/health +{{- end }} +{{- else }} + kubectl port-forward svc/{{ include "vesting-backend.fullname" . }} 3000:{{ .Values.service.port }} -n {{ .Release.Namespace }} + Then visit: http://localhost:3000/health +{{- end }} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + NEXT STEPS +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +1. Ensure a ClusterSecretStore named "{{ .Values.externalSecrets.secretStoreName }}" exists + (or disable ExternalSecrets with --set externalSecrets.enabled=false). + +2. Set your CONTRACT_ID: + helm upgrade {{ .Release.Name }} . --set config.CONTRACT_ID= + +3. Confirm pods are running: + kubectl get pods -l app.kubernetes.io/name={{ include "vesting-backend.name" . }} -n {{ .Release.Namespace }} + +4. Check logs: + kubectl logs -l app.kubernetes.io/name={{ include "vesting-backend.name" . }} -n {{ .Release.Namespace }} --tail=50 diff --git a/helm/vesting-backend/templates/_helpers.tpl b/helm/vesting-backend/templates/_helpers.tpl new file mode 100644 index 0000000..c4a9bdd --- /dev/null +++ b/helm/vesting-backend/templates/_helpers.tpl @@ -0,0 +1,68 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "vesting-backend.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited. +*/}} +{{- define "vesting-backend.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 }} + +{{/* +Create chart label (chart name + version, safe for label values). +*/}} +{{- define "vesting-backend.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels applied to every resource. +*/}} +{{- define "vesting-backend.labels" -}} +helm.sh/chart: {{ include "vesting-backend.chart" . }} +{{ include "vesting-backend.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels used by Deployments and Services. +*/}} +{{- define "vesting-backend.selectorLabels" -}} +app.kubernetes.io/name: {{ include "vesting-backend.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Resolved service account name. +*/}} +{{- define "vesting-backend.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "vesting-backend.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Resolved image tag (falls back to chart appVersion). +*/}} +{{- define "vesting-backend.imageTag" -}} +{{- .Values.image.tag | default .Chart.AppVersion }} +{{- end }} diff --git a/helm/vesting-backend/templates/configmap.yaml b/helm/vesting-backend/templates/configmap.yaml new file mode 100644 index 0000000..c8e345c --- /dev/null +++ b/helm/vesting-backend/templates/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "vesting-backend.fullname" . }}-config + labels: + {{- include "vesting-backend.labels" . | nindent 4 }} +data: + {{- range $key, $value := .Values.config }} + {{ $key }}: {{ $value | quote }} + {{- end }} diff --git a/helm/vesting-backend/templates/deployment.yaml b/helm/vesting-backend/templates/deployment.yaml new file mode 100644 index 0000000..57381db --- /dev/null +++ b/helm/vesting-backend/templates/deployment.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "vesting-backend.fullname" . }} + labels: + {{- include "vesting-backend.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "vesting-backend.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + # Force pod restart when ConfigMap content changes + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "vesting-backend.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "vesting-backend.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.containerSecurityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ include "vesting-backend.imageTag" . }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "vesting-backend.fullname" . }}-config + {{- if .Values.externalSecrets.enabled }} + - secretRef: + name: {{ include "vesting-backend.fullname" . }}-secrets + {{- end }} + {{- with .Values.extraEnv }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.extraVolumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.extraVolumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- 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 }} diff --git a/helm/vesting-backend/templates/externalsecret.yaml b/helm/vesting-backend/templates/externalsecret.yaml new file mode 100644 index 0000000..6a58512 --- /dev/null +++ b/helm/vesting-backend/templates/externalsecret.yaml @@ -0,0 +1,25 @@ +{{- if .Values.externalSecrets.enabled }} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: {{ include "vesting-backend.fullname" . }}-secrets + labels: + {{- include "vesting-backend.labels" . | nindent 4 }} +spec: + refreshInterval: {{ .Values.externalSecrets.refreshInterval }} + secretStoreRef: + name: {{ .Values.externalSecrets.secretStoreName }} + kind: {{ .Values.externalSecrets.secretStoreKind }} + target: + name: {{ include "vesting-backend.fullname" . }}-secrets + creationPolicy: Owner + data: + {{- range .Values.externalSecrets.secrets }} + - secretKey: {{ .secretKey }} + remoteRef: + key: {{ .remoteRef.key }} + {{- if .remoteRef.property }} + property: {{ .remoteRef.property }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/vesting-backend/templates/hpa.yaml b/helm/vesting-backend/templates/hpa.yaml new file mode 100644 index 0000000..b3d9bdb --- /dev/null +++ b/helm/vesting-backend/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "vesting-backend.fullname" . }} + labels: + {{- include "vesting-backend.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "vesting-backend.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/vesting-backend/templates/ingress.yaml b/helm/vesting-backend/templates/ingress.yaml new file mode 100644 index 0000000..863c55f --- /dev/null +++ b/helm/vesting-backend/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "vesting-backend.fullname" . }} + labels: + {{- include "vesting-backend.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "vesting-backend.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/vesting-backend/templates/service.yaml b/helm/vesting-backend/templates/service.yaml new file mode 100644 index 0000000..947d49a --- /dev/null +++ b/helm/vesting-backend/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "vesting-backend.fullname" . }} + labels: + {{- include "vesting-backend.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http + selector: + {{- include "vesting-backend.selectorLabels" . | nindent 4 }} diff --git a/helm/vesting-backend/templates/serviceaccount.yaml b/helm/vesting-backend/templates/serviceaccount.yaml new file mode 100644 index 0000000..1def9fd --- /dev/null +++ b/helm/vesting-backend/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "vesting-backend.serviceAccountName" . }} + labels: + {{- include "vesting-backend.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/vesting-backend/values.yaml b/helm/vesting-backend/values.yaml new file mode 100644 index 0000000..8f8a7c3 --- /dev/null +++ b/helm/vesting-backend/values.yaml @@ -0,0 +1,202 @@ +# ───────────────────────────────────────────────────────────────────────────── +# vesting-backend Helm chart – default values +# Override any value with --set key=value or -f custom-values.yaml +# ───────────────────────────────────────────────────────────────────────────── + +# -- Number of desired pod replicas (ignored when HPA is enabled) +replicaCount: 2 + +image: + # -- Container image repository + repository: ghcr.io/faveteamz/workload-governor + # -- Image pull policy: Always | IfNotPresent | Never + pullPolicy: IfNotPresent + # -- Image tag; defaults to the chart appVersion when empty + tag: "" + +# -- List of image pull secret names +imagePullSecrets: [] + +# -- String to partially override the chart name +nameOverride: "" +# -- String to fully override the release full name +fullnameOverride: "" + +# ─── Service Account ───────────────────────────────────────────────────────── +serviceAccount: + # -- Create a dedicated service account + create: true + # -- Automatically mount the service account API token + automount: true + # -- Additional annotations for the service account + annotations: {} + # -- Override the service account name (auto-generated when empty) + name: "" + +# ─── Pod annotations & labels ──────────────────────────────────────────────── +# -- Annotations added to every pod +podAnnotations: {} +# -- Labels added to every pod +podLabels: {} + +# ─── Security ──────────────────────────────────────────────────────────────── +podSecurityContext: + # -- Run as non-root user + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + +containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + +# ─── Service ───────────────────────────────────────────────────────────────── +service: + # -- Service type: ClusterIP | NodePort | LoadBalancer + type: ClusterIP + # -- Container port exposed by the application + port: 3000 + # -- Service port (clients connect here) + targetPort: 3000 + +# ─── Ingress ───────────────────────────────────────────────────────────────── +ingress: + # -- Enable Ingress resource + enabled: true + # -- Ingress class name (e.g. nginx, traefik) + className: "nginx" + # -- Additional annotations (e.g. cert-manager, rate limiting) + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/proxy-body-size: "1m" + # -- Ingress hostname rules + hosts: + - host: vesting-backend.example.com + paths: + - path: / + pathType: Prefix + # -- TLS configuration + tls: + - secretName: vesting-backend-tls + hosts: + - vesting-backend.example.com + +# ─── Resources ─────────────────────────────────────────────────────────────── +resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" + +# ─── Horizontal Pod Autoscaler ─────────────────────────────────────────────── +autoscaling: + # -- Enable HPA + enabled: true + # -- Minimum number of replicas + minReplicas: 2 + # -- Maximum number of replicas + maxReplicas: 10 + # -- Target CPU utilisation percentage + targetCPUUtilizationPercentage: 70 + # -- Target memory utilisation percentage (requires metrics-server) + targetMemoryUtilizationPercentage: 80 + +# ─── Probes ────────────────────────────────────────────────────────────────── +livenessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 15 + periodSeconds: 20 + failureThreshold: 3 + +readinessProbe: + httpGet: + path: /ready + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + +# ─── Application config (ConfigMap) ───────────────────────────────────────── +config: + # -- Stellar network to connect to: testnet | mainnet + STELLAR_NETWORK: "testnet" + # -- Soroban RPC endpoint + SOROBAN_RPC_URL: "https://soroban-testnet.stellar.org" + # -- Horizon API endpoint + HORIZON_URL: "https://horizon-testnet.stellar.org" + # -- Deployed contract ID (override per-environment) + CONTRACT_ID: "" + # -- Node.js environment + NODE_ENV: "production" + # -- Listening port (must match service.targetPort) + PORT: "3000" + # -- Log level: debug | info | warn | error + LOG_LEVEL: "info" + +# ─── Secrets via External Secrets Operator ─────────────────────────────────── +externalSecrets: + # -- Enable ExternalSecret resource (requires external-secrets operator) + enabled: true + # -- Name of the SecretStore or ClusterSecretStore to use + secretStoreName: "aws-secrets-manager" + # -- SecretStore kind: SecretStore | ClusterSecretStore + secretStoreKind: "ClusterSecretStore" + # -- How often the secret is refreshed from the backend + refreshInterval: "1h" + # -- Mapping of env var name → backend secret key path + secrets: + # AWS Secrets Manager path → env var name in the pod + - secretKey: ADMIN_TOKEN + remoteRef: + key: "workload-governor/admin-token" + - secretKey: STELLAR_SECRET_KEY + remoteRef: + key: "workload-governor/stellar-secret-key" + - secretKey: DATABASE_URL + remoteRef: + key: "workload-governor/database-url" + - secretKey: REDIS_URL + remoteRef: + key: "workload-governor/redis-url" + +# -- Node selector labels +nodeSelector: {} + +# -- Tolerations for pod scheduling +tolerations: [] + +# -- Affinity rules +affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: In + values: + - vesting-backend + topologyKey: kubernetes.io/hostname + +# -- Extra environment variables injected directly (non-secret) +extraEnv: [] +# - name: MY_VAR +# value: "my-value" + +# -- Additional volumes to mount +extraVolumes: [] +# - name: tmp +# emptyDir: {} + +# -- Additional volume mounts +extraVolumeMounts: [] +# - name: tmp +# mountPath: /tmp