diff --git a/charts/iron-control/.helmignore b/charts/iron-control/.helmignore new file mode 100644 index 0000000..c333d82 --- /dev/null +++ b/charts/iron-control/.helmignore @@ -0,0 +1,11 @@ +# Patterns to ignore when building packages. +.DS_Store +.git/ +.gitignore +.vscode/ +.idea/ +*.swp +*.bak +*.tmp +*.orig +*~ diff --git a/charts/iron-control/Chart.yaml b/charts/iron-control/Chart.yaml new file mode 100644 index 0000000..f1eab89 --- /dev/null +++ b/charts/iron-control/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: iron-control +description: Iron Control web server and Solid Queue background job workers +type: application +version: 0.1.0 +appVersion: "latest" diff --git a/charts/iron-control/README.md b/charts/iron-control/README.md new file mode 100644 index 0000000..f94c5bc --- /dev/null +++ b/charts/iron-control/README.md @@ -0,0 +1,96 @@ +# Iron Control Helm Chart + +Deploys Iron Control on Kubernetes with two workloads: + +- **web**: the Puma web server (fronted by Thruster), with a Service, optional Ingress, and `/up` health probes. +- **jobs**: the Solid Queue supervisor (`bin/jobs`), which runs background workers and the recurring-job scheduler from `config/recurring.yml`. + +Database migrations run as a Helm pre-install/pre-upgrade hook Job (`rails db:prepare`). Web pods bypass the image entrypoint's migration step, so rollouts do not race migrations. + +The app uses Solid Queue, Solid Cache, and Solid Cable, so the only external dependency is PostgreSQL. No Redis is required. + +## Prerequisites + +- An external PostgreSQL server reachable from the cluster, with four databases owned by the `iron_control` role: `iron_control_production`, `iron_control_production_cache`, `iron_control_production_queue`, `iron_control_production_cable`. The migration Job creates them if the role has `CREATEDB`. +- The Iron Control container image in a registry the cluster can pull from. + +## Installing + +```sh +helm install iron-control charts/iron-control \ + --set image.repository=ghcr.io/ironsh/iron-control \ + --set image.tag=v1.2.3 \ + --set database.host=postgres.example.internal \ + --set secrets.existingSecret=iron-control-secrets +``` + +### Secrets + +The app requires five secret environment variables. Provide them either via a pre-created Secret (recommended) or inline values. + +With `secrets.existingSecret`, the named Secret must contain these keys: + +| Key | Purpose | +|-----|---------| +| `RAILS_MASTER_KEY` | Rails credentials encryption | +| `IRON_CONTROL_DATABASE_PASSWORD` | Password for the `iron_control` Postgres role | +| `IRON_CONTROL_AR_ENCRYPTION_PRIMARY_KEY` | ActiveRecord encryption | +| `IRON_CONTROL_AR_ENCRYPTION_DETERMINISTIC_KEY` | ActiveRecord encryption | +| `IRON_CONTROL_AR_ENCRYPTION_KEY_DERIVATION_SALT` | ActiveRecord encryption | + +Optional bootstrap keys (`IRON_CONTROL_INITIAL_USER_EMAIL`, `IRON_CONTROL_INITIAL_USER_PASSWORD`, `IRON_CONTROL_INITIAL_API_KEY`) can be added to the same Secret to create the first user on boot. + +Without `existingSecret`, set all five `secrets.values.*` entries and the chart renders its own Secret. That Secret is hook-annotated so the migration Job can read it on first install. Two caveats: `helm uninstall` does not delete hook resources, and the secret values live in Helm release history. Prefer `existingSecret` in production (it also works with external-secrets operators). + +## Values + +| Value | Default | Description | +|-------|---------|-------------| +| `image.repository` | `""` (required) | Container image repository | +| `image.tag` | chart `appVersion` | Image tag | +| `image.pullPolicy` | `IfNotPresent` | Pull policy | +| `imagePullSecrets` | `[]` | Pull secrets for private registries | +| `database.host` | `""` (required) | Postgres hostname (`IRON_CONTROL_DB_HOST`) | +| `database.port` | `5432` | Postgres port (`IRON_CONTROL_DB_PORT`) | +| `secrets.existingSecret` | `""` | Name of a pre-created Secret (see above) | +| `secrets.values.*` | `""` | Inline secret values, used when `existingSecret` is unset | +| `bootstrap.*` | `""` | Optional first-boot user/API key (chart-managed Secret only) | +| `config.logLevel` | `info` | `RAILS_LOG_LEVEL` | +| `config.webConcurrency` | `1` | Puma worker processes (`WEB_CONCURRENCY`) | +| `config.railsMaxThreads` | `3` | Puma threads / AR pool (`RAILS_MAX_THREADS`) | +| `config.jobConcurrency` | `1` | Solid Queue worker processes (`IRON_CONTROL_JOB_CONCURRENCY`) | +| `extraEnv` / `extraEnvFrom` | `[]` | Extra env / envFrom for all workloads | +| `web.replicas` | `2` | Web pod count | +| `web.containerPort` | `8080` | Thruster listen port (`HTTP_PORT`); kept above 1024 because the image runs as a non-root user | +| `web.resources` | requests 250m/512Mi, limit 1Gi | Web resources | +| `web.{startup,readiness,liveness}Probe` | enabled | `/up` probes | +| `web.extraEnv` | `[]` | Extra env for web pods | +| `jobs.replicas` | `1` | Jobs pod count (Solid Queue locks; >1 is safe but rarely needed) | +| `jobs.resources` | requests 250m/512Mi, limit 1Gi | Jobs resources | +| `jobs.extraEnv` | `[]` | Extra env for jobs pods | +| `migrations.enabled` | `true` | Run `rails db:prepare` as a hook Job | +| `migrations.backoffLimit` | `1` | Job retries | +| `migrations.activeDeadlineSeconds` | `600` | Job timeout | +| `service.type` / `service.port` | `ClusterIP` / `80` | Web Service | +| `ingress.*` | disabled | Standard Ingress options | +| `serviceAccount.*` | created | ServiceAccount for web and jobs pods | +| `podSecurityContext` / `containerSecurityContext` | non-root, no caps | Security defaults | +| `web.podAnnotations`, `nodeSelector`, `tolerations`, `affinity` (also under `jobs`) | empty | Scheduling knobs | + +## Design Notes + +- **Migrations**: the image entrypoint runs `db:prepare` when started with the default server command. The web Deployment overrides `command` to skip that, so migrations only run in the hook Job. The migration Job runs before the release's regular resources exist, so it uses the namespace default ServiceAccount. +- **Jobs rollout strategy** is `Recreate` to avoid doubled recurring-job scheduler capacity during a rollout. Solid Queue's locking makes overlap safe, so this is for predictability, not correctness. +- **Do not set `IRON_CONTROL_SOLID_QUEUE_IN_PUMA`** via `extraEnv`. It would run the job supervisor inside every web pod in addition to the jobs Deployment. +- **No probes on jobs pods**: the Solid Queue supervisor restarts crashed workers, and if the supervisor itself exits the container dies and Kubernetes restarts it. Add an exec probe via your own tooling if needed. +- **Not included** (bring your own if needed): HorizontalPodAutoscaler (target the web Deployment; autoscaling jobs is rarely useful since the scheduler runs there), PodDisruptionBudget, NetworkPolicy, persistent storage (Active Storage local-disk uploads are not supported by this chart). + +## Verifying a Render + +```sh +helm lint charts/iron-control +helm template iron-control charts/iron-control \ + --set image.repository=ghcr.io/ironsh/iron-control \ + --set database.host=pg \ + --set secrets.existingSecret=iron-control-secrets +``` diff --git a/charts/iron-control/templates/NOTES.txt b/charts/iron-control/templates/NOTES.txt new file mode 100644 index 0000000..3e592e0 --- /dev/null +++ b/charts/iron-control/templates/NOTES.txt @@ -0,0 +1,34 @@ +Iron Control has been deployed. + +{{- if .Values.migrations.enabled }} +Database migrations ran as a pre-install/pre-upgrade hook Job +({{ include "iron-control.fullname" . }}-migrate). +{{- end }} + +{{- if .Values.ingress.enabled }} + +The web UI is exposed via Ingress at: +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }} +{{- end }} +{{- else }} + +To reach the web UI locally: + + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "iron-control.fullname" . }}-web 8080:{{ .Values.service.port }} + +then open http://localhost:8080 +{{- end }} + +{{- if .Values.secrets.existingSecret }} + +Secrets are read from the existing Secret "{{ .Values.secrets.existingSecret }}". +Make sure it contains: RAILS_MASTER_KEY, IRON_CONTROL_DATABASE_PASSWORD, +IRON_CONTROL_AR_ENCRYPTION_PRIMARY_KEY, IRON_CONTROL_AR_ENCRYPTION_DETERMINISTIC_KEY, +IRON_CONTROL_AR_ENCRYPTION_KEY_DERIVATION_SALT. +{{- else }} + +Secrets are managed by the chart in the hook-annotated Secret +"{{ include "iron-control.fullname" . }}". Note: hook resources are not removed +by `helm uninstall`; delete the Secret manually if you tear down the release. +{{- end }} diff --git a/charts/iron-control/templates/_helpers.tpl b/charts/iron-control/templates/_helpers.tpl new file mode 100644 index 0000000..f1bb5e5 --- /dev/null +++ b/charts/iron-control/templates/_helpers.tpl @@ -0,0 +1,74 @@ +{{- define "iron-control.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "iron-control.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 "iron-control.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "iron-control.labels" -}} +helm.sh/chart: {{ include "iron-control.chart" . }} +{{ include "iron-control.selectorLabels" . }} +{{- with .Chart.AppVersion }} +app.kubernetes.io/version: {{ . | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "iron-control.selectorLabels" -}} +app.kubernetes.io/name: {{ include "iron-control.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "iron-control.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "iron-control.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "iron-control.secretName" -}} +{{- default (include "iron-control.fullname" .) .Values.secrets.existingSecret }} +{{- end }} + +{{- define "iron-control.image" -}} +{{- printf "%s:%s" (required "image.repository is required" .Values.image.repository) (default .Chart.AppVersion .Values.image.tag) }} +{{- end }} + +{{/* Env entries shared by the web, jobs, and migration workloads. */}} +{{- define "iron-control.commonEnv" -}} +- name: IRON_CONTROL_DB_HOST + value: {{ required "database.host is required" .Values.database.host | quote }} +- name: IRON_CONTROL_DB_PORT + value: {{ .Values.database.port | quote }} +- name: RAILS_LOG_LEVEL + value: {{ .Values.config.logLevel | quote }} +- name: RAILS_MAX_THREADS + value: {{ .Values.config.railsMaxThreads | quote }} +{{- with .Values.extraEnv }} +{{ toYaml . }} +{{- end }} +{{- end }} + +{{/* envFrom entries shared by the web, jobs, and migration workloads. */}} +{{- define "iron-control.commonEnvFrom" -}} +- secretRef: + name: {{ include "iron-control.secretName" . }} +{{- with .Values.extraEnvFrom }} +{{ toYaml . }} +{{- end }} +{{- end }} diff --git a/charts/iron-control/templates/ingress.yaml b/charts/iron-control/templates/ingress.yaml new file mode 100644 index 0000000..7717d18 --- /dev/null +++ b/charts/iron-control/templates/ingress.yaml @@ -0,0 +1,35 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "iron-control.fullname" . }} + labels: + {{- include "iron-control.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 "iron-control.fullname" $ }}-web + port: + name: http + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/iron-control/templates/jobs-deployment.yaml b/charts/iron-control/templates/jobs-deployment.yaml new file mode 100644 index 0000000..3177203 --- /dev/null +++ b/charts/iron-control/templates/jobs-deployment.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "iron-control.fullname" . }}-jobs + labels: + {{- include "iron-control.labels" . | nindent 4 }} + app.kubernetes.io/component: jobs +spec: + replicas: {{ .Values.jobs.replicas }} + # Recreate avoids doubled recurring-job scheduler capacity mid-rollout. + strategy: + type: Recreate + selector: + matchLabels: + {{- include "iron-control.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: jobs + template: + metadata: + annotations: + {{- if not .Values.secrets.existingSecret }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} + {{- with .Values.jobs.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "iron-control.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: jobs + {{- with .Values.jobs.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "iron-control.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: jobs + image: {{ include "iron-control.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["./bin/jobs"] + securityContext: + {{- toYaml .Values.containerSecurityContext | nindent 12 }} + env: + - name: IRON_CONTROL_JOB_CONCURRENCY + value: {{ .Values.config.jobConcurrency | quote }} + {{- include "iron-control.commonEnv" . | nindent 12 }} + {{- with .Values.jobs.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + {{- include "iron-control.commonEnvFrom" . | nindent 12 }} + resources: + {{- toYaml .Values.jobs.resources | nindent 12 }} + {{- with .Values.jobs.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.jobs.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.jobs.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/iron-control/templates/migrate-job.yaml b/charts/iron-control/templates/migrate-job.yaml new file mode 100644 index 0000000..a952655 --- /dev/null +++ b/charts/iron-control/templates/migrate-job.yaml @@ -0,0 +1,51 @@ +{{- if .Values.migrations.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "iron-control.fullname" . }}-migrate + labels: + {{- include "iron-control.labels" . | nindent 4 }} + app.kubernetes.io/component: migrations + annotations: + helm.sh/hook: pre-install,pre-upgrade + helm.sh/hook-weight: "0" + # Failed Jobs are kept for debugging and replaced on the next attempt. + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + backoffLimit: {{ .Values.migrations.backoffLimit }} + activeDeadlineSeconds: {{ .Values.migrations.activeDeadlineSeconds }} + template: + metadata: + {{- with .Values.migrations.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "iron-control.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: migrations + spec: + restartPolicy: Never + # No serviceAccountName: this hook runs before the release's regular + # resources (including the chart ServiceAccount) exist. + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: migrate + image: {{ include "iron-control.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: ["./bin/rails", "db:prepare"] + securityContext: + {{- toYaml .Values.containerSecurityContext | nindent 12 }} + env: + {{- include "iron-control.commonEnv" . | nindent 12 }} + envFrom: + {{- include "iron-control.commonEnvFrom" . | nindent 12 }} + {{- with .Values.migrations.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} +{{- end }} diff --git a/charts/iron-control/templates/secret.yaml b/charts/iron-control/templates/secret.yaml new file mode 100644 index 0000000..4b71d49 --- /dev/null +++ b/charts/iron-control/templates/secret.yaml @@ -0,0 +1,30 @@ +{{- if not .Values.secrets.existingSecret }} +{{- $v := .Values.secrets.values }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "iron-control.fullname" . }} + labels: + {{- include "iron-control.labels" . | nindent 4 }} + annotations: + # Hook-annotated so it exists before the pre-install migration Job runs. + helm.sh/hook: pre-install,pre-upgrade + helm.sh/hook-weight: "-10" + helm.sh/hook-delete-policy: before-hook-creation +type: Opaque +stringData: + RAILS_MASTER_KEY: {{ required "secrets.values.railsMasterKey is required when secrets.existingSecret is unset" $v.railsMasterKey | quote }} + IRON_CONTROL_DATABASE_PASSWORD: {{ required "secrets.values.databasePassword is required when secrets.existingSecret is unset" $v.databasePassword | quote }} + IRON_CONTROL_AR_ENCRYPTION_PRIMARY_KEY: {{ required "secrets.values.arEncryptionPrimaryKey is required when secrets.existingSecret is unset" $v.arEncryptionPrimaryKey | quote }} + IRON_CONTROL_AR_ENCRYPTION_DETERMINISTIC_KEY: {{ required "secrets.values.arEncryptionDeterministicKey is required when secrets.existingSecret is unset" $v.arEncryptionDeterministicKey | quote }} + IRON_CONTROL_AR_ENCRYPTION_KEY_DERIVATION_SALT: {{ required "secrets.values.arEncryptionKeyDerivationSalt is required when secrets.existingSecret is unset" $v.arEncryptionKeyDerivationSalt | quote }} + {{- with .Values.bootstrap.initialUserEmail }} + IRON_CONTROL_INITIAL_USER_EMAIL: {{ . | quote }} + {{- end }} + {{- with .Values.bootstrap.initialUserPassword }} + IRON_CONTROL_INITIAL_USER_PASSWORD: {{ . | quote }} + {{- end }} + {{- with .Values.bootstrap.initialApiKey }} + IRON_CONTROL_INITIAL_API_KEY: {{ . | quote }} + {{- end }} +{{- end }} diff --git a/charts/iron-control/templates/service.yaml b/charts/iron-control/templates/service.yaml new file mode 100644 index 0000000..f8c3ef9 --- /dev/null +++ b/charts/iron-control/templates/service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "iron-control.fullname" . }}-web + labels: + {{- include "iron-control.labels" . | nindent 4 }} + app.kubernetes.io/component: web + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + selector: + {{- include "iron-control.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: web diff --git a/charts/iron-control/templates/serviceaccount.yaml b/charts/iron-control/templates/serviceaccount.yaml new file mode 100644 index 0000000..c4c2944 --- /dev/null +++ b/charts/iron-control/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "iron-control.serviceAccountName" . }} + labels: + {{- include "iron-control.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/iron-control/templates/web-deployment.yaml b/charts/iron-control/templates/web-deployment.yaml new file mode 100644 index 0000000..664ced8 --- /dev/null +++ b/charts/iron-control/templates/web-deployment.yaml @@ -0,0 +1,96 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "iron-control.fullname" . }}-web + labels: + {{- include "iron-control.labels" . | nindent 4 }} + app.kubernetes.io/component: web +spec: + replicas: {{ .Values.web.replicas }} + selector: + matchLabels: + {{- include "iron-control.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: web + template: + metadata: + annotations: + {{- if not .Values.secrets.existingSecret }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} + {{- with .Values.web.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "iron-control.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: web + {{- with .Values.web.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "iron-control.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: web + image: {{ include "iron-control.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + # Replaces the image entrypoint so web pods skip its db:prepare; + # migrations run in the pre-upgrade hook Job instead. + command: ["./bin/thrust", "./bin/rails", "server"] + securityContext: + {{- toYaml .Values.containerSecurityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.web.containerPort }} + protocol: TCP + env: + - name: HTTP_PORT + value: {{ .Values.web.containerPort | quote }} + - name: WEB_CONCURRENCY + value: {{ .Values.config.webConcurrency | quote }} + {{- include "iron-control.commonEnv" . | nindent 12 }} + {{- with .Values.web.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + {{- include "iron-control.commonEnvFrom" . | nindent 12 }} + {{- if .Values.web.startupProbe.enabled }} + startupProbe: + httpGet: + path: /up + port: http + failureThreshold: {{ .Values.web.startupProbe.failureThreshold }} + periodSeconds: {{ .Values.web.startupProbe.periodSeconds }} + {{- end }} + {{- if .Values.web.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: /up + port: http + periodSeconds: {{ .Values.web.readinessProbe.periodSeconds }} + {{- end }} + {{- if .Values.web.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: /up + port: http + periodSeconds: {{ .Values.web.livenessProbe.periodSeconds }} + {{- end }} + resources: + {{- toYaml .Values.web.resources | nindent 12 }} + {{- with .Values.web.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.web.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.web.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/iron-control/values.yaml b/charts/iron-control/values.yaml new file mode 100644 index 0000000..3748bba --- /dev/null +++ b/charts/iron-control/values.yaml @@ -0,0 +1,152 @@ +image: + # Required. e.g. ghcr.io/ironsh/iron-control + repository: "" + # Defaults to the chart's appVersion. + tag: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + annotations: {} + # Defaults to the release fullname when create is true, otherwise "default". + name: "" + +database: + # Required. Hostname of the external Postgres server. The app expects four + # databases on it (iron_control_production, _cache, _queue, _cable) owned by + # the "iron_control" role; the password comes from the secret below. + host: "" + port: 5432 + +secrets: + # Name of a pre-created Secret containing the keys below. Recommended for + # production (plays well with external-secrets). When set, the chart does + # not create its own Secret and secrets.values is ignored. + # + # Required keys: RAILS_MASTER_KEY, IRON_CONTROL_DATABASE_PASSWORD, + # IRON_CONTROL_AR_ENCRYPTION_PRIMARY_KEY, + # IRON_CONTROL_AR_ENCRYPTION_DETERMINISTIC_KEY, + # IRON_CONTROL_AR_ENCRYPTION_KEY_DERIVATION_SALT + existingSecret: "" + # Used only when existingSecret is empty; the chart renders these into a + # Secret of its own. All five are required in that case. + values: + railsMasterKey: "" + databasePassword: "" + arEncryptionPrimaryKey: "" + arEncryptionDeterministicKey: "" + arEncryptionKeyDerivationSalt: "" + +# Optional first-boot bootstrap. Only applied when the chart manages the +# Secret (secrets.existingSecret unset); with an existing Secret, add the +# IRON_CONTROL_INITIAL_* keys there instead. +bootstrap: + initialUserEmail: "" + initialUserPassword: "" + initialApiKey: "" + +config: + # RAILS_LOG_LEVEL + logLevel: info + # WEB_CONCURRENCY: Puma worker processes (web pods only). + webConcurrency: 1 + # RAILS_MAX_THREADS: Puma threads and ActiveRecord pool size. + railsMaxThreads: 3 + # IRON_CONTROL_JOB_CONCURRENCY: Solid Queue worker processes (jobs pods only). + jobConcurrency: 1 + +# Extra env entries / envFrom sources appended to all workloads (web, jobs, +# migrations). Do NOT set IRON_CONTROL_SOLID_QUEUE_IN_PUMA here; this chart +# runs jobs in a dedicated Deployment. +extraEnv: [] +extraEnvFrom: [] + +web: + replicas: 2 + # Port Thruster listens on (HTTP_PORT). Kept above 1024 because the image + # runs as a non-root user. + containerPort: 8080 + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + memory: 1Gi + podAnnotations: {} + podLabels: {} + nodeSelector: {} + tolerations: [] + affinity: {} + startupProbe: + enabled: true + failureThreshold: 30 + periodSeconds: 2 + readinessProbe: + enabled: true + periodSeconds: 10 + livenessProbe: + enabled: true + periodSeconds: 15 + # Extra env entries for web pods only. + extraEnv: [] + +jobs: + # Solid Queue handles its own locking, so >1 is safe but rarely needed. + # Recurring jobs (config/recurring.yml) are scheduled by the supervisor. + replicas: 1 + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + memory: 1Gi + podAnnotations: {} + podLabels: {} + nodeSelector: {} + tolerations: [] + affinity: {} + # Extra env entries for jobs pods only. + extraEnv: [] + +migrations: + # Run `rails db:prepare` as a Helm pre-install/pre-upgrade hook Job. + enabled: true + backoffLimit: 1 + activeDeadlineSeconds: 600 + resources: {} + podAnnotations: {} + +service: + type: ClusterIP + port: 80 + annotations: {} + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: iron-control.example.com + paths: + - path: / + pathType: Prefix + tls: [] + +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL