diff --git a/helm/taskchampion-sync-server/.helmignore b/helm/taskchampion-sync-server/.helmignore new file mode 100644 index 0000000..6c3a50f --- /dev/null +++ b/helm/taskchampion-sync-server/.helmignore @@ -0,0 +1,5 @@ +# Patterns to ignore when building the Helm chart +.git +.gitignore +*.md +examples/ diff --git a/helm/taskchampion-sync-server/Chart.yaml b/helm/taskchampion-sync-server/Chart.yaml new file mode 100644 index 0000000..1d431c1 --- /dev/null +++ b/helm/taskchampion-sync-server/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: taskchampion-sync-server +description: A Helm chart for deploying TaskChampion Sync Server on Kubernetes +type: application +version: 0.1.0 +appVersion: "0.7.0" +keywords: + - taskchampion + - taskwarrior + - sync + - task-management +home: https://github.com/GothenburgBitFactory/taskchampion-sync-server +sources: + - https://github.com/GothenburgBitFactory/taskchampion-sync-server +maintainers: [] diff --git a/helm/taskchampion-sync-server/README.md b/helm/taskchampion-sync-server/README.md new file mode 100644 index 0000000..5631a76 --- /dev/null +++ b/helm/taskchampion-sync-server/README.md @@ -0,0 +1,61 @@ +# TaskChampion Sync Server Helm Chart + +Deploy the [TaskChampion Sync Server](https://github.com/GothenburgBitFactory/taskchampion-sync-server) on Kubernetes. + +## Prerequisites + +- Kubernetes 1.23+ +- Helm 3+ + +## Storage Backends + +Exactly one storage backend must be enabled. The chart will fail validation if both or neither are enabled. + +### SQLite + +```console +helm install my-release ./helm/taskchampion-sync-server -f helm/taskchampion-sync-server/examples/sqlite-values.yaml +``` + +### PostgreSQL + +```console +helm install my-release ./helm/taskchampion-sync-server -f helm/taskchampion-sync-server/examples/postgres-values.yaml +``` + +## Secrets + +The chart expects pre-created secrets referenced by name: + +### Client ID Secret + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-client-ids +type: Opaque +data: + client-ids: +``` + +Reference it via `clientIdSecret: "my-client-ids"`. + +### PostgreSQL Secret + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-postgres-creds +type: Opaque +data: + connection: + password: +``` + +Reference it via `postgres.existingSecret: "my-postgres-creds"`. + +## Configuration + +See [values.yaml](values.yaml) for all configurable options. diff --git a/helm/taskchampion-sync-server/examples/postgres-values.yaml b/helm/taskchampion-sync-server/examples/postgres-values.yaml new file mode 100644 index 0000000..6741d71 --- /dev/null +++ b/helm/taskchampion-sync-server/examples/postgres-values.yaml @@ -0,0 +1,31 @@ +sqlite: + enabled: false + +postgres: + enabled: true + existingSecret: "taskchampion-postgres-creds" + database: taskchampion + host: postgres-primary + username: taskchampion-user + +clientIdSecret: "taskchampion-client-ids" + +env: + RUST_LOG: debug + LISTEN: "0.0.0.0:8080" + CREATE_CLIENTS: "false" + +replicas: + enabled: true + count: 3 + +image: + pullSecrets: + - my-registry-secret + +httpRoute: + enabled: true + gateway: "my-gateway" + host: "taskchampion.example.com" + path: "/" + port: 8080 diff --git a/helm/taskchampion-sync-server/examples/sqlite-values.yaml b/helm/taskchampion-sync-server/examples/sqlite-values.yaml new file mode 100644 index 0000000..9159979 --- /dev/null +++ b/helm/taskchampion-sync-server/examples/sqlite-values.yaml @@ -0,0 +1,22 @@ +sqlite: + enabled: true + existingPV: "" + emptyDir: + sizeLimit: 1Gi + persistence: + enabled: false + +postgres: + enabled: false + +clientIdSecret: "taskchampion-client-ids" + +env: + RUST_LOG: info + LISTEN: "0.0.0.0:8080" + CREATE_CLIENTS: "false" + +ingress: + enabled: true + hosts: + - taskchampion.example.com diff --git a/helm/taskchampion-sync-server/templates/_helpers.tpl b/helm/taskchampion-sync-server/templates/_helpers.tpl new file mode 100644 index 0000000..be73dc9 --- /dev/null +++ b/helm/taskchampion-sync-server/templates/_helpers.tpl @@ -0,0 +1,41 @@ +{{- /* +taskchampion-sync-server helpers +*/ -}} + +{{- define "taskchampion-sync-server.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "taskchampion-sync-server.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 "taskchampion-sync-server.labels" -}} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +app.kubernetes.io/name: {{ include "taskchampion-sync-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "taskchampion-sync-server.selectorLabels" -}} +app.kubernetes.io/name: {{ include "taskchampion-sync-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "taskchampion-sync-server.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "taskchampion-sync-server.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/taskchampion-sync-server/templates/deployment.yaml b/helm/taskchampion-sync-server/templates/deployment.yaml new file mode 100644 index 0000000..aef6395 --- /dev/null +++ b/helm/taskchampion-sync-server/templates/deployment.yaml @@ -0,0 +1,100 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "taskchampion-sync-server.fullname" . }} + labels: + {{- include "taskchampion-sync-server.labels" . | nindent 4 }} +spec: + {{- if and (eq .Values.postgres.enabled true) .Values.replicas.enabled }} + replicas: {{ .Values.replicas.count }} + {{- else }} + replicas: 1 + {{- end }} + selector: + matchLabels: + {{- include "taskchampion-sync-server.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "taskchampion-sync-server.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- range . }} + - name: {{ . }} + {{- end }} + {{- end }} + securityContext: + {{- toYaml .Values.securityContext | nindent 8 }} + containers: + - name: taskchampion-sync-server + {{- if eq .Values.postgres.enabled true }} + image: "{{ .Values.image.repository }}-postgres:{{ .Values.image.tag }}" + {{- else }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- range $name, $value := .Values.env }} + - name: {{ $name }} + value: {{ $value | quote }} + {{- end }} + {{- if .Values.clientIdSecret }} + - name: CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ .Values.clientIdSecret }} + key: client-ids + {{- end }} + {{- if eq .Values.sqlite.enabled true }} + - name: DATA_DIR + value: {{ .Values.sqlite.dataDir }} + {{- end }} + {{- if eq .Values.postgres.enabled true }} + - name: CONNECTION + valueFrom: + secretKeyRef: + name: {{ required "postgres.existingSecret is required when postgres is enabled" .Values.postgres.existingSecret }} + key: connection + optional: true + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.postgres.existingSecret }} + key: password + optional: true + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if eq .Values.sqlite.enabled true }} + volumeMounts: + - name: data + mountPath: {{ .Values.sqlite.dataDir }} + {{- end }} + {{- if eq .Values.sqlite.enabled true }} + volumes: + {{- if .Values.sqlite.existingPV }} + - name: data + persistentVolumeClaim: + claimName: {{ .Values.sqlite.existingPV }} + {{- else if .Values.sqlite.persistence.enabled }} + - name: data + persistentVolumeClaim: + claimName: {{ include "taskchampion-sync-server.fullname" . }}-pvc + {{- else }} + - name: data + emptyDir: + {{- if .Values.sqlite.emptyDir.sizeLimit }} + sizeLimit: {{ .Values.sqlite.emptyDir.sizeLimit }} + {{- end }} + {{- if .Values.sqlite.emptyDir.medium }} + medium: {{ .Values.sqlite.emptyDir.medium }} + {{- end }} + {{- end }} + {{- end }} diff --git a/helm/taskchampion-sync-server/templates/httproute.yaml b/helm/taskchampion-sync-server/templates/httproute.yaml new file mode 100644 index 0000000..8ca5245 --- /dev/null +++ b/helm/taskchampion-sync-server/templates/httproute.yaml @@ -0,0 +1,23 @@ +{{- if .Values.httpRoute.enabled }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "taskchampion-sync-server.fullname" . }} + labels: + {{- include "taskchampion-sync-server.labels" . | nindent 4 }} +spec: + parentRefs: + - name: {{ .Values.httpRoute.gateway }} + {{- if .Values.httpRoute.host }} + hostnames: + - {{ .Values.httpRoute.host }} + {{- end }} + rules: + - matches: + - path: + type: PathPrefix + value: {{ .Values.httpRoute.path }} + backendRefs: + - name: {{ include "taskchampion-sync-server.fullname" . }} + port: {{ .Values.httpRoute.port }} +{{- end }} diff --git a/helm/taskchampion-sync-server/templates/ingress.yaml b/helm/taskchampion-sync-server/templates/ingress.yaml new file mode 100644 index 0000000..8565711 --- /dev/null +++ b/helm/taskchampion-sync-server/templates/ingress.yaml @@ -0,0 +1,33 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "taskchampion-sync-server.fullname" . }} + labels: + {{- include "taskchampion-sync-server.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: + {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "taskchampion-sync-server.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} +{{- end }} diff --git a/helm/taskchampion-sync-server/templates/pvc.yaml b/helm/taskchampion-sync-server/templates/pvc.yaml new file mode 100644 index 0000000..42b701a --- /dev/null +++ b/helm/taskchampion-sync-server/templates/pvc.yaml @@ -0,0 +1,17 @@ +{{- if and (eq .Values.sqlite.enabled true) .Values.sqlite.persistence.enabled (not .Values.sqlite.existingPV) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "taskchampion-sync-server.fullname" . }}-pvc + labels: + {{- include "taskchampion-sync-server.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.sqlite.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.sqlite.persistence.size }} + {{- if .Values.sqlite.persistence.storageClass }} + storageClassName: {{ .Values.sqlite.persistence.storageClass }} + {{- end }} +{{- end }} diff --git a/helm/taskchampion-sync-server/templates/service.yaml b/helm/taskchampion-sync-server/templates/service.yaml new file mode 100644 index 0000000..9083f04 --- /dev/null +++ b/helm/taskchampion-sync-server/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "taskchampion-sync-server.fullname" . }} + labels: + {{- include "taskchampion-sync-server.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http + selector: + {{- include "taskchampion-sync-server.selectorLabels" . | nindent 4 }} diff --git a/helm/taskchampion-sync-server/templates/validation-config.yaml b/helm/taskchampion-sync-server/templates/validation-config.yaml new file mode 100644 index 0000000..9add1b5 --- /dev/null +++ b/helm/taskchampion-sync-server/templates/validation-config.yaml @@ -0,0 +1,7 @@ +{{- if and (eq .Values.sqlite.enabled false) (eq .Values.postgres.enabled false) -}} +{{- fail "ERROR: Exactly one storage backend must be enabled. Either sqlite.enabled or postgres.enabled must be true." -}} +{{- end -}} + +{{- if and (eq .Values.sqlite.enabled true) (eq .Values.postgres.enabled true) -}} +{{- fail "ERROR: Only one storage backend can be enabled. Both sqlite.enabled and postgres.enabled cannot be true at the same time." -}} +{{- end -}} diff --git a/helm/taskchampion-sync-server/values.yaml b/helm/taskchampion-sync-server/values.yaml new file mode 100644 index 0000000..46091d4 --- /dev/null +++ b/helm/taskchampion-sync-server/values.yaml @@ -0,0 +1,84 @@ +# Image configuration +image: + repository: ghcr.io/gothenburgbitfactory/taskchampion-sync-server + tag: "0.7.0" + pullPolicy: IfNotPresent + pullSecrets: [] + +# Existing secret containing client IDs (comma-separated UUIDs) +# Expected key: client-ids +clientIdSecret: "" + +# Environment variables passed directly to the container +# NOTE: DATA_DIR and CONNECTION are set automatically based on backend +env: + RUST_LOG: info + LISTEN: "0.0.0.0:8080" + CREATE_CLIENTS: "false" + +# Service configuration +service: + type: ClusterIP + port: 8080 + targetPort: 8080 + +# Ingress configuration +ingress: + enabled: false + className: "" + annotations: {} + hosts: [] + tls: [] + +# HTTPRoute configuration (Kubernetes Gateway API) +httpRoute: + enabled: false + gateway: "" + host: "" + path: "/" + port: 8080 + +# Resource limits and requests +resources: {} + # limits: + # memory: 128Mi + # cpu: 100m + # requests: + # memory: 64Mi + # cpu: 50m + +# Replica configuration (only applies when postgres is enabled) +replicas: + enabled: false + count: 1 + +# Security context for the pod +securityContext: + runAsUser: 1092 + runAsGroup: 100 + fsGroup: 100 + +# SQLite backend configuration (mutually exclusive with postgres) +sqlite: + enabled: false + dataDir: /var/lib/taskchampion-sync-server/data + existingPV: "" + emptyDir: + sizeLimit: "" + medium: "" + persistence: + enabled: true + size: 1Gi + accessMode: ReadWriteOnce + storageClass: "" + existingClaim: "" + +# PostgreSQL backend configuration (mutually exclusive with sqlite) +postgres: + enabled: false + existingSecret: "" + database: taskchampion + host: postgres + port: 5432 + username: user + password: ""