diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..4cfdff0 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,252 @@ +name: Publish Images And Chart + +on: + workflow_dispatch: + push: + branches: + - main + - master + tags: + - "v*" + +env: + REGISTRY_HOST: ${{ vars.GITEA_REGISTRY_HOST }} + REGISTRY_NAMESPACE: ${{ vars.GITEA_REGISTRY_NAMESPACE }} + CHART_NAME: attune + +jobs: + metadata: + name: Resolve Publish Metadata + runs-on: ubuntu-latest + outputs: + registry: ${{ steps.meta.outputs.registry }} + namespace: ${{ steps.meta.outputs.namespace }} + image_tag: ${{ steps.meta.outputs.image_tag }} + image_tags: ${{ steps.meta.outputs.image_tags }} + chart_version: ${{ steps.meta.outputs.chart_version }} + app_version: ${{ steps.meta.outputs.app_version }} + release_channel: ${{ steps.meta.outputs.release_channel }} + steps: + - name: Resolve tags and registry paths + id: meta + shell: bash + run: | + set -euo pipefail + + registry="${REGISTRY_HOST}" + namespace="${REGISTRY_NAMESPACE}" + + if [ -z "$registry" ]; then + echo "GITEA_REGISTRY_HOST repository variable is required" + exit 1 + fi + + if [ -z "$namespace" ]; then + namespace="${{ github.repository_owner }}" + fi + + short_sha="$(printf '%s' "${{ github.sha }}" | cut -c1-12)" + ref_type="${{ github.ref_type }}" + ref_name="${{ github.ref_name }}" + + if [ "$ref_type" = "tag" ] && printf '%s' "$ref_name" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-.].*)?$'; then + version="${ref_name#v}" + image_tags="${version},latest,sha-${short_sha}" + chart_version="$version" + release_channel="release" + else + version="sha-${short_sha}" + image_tags="edge,sha-${short_sha}" + chart_version="0.0.0-dev.${{ github.run_number }}" + release_channel="edge" + fi + + { + echo "registry=$registry" + echo "namespace=$namespace" + echo "image_tag=$version" + echo "image_tags=$image_tags" + echo "chart_version=$chart_version" + echo "app_version=$version" + echo "release_channel=$release_channel" + } >> "$GITHUB_OUTPUT" + + publish-images: + name: Publish ${{ matrix.image.name }} + runs-on: ubuntu-latest + needs: metadata + strategy: + fail-fast: false + matrix: + image: + - name: api + repository: attune-api + dockerfile: docker/Dockerfile.optimized + context: . + target: "" + build_args: | + SERVICE=api + - name: executor + repository: attune-executor + dockerfile: docker/Dockerfile.optimized + context: . + target: "" + build_args: | + SERVICE=executor + - name: notifier + repository: attune-notifier + dockerfile: docker/Dockerfile.optimized + context: . + target: "" + build_args: | + SERVICE=notifier + - name: sensor + repository: attune-sensor + dockerfile: docker/Dockerfile.sensor.optimized + context: . + target: sensor-full + build_args: "" + - name: worker + repository: attune-worker + dockerfile: docker/Dockerfile.worker.optimized + context: . + target: worker-full + build_args: "" + - name: web + repository: attune-web + dockerfile: docker/Dockerfile.web + context: . + target: "" + build_args: "" + - name: migrations + repository: attune-migrations + dockerfile: docker/Dockerfile.migrations + context: . + target: "" + build_args: "" + - name: init-user + repository: attune-init-user + dockerfile: docker/Dockerfile.init-user + context: . + target: "" + build_args: "" + - name: init-packs + repository: attune-init-packs + dockerfile: docker/Dockerfile.init-packs + context: . + target: "" + build_args: "" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea OCI registry + shell: bash + env: + REGISTRY_USERNAME: ${{ secrets.GITEA_REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.GITEA_REGISTRY_PASSWORD }} + GITHUB_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + username="${REGISTRY_USERNAME:-${{ github.actor }}}" + password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}" + + if [ -z "$password" ]; then + echo "Set GITEA_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes" + exit 1 + fi + + printf '%s' "$password" | docker login "${{ needs.metadata.outputs.registry }}" \ + --username "$username" \ + --password-stdin + + - name: Prepare image tags + id: tags + shell: bash + run: | + set -euo pipefail + image_ref_base="${{ needs.metadata.outputs.registry }}/${{ needs.metadata.outputs.namespace }}/${{ matrix.image.repository }}" + tag_lines="" + IFS=',' read -ra tags <<< "${{ needs.metadata.outputs.image_tags }}" + for tag in "${tags[@]}"; do + tag_lines="${tag_lines}${image_ref_base}:${tag}"$'\n' + done + printf 'tags<> "$GITHUB_OUTPUT" + + - name: Build and push image + shell: bash + run: | + set -euo pipefail + build_cmd=( + docker buildx build + "${{ matrix.image.context }}" + --file "${{ matrix.image.dockerfile }}" + --push + ) + + if [ -n "${{ matrix.image.target }}" ]; then + build_cmd+=(--target "${{ matrix.image.target }}") + fi + + while IFS= read -r tag; do + [ -n "$tag" ] && build_cmd+=(--tag "$tag") + done <<< "${{ steps.tags.outputs.tags }}" + + while IFS= read -r build_arg; do + [ -n "$build_arg" ] && build_cmd+=(--build-arg "$build_arg") + done <<< "${{ matrix.image.build_args }}" + + "${build_cmd[@]}" + + publish-chart: + name: Publish Helm Chart + runs-on: ubuntu-latest + needs: + - metadata + - publish-images + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Helm + uses: azure/setup-helm@v4 + + - name: Log in to Gitea OCI registry + shell: bash + env: + REGISTRY_USERNAME: ${{ secrets.GITEA_REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.GITEA_REGISTRY_PASSWORD }} + GITHUB_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + registry_username="${REGISTRY_USERNAME:-${{ github.actor }}}" + registry_password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}" + + if [ -z "$registry_password" ]; then + echo "Set GITEA_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes" + exit 1 + fi + + printf '%s' "$registry_password" | helm registry login "${{ needs.metadata.outputs.registry }}" \ + --username "$registry_username" \ + --password-stdin + + - name: Lint chart + run: | + helm lint charts/attune + + - name: Package chart + run: | + mkdir -p dist + helm package charts/attune \ + --destination dist \ + --version "${{ needs.metadata.outputs.chart_version }}" \ + --app-version "${{ needs.metadata.outputs.app_version }}" + + - name: Push chart to OCI registry + run: | + helm push "dist/${CHART_NAME}-${{ needs.metadata.outputs.chart_version }}.tgz" \ + "oci://${{ needs.metadata.outputs.registry }}/${{ needs.metadata.outputs.namespace }}/helm" diff --git a/charts/attune/Chart.yaml b/charts/attune/Chart.yaml new file mode 100644 index 0000000..88d2911 --- /dev/null +++ b/charts/attune/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: attune +description: Helm chart for deploying the Attune automation platform +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/charts/attune/templates/NOTES.txt b/charts/attune/templates/NOTES.txt new file mode 100644 index 0000000..f10ad32 --- /dev/null +++ b/charts/attune/templates/NOTES.txt @@ -0,0 +1,3 @@ +1. Set `global.imageRegistry`, `global.imageNamespace`, and `global.imageTag` so the chart pulls the images published by the Gitea workflow. +2. Set `web.config.apiUrl` and `web.config.wsUrl` to browser-reachable endpoints before exposing the web UI. +3. The shared `packs`, `runtime_envs`, and `artifacts` PVCs default to `ReadWriteMany`; your cluster storage class must support RWX or you need to override those claims. diff --git a/charts/attune/templates/_helpers.tpl b/charts/attune/templates/_helpers.tpl new file mode 100644 index 0000000..127873a --- /dev/null +++ b/charts/attune/templates/_helpers.tpl @@ -0,0 +1,113 @@ +{{- define "attune.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "attune.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name (include "attune.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "attune.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" -}} +{{- end -}} + +{{- define "attune.labels" -}} +helm.sh/chart: {{ include "attune.chart" . }} +app.kubernetes.io/name: {{ include "attune.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "attune.selectorLabels" -}} +app.kubernetes.io/name: {{ include "attune.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "attune.componentLabels" -}} +{{ include "attune.selectorLabels" .root }} +app.kubernetes.io/component: {{ .component }} +{{- end -}} + +{{- define "attune.image" -}} +{{- $root := .root -}} +{{- $image := .image -}} +{{- $registry := $root.Values.global.imageRegistry -}} +{{- $namespace := $root.Values.global.imageNamespace -}} +{{- $repository := $image.repository -}} +{{- $tag := default $root.Values.global.imageTag $image.tag -}} +{{- if and $registry $namespace -}} +{{- printf "%s/%s/%s:%s" $registry $namespace $repository $tag -}} +{{- else if $registry -}} +{{- printf "%s/%s:%s" $registry $repository $tag -}} +{{- else -}} +{{- printf "%s:%s" $repository $tag -}} +{{- end -}} +{{- end -}} + +{{- define "attune.secretName" -}} +{{- if .Values.security.existingSecret -}} +{{- .Values.security.existingSecret -}} +{{- else -}} +{{- printf "%s-secrets" (include "attune.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{- define "attune.postgresqlServiceName" -}} +{{- if .Values.database.host -}} +{{- .Values.database.host -}} +{{- else -}} +{{- printf "%s-postgresql" (include "attune.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{- define "attune.rabbitmqServiceName" -}} +{{- if .Values.rabbitmq.host -}} +{{- .Values.rabbitmq.host -}} +{{- else -}} +{{- printf "%s-rabbitmq" (include "attune.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{- define "attune.redisServiceName" -}} +{{- if .Values.redis.host -}} +{{- .Values.redis.host -}} +{{- else -}} +{{- printf "%s-redis" (include "attune.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{- define "attune.databaseUrl" -}} +{{- if .Values.database.url -}} +{{- .Values.database.url -}} +{{- else -}} +{{- printf "postgresql://%s:%s@%s:%v/%s" .Values.database.username .Values.database.password (include "attune.postgresqlServiceName" .) .Values.database.port .Values.database.database -}} +{{- end -}} +{{- end -}} + +{{- define "attune.rabbitmqUrl" -}} +{{- if .Values.rabbitmq.url -}} +{{- .Values.rabbitmq.url -}} +{{- else -}} +{{- printf "amqp://%s:%s@%s:%v" .Values.rabbitmq.username .Values.rabbitmq.password (include "attune.rabbitmqServiceName" .) .Values.rabbitmq.port -}} +{{- end -}} +{{- end -}} + +{{- define "attune.redisUrl" -}} +{{- if .Values.redis.url -}} +{{- .Values.redis.url -}} +{{- else -}} +{{- printf "redis://%s:%v" (include "attune.redisServiceName" .) .Values.redis.port -}} +{{- end -}} +{{- end -}} + +{{- define "attune.apiServiceName" -}} +{{- printf "%s-api" (include "attune.fullname" .) -}} +{{- end -}} + +{{- define "attune.notifierServiceName" -}} +{{- printf "%s-notifier" (include "attune.fullname" .) -}} +{{- end -}} diff --git a/charts/attune/templates/applications.yaml b/charts/attune/templates/applications.yaml new file mode 100644 index 0000000..b527f97 --- /dev/null +++ b/charts/attune/templates/applications.yaml @@ -0,0 +1,523 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "attune.apiServiceName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + type: {{ .Values.api.service.type }} + selector: + {{- include "attune.componentLabels" (dict "root" . "component" "api") | nindent 4 }} + ports: + - name: http + port: {{ .Values.api.service.port }} + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "attune.apiServiceName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.api.replicaCount }} + selector: + matchLabels: + {{- include "attune.componentLabels" (dict "root" . "component" "api") | nindent 6 }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "api") | nindent 8 }} + spec: + {{- if .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.global.imagePullSecrets | nindent 8 }} + {{- end }} + initContainers: + - name: wait-for-schema + image: postgres:16-alpine + command: ["/bin/sh", "-ec"] + args: + - | + until PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -tAc "SELECT to_regclass('${DB_SCHEMA}.identity')" | grep -q identity; do + echo "waiting for schema"; + sleep 2; + done + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + - name: wait-for-packs + image: busybox:1.36 + command: ["/bin/sh", "-ec"] + args: + - | + until [ -f /opt/attune/packs/core/pack.yaml ]; do + echo "waiting for packs"; + sleep 2; + done + volumeMounts: + - name: packs + mountPath: /opt/attune/packs + containers: + - name: api + image: {{ include "attune.image" (dict "root" . "image" .Values.images.api) }} + imagePullPolicy: {{ .Values.images.api.pullPolicy }} + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + env: + - name: ATTUNE_CONFIG + value: /opt/attune/config.yaml + - name: ATTUNE__DATABASE__SCHEMA + value: {{ .Values.database.schema | quote }} + - name: ATTUNE__WORKER__WORKER_TYPE + value: container + ports: + - name: http + containerPort: 8080 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 20 + periodSeconds: 15 + resources: + {{- toYaml .Values.api.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /opt/attune/config.yaml + subPath: config.yaml + - name: packs + mountPath: /opt/attune/packs + - name: runtime-envs + mountPath: /opt/attune/runtime_envs + - name: artifacts + mountPath: /opt/attune/artifacts + volumes: + - name: config + configMap: + name: {{ include "attune.fullname" . }}-config + - name: packs + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-packs + - name: runtime-envs + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-runtime-envs + - name: artifacts + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-artifacts +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "attune.fullname" . }}-executor + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.executor.replicaCount }} + selector: + matchLabels: + {{- include "attune.componentLabels" (dict "root" . "component" "executor") | nindent 6 }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "executor") | nindent 8 }} + spec: + {{- if .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.global.imagePullSecrets | nindent 8 }} + {{- end }} + initContainers: + - name: wait-for-schema + image: postgres:16-alpine + command: ["/bin/sh", "-ec"] + args: + - | + until PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -tAc "SELECT to_regclass('${DB_SCHEMA}.identity')" | grep -q identity; do + echo "waiting for schema"; + sleep 2; + done + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + - name: wait-for-packs + image: busybox:1.36 + command: ["/bin/sh", "-ec"] + args: + - | + until [ -f /opt/attune/packs/core/pack.yaml ]; do + echo "waiting for packs"; + sleep 2; + done + volumeMounts: + - name: packs + mountPath: /opt/attune/packs + containers: + - name: executor + image: {{ include "attune.image" (dict "root" . "image" .Values.images.executor) }} + imagePullPolicy: {{ .Values.images.executor.pullPolicy }} + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + env: + - name: ATTUNE_CONFIG + value: /opt/attune/config.yaml + - name: ATTUNE__DATABASE__SCHEMA + value: {{ .Values.database.schema | quote }} + - name: ATTUNE__WORKER__WORKER_TYPE + value: container + resources: + {{- toYaml .Values.executor.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /opt/attune/config.yaml + subPath: config.yaml + - name: packs + mountPath: /opt/attune/packs + - name: artifacts + mountPath: /opt/attune/artifacts + volumes: + - name: config + configMap: + name: {{ include "attune.fullname" . }}-config + - name: packs + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-packs + - name: artifacts + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-artifacts +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "attune.fullname" . }}-worker + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.worker.replicaCount }} + selector: + matchLabels: + {{- include "attune.componentLabels" (dict "root" . "component" "worker") | nindent 6 }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "worker") | nindent 8 }} + spec: + {{- if .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.global.imagePullSecrets | nindent 8 }} + {{- end }} + initContainers: + - name: wait-for-schema + image: postgres:16-alpine + command: ["/bin/sh", "-ec"] + args: + - | + until PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -tAc "SELECT to_regclass('${DB_SCHEMA}.identity')" | grep -q identity; do + echo "waiting for schema"; + sleep 2; + done + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + - name: wait-for-packs + image: busybox:1.36 + command: ["/bin/sh", "-ec"] + args: + - | + until [ -f /opt/attune/packs/core/pack.yaml ]; do + echo "waiting for packs"; + sleep 2; + done + volumeMounts: + - name: packs + mountPath: /opt/attune/packs + containers: + - name: worker + image: {{ include "attune.image" (dict "root" . "image" .Values.images.worker) }} + imagePullPolicy: {{ .Values.images.worker.pullPolicy }} + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + env: + - name: ATTUNE_CONFIG + value: /opt/attune/config.yaml + - name: ATTUNE__DATABASE__SCHEMA + value: {{ .Values.database.schema | quote }} + - name: ATTUNE_WORKER_RUNTIMES + value: {{ .Values.worker.runtimes | quote }} + - name: ATTUNE_WORKER_TYPE + value: container + - name: ATTUNE_WORKER_NAME + value: {{ .Values.worker.name | quote }} + - name: ATTUNE_API_URL + value: http://{{ include "attune.apiServiceName" . }}:{{ .Values.api.service.port }} + resources: + {{- toYaml .Values.worker.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /opt/attune/config.yaml + subPath: config.yaml + - name: packs + mountPath: /opt/attune/packs + - name: runtime-envs + mountPath: /opt/attune/runtime_envs + - name: artifacts + mountPath: /opt/attune/artifacts + volumes: + - name: config + configMap: + name: {{ include "attune.fullname" . }}-config + - name: packs + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-packs + - name: runtime-envs + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-runtime-envs + - name: artifacts + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-artifacts +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "attune.fullname" . }}-sensor + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.sensor.replicaCount }} + selector: + matchLabels: + {{- include "attune.componentLabels" (dict "root" . "component" "sensor") | nindent 6 }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "sensor") | nindent 8 }} + spec: + {{- if .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.global.imagePullSecrets | nindent 8 }} + {{- end }} + initContainers: + - name: wait-for-schema + image: postgres:16-alpine + command: ["/bin/sh", "-ec"] + args: + - | + until PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -tAc "SELECT to_regclass('${DB_SCHEMA}.identity')" | grep -q identity; do + echo "waiting for schema"; + sleep 2; + done + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + - name: wait-for-packs + image: busybox:1.36 + command: ["/bin/sh", "-ec"] + args: + - | + until [ -f /opt/attune/packs/core/pack.yaml ]; do + echo "waiting for packs"; + sleep 2; + done + volumeMounts: + - name: packs + mountPath: /opt/attune/packs + containers: + - name: sensor + image: {{ include "attune.image" (dict "root" . "image" .Values.images.sensor) }} + imagePullPolicy: {{ .Values.images.sensor.pullPolicy }} + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + env: + - name: ATTUNE_CONFIG + value: /opt/attune/config.yaml + - name: ATTUNE__DATABASE__SCHEMA + value: {{ .Values.database.schema | quote }} + - name: ATTUNE__WORKER__WORKER_TYPE + value: container + - name: ATTUNE_API_URL + value: http://{{ include "attune.apiServiceName" . }}:{{ .Values.api.service.port }} + - name: ATTUNE_MQ_URL + value: {{ include "attune.rabbitmqUrl" . | quote }} + - name: ATTUNE_PACKS_BASE_DIR + value: /opt/attune/packs + resources: + {{- toYaml .Values.sensor.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /opt/attune/config.yaml + subPath: config.yaml + - name: packs + mountPath: /opt/attune/packs + - name: runtime-envs + mountPath: /opt/attune/runtime_envs + volumes: + - name: config + configMap: + name: {{ include "attune.fullname" . }}-config + - name: packs + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-packs + - name: runtime-envs + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-runtime-envs +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "attune.notifierServiceName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + type: {{ .Values.notifier.service.type }} + selector: + {{- include "attune.componentLabels" (dict "root" . "component" "notifier") | nindent 4 }} + ports: + - name: ws + port: {{ .Values.notifier.service.port }} + targetPort: ws +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "attune.notifierServiceName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.notifier.replicaCount }} + selector: + matchLabels: + {{- include "attune.componentLabels" (dict "root" . "component" "notifier") | nindent 6 }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "notifier") | nindent 8 }} + spec: + {{- if .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.global.imagePullSecrets | nindent 8 }} + {{- end }} + initContainers: + - name: wait-for-schema + image: postgres:16-alpine + command: ["/bin/sh", "-ec"] + args: + - | + until PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -tAc "SELECT to_regclass('${DB_SCHEMA}.identity')" | grep -q identity; do + echo "waiting for schema"; + sleep 2; + done + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + containers: + - name: notifier + image: {{ include "attune.image" (dict "root" . "image" .Values.images.notifier) }} + imagePullPolicy: {{ .Values.images.notifier.pullPolicy }} + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + env: + - name: ATTUNE_CONFIG + value: /opt/attune/config.yaml + - name: ATTUNE__DATABASE__SCHEMA + value: {{ .Values.database.schema | quote }} + - name: ATTUNE__WORKER__WORKER_TYPE + value: container + ports: + - name: ws + containerPort: 8081 + readinessProbe: + httpGet: + path: /health + port: ws + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: ws + initialDelaySeconds: 20 + periodSeconds: 15 + resources: + {{- toYaml .Values.notifier.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /opt/attune/config.yaml + subPath: config.yaml + volumes: + - name: config + configMap: + name: {{ include "attune.fullname" . }}-config +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "attune.fullname" . }}-web + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + type: {{ .Values.web.service.type }} + selector: + {{- include "attune.componentLabels" (dict "root" . "component" "web") | nindent 4 }} + ports: + - name: http + port: {{ .Values.web.service.port }} + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "attune.fullname" . }}-web + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.web.replicaCount }} + selector: + matchLabels: + {{- include "attune.componentLabels" (dict "root" . "component" "web") | nindent 6 }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "web") | nindent 8 }} + spec: + {{- if .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.global.imagePullSecrets | nindent 8 }} + {{- end }} + containers: + - name: web + image: {{ include "attune.image" (dict "root" . "image" .Values.images.web) }} + imagePullPolicy: {{ .Values.images.web.pullPolicy }} + env: + - name: API_URL + value: {{ .Values.web.config.apiUrl | quote }} + - name: WS_URL + value: {{ .Values.web.config.wsUrl | quote }} + - name: ENVIRONMENT + value: {{ .Values.web.config.environment | quote }} + ports: + - name: http + containerPort: 80 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 20 + periodSeconds: 15 + resources: + {{- toYaml .Values.web.resources | nindent 12 }} diff --git a/charts/attune/templates/configmap.yaml b/charts/attune/templates/configmap.yaml new file mode 100644 index 0000000..50ca5f7 --- /dev/null +++ b/charts/attune/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "attune.fullname" . }}-config + labels: + {{- include "attune.labels" . | nindent 4 }} +data: + config.yaml: | +{{ .Files.Get "files/config.docker.yaml" | indent 4 }} diff --git a/charts/attune/templates/infrastructure.yaml b/charts/attune/templates/infrastructure.yaml new file mode 100644 index 0000000..2b37deb --- /dev/null +++ b/charts/attune/templates/infrastructure.yaml @@ -0,0 +1,225 @@ +{{- if .Values.database.postgresql.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "attune.postgresqlServiceName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + selector: + {{- include "attune.componentLabels" (dict "root" . "component" "postgresql") | nindent 4 }} + ports: + - name: postgres + port: {{ .Values.database.port }} + targetPort: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "attune.postgresqlServiceName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + serviceName: {{ include "attune.postgresqlServiceName" . }} + replicas: 1 + selector: + matchLabels: + {{- include "attune.componentLabels" (dict "root" . "component" "postgresql") | nindent 6 }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "postgresql") | nindent 8 }} + spec: + containers: + - name: postgresql + image: "{{ .Values.database.postgresql.image.repository }}:{{ .Values.database.postgresql.image.tag }}" + imagePullPolicy: IfNotPresent + env: + - name: POSTGRES_USER + value: {{ .Values.database.username | quote }} + - name: POSTGRES_PASSWORD + value: {{ .Values.database.password | quote }} + - name: POSTGRES_DB + value: {{ .Values.database.database | quote }} + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - name: postgres + containerPort: 5432 + livenessProbe: + exec: + command: ["pg_isready", "-U", "{{ .Values.database.username }}"] + initialDelaySeconds: 20 + periodSeconds: 10 + readinessProbe: + exec: + command: ["pg_isready", "-U", "{{ .Values.database.username }}"] + initialDelaySeconds: 10 + periodSeconds: 10 + resources: + {{- toYaml .Values.database.postgresql.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: + {{- toYaml .Values.database.postgresql.persistence.accessModes | nindent 10 }} + resources: + requests: + storage: {{ .Values.database.postgresql.persistence.size }} + {{- if .Values.database.postgresql.persistence.storageClassName }} + storageClassName: {{ .Values.database.postgresql.persistence.storageClassName }} + {{- end }} +{{- end }} +{{- if .Values.rabbitmq.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "attune.rabbitmqServiceName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + selector: + {{- include "attune.componentLabels" (dict "root" . "component" "rabbitmq") | nindent 4 }} + ports: + - name: amqp + port: {{ .Values.rabbitmq.port }} + targetPort: amqp + - name: management + port: {{ .Values.rabbitmq.managementPort }} + targetPort: management +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "attune.rabbitmqServiceName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + serviceName: {{ include "attune.rabbitmqServiceName" . }} + replicas: 1 + selector: + matchLabels: + {{- include "attune.componentLabels" (dict "root" . "component" "rabbitmq") | nindent 6 }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "rabbitmq") | nindent 8 }} + spec: + containers: + - name: rabbitmq + image: "{{ .Values.rabbitmq.image.repository }}:{{ .Values.rabbitmq.image.tag }}" + imagePullPolicy: IfNotPresent + env: + - name: RABBITMQ_DEFAULT_USER + value: {{ .Values.rabbitmq.username | quote }} + - name: RABBITMQ_DEFAULT_PASS + value: {{ .Values.rabbitmq.password | quote }} + - name: RABBITMQ_DEFAULT_VHOST + value: / + ports: + - name: amqp + containerPort: 5672 + - name: management + containerPort: 15672 + livenessProbe: + exec: + command: ["rabbitmq-diagnostics", "-q", "ping"] + initialDelaySeconds: 20 + periodSeconds: 15 + readinessProbe: + exec: + command: ["rabbitmq-diagnostics", "-q", "ping"] + initialDelaySeconds: 10 + periodSeconds: 10 + resources: + {{- toYaml .Values.rabbitmq.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/rabbitmq + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: + {{- toYaml .Values.rabbitmq.persistence.accessModes | nindent 10 }} + resources: + requests: + storage: {{ .Values.rabbitmq.persistence.size }} + {{- if .Values.rabbitmq.persistence.storageClassName }} + storageClassName: {{ .Values.rabbitmq.persistence.storageClassName }} + {{- end }} +{{- end }} +{{- if .Values.redis.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "attune.redisServiceName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + selector: + {{- include "attune.componentLabels" (dict "root" . "component" "redis") | nindent 4 }} + ports: + - name: redis + port: {{ .Values.redis.port }} + targetPort: redis +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "attune.redisServiceName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + serviceName: {{ include "attune.redisServiceName" . }} + replicas: 1 + selector: + matchLabels: + {{- include "attune.componentLabels" (dict "root" . "component" "redis") | nindent 6 }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "redis") | nindent 8 }} + spec: + containers: + - name: redis + image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}" + imagePullPolicy: IfNotPresent + command: ["redis-server", "--appendonly", "yes"] + ports: + - name: redis + containerPort: 6379 + livenessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 15 + periodSeconds: 10 + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 10 + periodSeconds: 10 + resources: + {{- toYaml .Values.redis.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: + {{- toYaml .Values.redis.persistence.accessModes | nindent 10 }} + resources: + requests: + storage: {{ .Values.redis.persistence.size }} + {{- if .Values.redis.persistence.storageClassName }} + storageClassName: {{ .Values.redis.persistence.storageClassName }} + {{- end }} +{{- end }} diff --git a/charts/attune/templates/ingress.yaml b/charts/attune/templates/ingress.yaml new file mode 100644 index 0000000..f31a68a --- /dev/null +++ b/charts/attune/templates/ingress.yaml @@ -0,0 +1,35 @@ +{{- if .Values.web.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "attune.fullname" . }}-web + labels: + {{- include "attune.labels" . | nindent 4 }} + {{- with .Values.web.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.web.ingress.className }} + ingressClassName: {{ .Values.web.ingress.className }} + {{- end }} + rules: + {{- range .Values.web.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "attune.fullname" $ }}-web + port: + number: {{ $.Values.web.service.port }} + {{- end }} + {{- end }} + {{- with .Values.web.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/attune/templates/jobs.yaml b/charts/attune/templates/jobs.yaml new file mode 100644 index 0000000..3bef27e --- /dev/null +++ b/charts/attune/templates/jobs.yaml @@ -0,0 +1,154 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "attune.fullname" . }}-migrations + labels: + {{- include "attune.labels" . | nindent 4 }} + app.kubernetes.io/component: migrations + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "-20" + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + ttlSecondsAfterFinished: {{ .Values.jobs.migrations.ttlSecondsAfterFinished }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "migrations") | nindent 8 }} + spec: + restartPolicy: OnFailure + {{- if .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.global.imagePullSecrets | nindent 8 }} + {{- end }} + containers: + - name: migrations + image: {{ include "attune.image" (dict "root" . "image" .Values.images.migrations) }} + imagePullPolicy: {{ .Values.images.migrations.pullPolicy }} + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + env: + - name: MIGRATIONS_DIR + value: /migrations + resources: + {{- toYaml .Values.jobs.migrations.resources | nindent 12 }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "attune.fullname" . }}-init-user + labels: + {{- include "attune.labels" . | nindent 4 }} + app.kubernetes.io/component: init-user + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "-10" + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + ttlSecondsAfterFinished: {{ .Values.jobs.initUser.ttlSecondsAfterFinished }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "init-user") | nindent 8 }} + spec: + restartPolicy: OnFailure + {{- if .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.global.imagePullSecrets | nindent 8 }} + {{- end }} + containers: + - name: init-user + image: {{ include "attune.image" (dict "root" . "image" .Values.images.initUser) }} + imagePullPolicy: {{ .Values.images.initUser.pullPolicy }} + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + command: ["/bin/sh", "-ec"] + args: + - | + until PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -tAc "SELECT to_regclass('${DB_SCHEMA}.identity')" | grep -q identity; do + echo "waiting for database schema"; + sleep 2; + done + exec /init-user.sh + resources: + {{- toYaml .Values.jobs.initUser.resources | nindent 12 }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "attune.fullname" . }}-init-packs + labels: + {{- include "attune.labels" . | nindent 4 }} + app.kubernetes.io/component: init-packs + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "0" + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + ttlSecondsAfterFinished: {{ .Values.jobs.initPacks.ttlSecondsAfterFinished }} + template: + metadata: + labels: + {{- include "attune.componentLabels" (dict "root" . "component" "init-packs") | nindent 8 }} + spec: + restartPolicy: OnFailure + {{- if .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.global.imagePullSecrets | nindent 8 }} + {{- end }} + containers: + - name: init-packs + image: {{ include "attune.image" (dict "root" . "image" .Values.images.initPacks) }} + imagePullPolicy: {{ .Values.images.initPacks.pullPolicy }} + envFrom: + - secretRef: + name: {{ include "attune.secretName" . }} + command: ["/bin/sh", "-ec"] + args: + - | + until python3 - <<'PY' + import os + import psycopg2 + + conn = psycopg2.connect( + host=os.environ["DB_HOST"], + port=os.environ["DB_PORT"], + user=os.environ["DB_USER"], + password=os.environ["DB_PASSWORD"], + dbname=os.environ["DB_NAME"], + ) + try: + with conn.cursor() as cur: + cur.execute("SET search_path TO %s, public" % os.environ["DB_SCHEMA"]) + cur.execute("SELECT to_regclass(%s)", (f"{os.environ['DB_SCHEMA']}.identity",)) + value = cur.fetchone()[0] + raise SystemExit(0 if value else 1) + finally: + conn.close() + PY + do + echo "waiting for database schema"; + sleep 2; + done + exec /init-packs.sh + volumeMounts: + - name: packs + mountPath: /opt/attune/packs + - name: runtime-envs + mountPath: /opt/attune/runtime_envs + - name: artifacts + mountPath: /opt/attune/artifacts + resources: + {{- toYaml .Values.jobs.initPacks.resources | nindent 12 }} + volumes: + - name: packs + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-packs + - name: runtime-envs + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-runtime-envs + - name: artifacts + persistentVolumeClaim: + claimName: {{ include "attune.fullname" . }}-artifacts diff --git a/charts/attune/templates/pvc.yaml b/charts/attune/templates/pvc.yaml new file mode 100644 index 0000000..32d69e0 --- /dev/null +++ b/charts/attune/templates/pvc.yaml @@ -0,0 +1,53 @@ +{{- if .Values.sharedStorage.packs.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "attune.fullname" . }}-packs + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + accessModes: + {{- toYaml .Values.sharedStorage.packs.accessModes | nindent 4 }} + resources: + requests: + storage: {{ .Values.sharedStorage.packs.size }} + {{- if .Values.sharedStorage.packs.storageClassName }} + storageClassName: {{ .Values.sharedStorage.packs.storageClassName }} + {{- end }} +--- +{{- end }} +{{- if .Values.sharedStorage.runtimeEnvs.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "attune.fullname" . }}-runtime-envs + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + accessModes: + {{- toYaml .Values.sharedStorage.runtimeEnvs.accessModes | nindent 4 }} + resources: + requests: + storage: {{ .Values.sharedStorage.runtimeEnvs.size }} + {{- if .Values.sharedStorage.runtimeEnvs.storageClassName }} + storageClassName: {{ .Values.sharedStorage.runtimeEnvs.storageClassName }} + {{- end }} +--- +{{- end }} +{{- if .Values.sharedStorage.artifacts.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "attune.fullname" . }}-artifacts + labels: + {{- include "attune.labels" . | nindent 4 }} +spec: + accessModes: + {{- toYaml .Values.sharedStorage.artifacts.accessModes | nindent 4 }} + resources: + requests: + storage: {{ .Values.sharedStorage.artifacts.size }} + {{- if .Values.sharedStorage.artifacts.storageClassName }} + storageClassName: {{ .Values.sharedStorage.artifacts.storageClassName }} + {{- end }} +{{- end }} diff --git a/charts/attune/templates/secret.yaml b/charts/attune/templates/secret.yaml new file mode 100644 index 0000000..40dda71 --- /dev/null +++ b/charts/attune/templates/secret.yaml @@ -0,0 +1,31 @@ +{{- if not .Values.security.existingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "attune.secretName" . }} + labels: + {{- include "attune.labels" . | nindent 4 }} +type: Opaque +stringData: + ATTUNE__SECURITY__JWT_SECRET: {{ .Values.security.jwtSecret | quote }} + ATTUNE__SECURITY__ENCRYPTION_KEY: {{ .Values.security.encryptionKey | quote }} + ATTUNE__DATABASE__URL: {{ include "attune.databaseUrl" . | quote }} + ATTUNE__MESSAGE_QUEUE__URL: {{ include "attune.rabbitmqUrl" . | quote }} + ATTUNE__CACHE__URL: {{ include "attune.redisUrl" . | quote }} + DB_HOST: {{ include "attune.postgresqlServiceName" . | quote }} + DB_PORT: {{ .Values.database.port | quote }} + DB_USER: {{ .Values.database.username | quote }} + DB_PASSWORD: {{ .Values.database.password | quote }} + DB_NAME: {{ .Values.database.database | quote }} + DB_SCHEMA: {{ .Values.database.schema | quote }} + TEST_LOGIN: {{ .Values.bootstrap.testUser.login | quote }} + TEST_DISPLAY_NAME: {{ .Values.bootstrap.testUser.displayName | quote }} + TEST_PASSWORD: {{ .Values.bootstrap.testUser.password | quote }} + DEFAULT_ADMIN_LOGIN: {{ .Values.bootstrap.testUser.login | quote }} + DEFAULT_ADMIN_PERMISSION_SET_REF: "core.admin" + SOURCE_PACKS_DIR: "/source/packs" + TARGET_PACKS_DIR: "/opt/attune/packs" + RUNTIME_ENVS_DIR: "/opt/attune/runtime_envs" + ARTIFACTS_DIR: "/opt/attune/artifacts" + LOADER_SCRIPT: "/scripts/load_core_pack.py" +{{- end }} diff --git a/charts/attune/values.yaml b/charts/attune/values.yaml new file mode 100644 index 0000000..04dca02 --- /dev/null +++ b/charts/attune/values.yaml @@ -0,0 +1,193 @@ +nameOverride: "" +fullnameOverride: "" + +global: + imageRegistry: "" + imageNamespace: "" + imageTag: edge + imagePullSecrets: [] + +security: + existingSecret: "" + jwtSecret: change-me-in-production + encryptionKey: change-me-in-production-32-bytes-minimum + +database: + schema: public + username: attune + password: attune + database: attune + host: "" + port: 5432 + url: "" + postgresql: + enabled: true + image: + repository: timescale/timescaledb + tag: 2.17.2-pg16 + persistence: + enabled: true + accessModes: + - ReadWriteOnce + size: 20Gi + storageClassName: "" + resources: {} + +rabbitmq: + username: attune + password: attune + host: "" + port: 5672 + url: "" + managementPort: 15672 + enabled: true + image: + repository: rabbitmq + tag: 3.13-management-alpine + persistence: + enabled: true + accessModes: + - ReadWriteOnce + size: 8Gi + storageClassName: "" + resources: {} + +redis: + enabled: true + host: "" + port: 6379 + url: "" + image: + repository: redis + tag: 7-alpine + persistence: + enabled: true + accessModes: + - ReadWriteOnce + size: 8Gi + storageClassName: "" + resources: {} + +bootstrap: + testUser: + login: test@attune.local + displayName: Test User + password: TestPass123! + +sharedStorage: + packs: + enabled: true + accessModes: + - ReadWriteMany + size: 2Gi + storageClassName: "" + runtimeEnvs: + enabled: true + accessModes: + - ReadWriteMany + size: 10Gi + storageClassName: "" + artifacts: + enabled: true + accessModes: + - ReadWriteMany + size: 20Gi + storageClassName: "" + +images: + api: + repository: attune-api + tag: "" + pullPolicy: IfNotPresent + executor: + repository: attune-executor + tag: "" + pullPolicy: IfNotPresent + worker: + repository: attune-worker + tag: "" + pullPolicy: IfNotPresent + sensor: + repository: attune-sensor + tag: "" + pullPolicy: IfNotPresent + notifier: + repository: attune-notifier + tag: "" + pullPolicy: IfNotPresent + web: + repository: attune-web + tag: "" + pullPolicy: IfNotPresent + migrations: + repository: attune-migrations + tag: "" + pullPolicy: IfNotPresent + initUser: + repository: attune-init-user + tag: "" + pullPolicy: IfNotPresent + initPacks: + repository: attune-init-packs + tag: "" + pullPolicy: IfNotPresent + +jobs: + migrations: + ttlSecondsAfterFinished: 300 + resources: {} + initUser: + ttlSecondsAfterFinished: 300 + resources: {} + initPacks: + ttlSecondsAfterFinished: 300 + resources: {} + +api: + replicaCount: 1 + service: + type: ClusterIP + port: 8080 + resources: {} + +executor: + replicaCount: 1 + resources: {} + +worker: + replicaCount: 1 + runtimes: shell,python,node,native + name: worker-full-01 + resources: {} + +sensor: + replicaCount: 1 + resources: {} + +notifier: + replicaCount: 1 + service: + type: ClusterIP + port: 8081 + resources: {} + +web: + replicaCount: 1 + service: + type: ClusterIP + port: 80 + config: + environment: kubernetes + apiUrl: http://localhost:8080 + wsUrl: ws://localhost:8081 + resources: {} + ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: attune.local + paths: + - path: / + pathType: Prefix + tls: [] diff --git a/docker/Dockerfile.init-packs b/docker/Dockerfile.init-packs new file mode 100644 index 0000000..3732bfc --- /dev/null +++ b/docker/Dockerfile.init-packs @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +COPY packs /source/packs +COPY scripts/load_core_pack.py /scripts/load_core_pack.py +COPY docker/init-packs.sh /init-packs.sh + +RUN pip install --no-cache-dir psycopg2-binary pyyaml && \ + chmod +x /init-packs.sh + +CMD ["/bin/sh", "/init-packs.sh"] diff --git a/docker/Dockerfile.init-user b/docker/Dockerfile.init-user new file mode 100644 index 0000000..110139d --- /dev/null +++ b/docker/Dockerfile.init-user @@ -0,0 +1,7 @@ +FROM postgres:16-alpine + +COPY docker/init-user.sh /init-user.sh + +RUN chmod +x /init-user.sh + +CMD ["/bin/sh", "/init-user.sh"] diff --git a/docker/Dockerfile.migrations b/docker/Dockerfile.migrations new file mode 100644 index 0000000..7442fb1 --- /dev/null +++ b/docker/Dockerfile.migrations @@ -0,0 +1,9 @@ +FROM postgres:16-alpine + +COPY migrations /migrations +COPY docker/run-migrations.sh /run-migrations.sh +COPY docker/init-roles.sql /docker/init-roles.sql + +RUN chmod +x /run-migrations.sh + +CMD ["/bin/sh", "/run-migrations.sh"] diff --git a/docs/deployment/gitea-registry-and-helm.md b/docs/deployment/gitea-registry-and-helm.md new file mode 100644 index 0000000..f6830e0 --- /dev/null +++ b/docs/deployment/gitea-registry-and-helm.md @@ -0,0 +1,110 @@ +# Gitea Registry And Helm Publishing + +This repository now includes: + +- A Gitea Actions publish workflow at `.gitea/workflows/publish.yml` +- OCI-published container images for the Kubernetes deployment path +- A Helm chart at `charts/attune` + +## What Gets Published + +The workflow publishes these images to the Gitea OCI registry: + +- `attune-api` +- `attune-executor` +- `attune-worker` +- `attune-sensor` +- `attune-notifier` +- `attune-web` +- `attune-migrations` +- `attune-init-user` +- `attune-init-packs` + +The Helm chart is pushed as an OCI chart to: + +- `oci:////helm/attune` + +## Required Gitea Repository Configuration + +Set these repository variables: + +- `GITEA_REGISTRY_HOST`: Registry hostname only, for example `gitea.example.com` +- `GITEA_REGISTRY_NAMESPACE`: Optional override for the registry namespace. If omitted, the workflow uses the repository owner. + +Set one of these authentication options: + +- Preferred: `GITEA_REGISTRY_USERNAME` and `GITEA_REGISTRY_PASSWORD` +- Fallback: allow the workflow `GITHUB_TOKEN` or Gitea-provided token to push packages + +## Publish Behavior + +The workflow runs on: + +- pushes to `main` +- pushes to `master` +- tags matching `v*` +- manual dispatch + +Tag behavior: + +- branch pushes publish `edge` and `sha-<12-char-sha>` +- release tags like `v0.3.0` publish `0.3.0`, `latest`, and `sha-<12-char-sha>` + +Chart packaging behavior: + +- branch pushes package the chart as `0.0.0-dev.` +- release tags package the chart with the tag version, for example `0.3.0` + +## Helm Install Flow + +Log in to the registry: + +```bash +helm registry login gitea.example.com --username +``` + +Install the chart: + +```bash +helm install attune oci://gitea.example.com//helm/attune \ + --version 0.3.0 \ + --set global.imageRegistry=gitea.example.com \ + --set global.imageNamespace= \ + --set global.imageTag=0.3.0 \ + --set web.config.apiUrl=https://attune.example.com/api \ + --set web.config.wsUrl=wss://attune.example.com/ws +``` + +For a branch build: + +```bash +helm install attune oci://gitea.example.com//helm/attune \ + --version 0.0.0-dev. \ + --set global.imageRegistry=gitea.example.com \ + --set global.imageNamespace= \ + --set global.imageTag=edge +``` + +## Chart Expectations + +The chart defaults to deploying: + +- PostgreSQL via TimescaleDB +- RabbitMQ +- Redis +- Attune API, executor, worker, sensor, notifier, and web services +- Migration, test-user bootstrap, and built-in pack bootstrap jobs + +Important constraints: + +- The shared `packs`, `runtime_envs`, and `artifacts` claims default to `ReadWriteMany` +- Your cluster storage class must support RWX for the default values to work as written +- `web.config.apiUrl` and `web.config.wsUrl` must be browser-reachable URLs, not cluster-internal service DNS names +- The default security and bootstrap values in `charts/attune/values.yaml` are placeholders and should be overridden + +## Suggested First Release Sequence + +1. Push the workflow and chart changes to `main`. +2. Verify that the workflow publishes the `edge` images and dev chart package. +3. Create a release tag such as `v0.1.0`. +4. Install the chart using that exact image tag and chart version.