From 16c65f8bf2075fd9bbe41bb3f8e5b056633e7496 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Sun, 18 Oct 2026 23:42:36 +0200 Subject: [PATCH] Add maintained Asciinema Helm chart --- .gitea/workflows/chart.yml | 86 ++++++++++ .gitea/workflows/deploy.yml | 139 ----------------- chart/asciinema/Chart.yaml | 6 + chart/asciinema/templates/_helpers.tpl | 20 +++ chart/asciinema/templates/asciinema.yaml | 147 ++++++++++++++++++ .../asciinema/templates/bucket-bootstrap.yaml | 43 +++++ chart/asciinema/templates/configmap.yaml | 27 ++++ chart/asciinema/templates/postgresql.yaml | 126 +++++++++++++++ chart/asciinema/values.schema.json | 20 +++ chart/asciinema/values.yaml | 93 +++++++++++ examples/values.yaml | 16 ++ infrastructure.yaml | 2 +- scripts/chart-version.sh | 13 ++ test.sh | 67 ++------ 14 files changed, 611 insertions(+), 194 deletions(-) create mode 100644 .gitea/workflows/chart.yml delete mode 100644 .gitea/workflows/deploy.yml create mode 100644 chart/asciinema/Chart.yaml create mode 100644 chart/asciinema/templates/_helpers.tpl create mode 100644 chart/asciinema/templates/asciinema.yaml create mode 100644 chart/asciinema/templates/bucket-bootstrap.yaml create mode 100644 chart/asciinema/templates/configmap.yaml create mode 100644 chart/asciinema/templates/postgresql.yaml create mode 100644 chart/asciinema/values.schema.json create mode 100644 chart/asciinema/values.yaml create mode 100644 examples/values.yaml create mode 100755 scripts/chart-version.sh diff --git a/.gitea/workflows/chart.yml b/.gitea/workflows/chart.yml new file mode 100644 index 0000000..eac67c0 --- /dev/null +++ b/.gitea/workflows/chart.yml @@ -0,0 +1,86 @@ +name: Validate, publish and promote Asciinema chart + +on: + push: + pull_request: + +env: + HELM_VERSION: v4.2.2 + KUBECONFORM_VERSION: v0.7.0 + GITOPS_REPOSITORY: feedc0de/homelab-gitops + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: azure/setup-helm@v4 + with: + version: ${{ env.HELM_VERSION }} + - name: Lint, render and validate the chart + run: ./test.sh + + publish: + if: gitea.ref == 'refs/heads/main' + needs: validate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: azure/setup-helm@v4 + with: + version: ${{ env.HELM_VERSION }} + - name: Select immutable chart version + run: printf 'CHART_VERSION=%s\n' "$(./scripts/chart-version.sh)" >> "$GITHUB_ENV" + - name: Package and publish chart + env: + PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + run: | + set -euo pipefail + test -n "$PACKAGE_USERNAME" + test -n "$PACKAGE_TOKEN" + mkdir -p .chart-packages + helm package chart/asciinema --destination .chart-packages --version "$CHART_VERSION" + curl --fail --silent --show-error --user "$PACKAGE_USERNAME:$PACKAGE_TOKEN" \ + --request POST --upload-file ".chart-packages/asciinema-$CHART_VERSION.tgz" \ + https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts + - name: Check that this is still current main + id: current-main + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + git fetch --no-tags origin main + if [ "$(git rev-parse FETCH_HEAD)" = "$COMMIT_SHA" ]; then + echo 'promote=true' >> "$GITEA_OUTPUT" + else + echo 'promote=false' >> "$GITEA_OUTPUT" + fi + - name: Check out GitOps repository + if: steps.current-main.outputs.promote == 'true' + uses: actions/checkout@v4 + with: + repository: ${{ env.GITOPS_REPOSITORY }} + ref: main + token: ${{ secrets.GITOPS_TOKEN }} + path: homelab-gitops + - name: Commit chart promotion + if: steps.current-main.outputs.promote == 'true' + working-directory: homelab-gitops + run: | + set -euo pipefail + ./scripts/update-chart-version.sh asciinema "$CHART_VERSION" + git config user.name feedc0de + git config user.email feedc0de@users.noreply.code.brunner.ninja + git add bootstrap/asciinema.yaml + if git diff --cached --quiet; then exit 0; fi + git commit -m "Deploy asciinema chart $CHART_VERSION" + for attempt in 1 2 3; do + git push origin HEAD:main && exit 0 + [ "$attempt" = 3 ] && exit 1 + git fetch origin main + git rebase origin/main + done diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml deleted file mode 100644 index 7c81b27..0000000 --- a/.gitea/workflows/deploy.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: Validate and deploy asciinema - -on: - push: - pull_request: - -env: - KUBECTL_VERSION: v1.36.3 - KUBERNETES_API: https://host.containers.internal:6443 - KUBERNETES_TLS_SERVER_NAME: 192.168.0.2 - NAMESPACE: asciinema - -jobs: - validate: - name: Validate manifests - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Check out source - uses: actions/checkout@v4 - - - name: Validate scripts and Kubernetes resources - run: bash test.sh - - deploy: - name: Deploy to Kubernetes - if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' - needs: validate - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - name: Check out source - uses: actions/checkout@v4 - - - name: Install kubectl - run: | - curl --fail --silent --show-error --location \ - --output "${RUNNER_TEMP}/kubectl" \ - "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" - chmod 0700 "${RUNNER_TEMP}/kubectl" - - - name: Configure Kubernetes access - env: - KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }} - run: | - test -n "${KUBE_CONFIG_BASE64}" - printf '%s' "${KUBE_CONFIG_BASE64}" | base64 --decode > "${RUNNER_TEMP}/kubeconfig" - chmod 0600 "${RUNNER_TEMP}/kubeconfig" - export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" - "${RUNNER_TEMP}/kubectl" config set-cluster cluster \ - --server="${KUBERNETES_API}" \ - --tls-server-name="${KUBERNETES_TLS_SERVER_NAME}" - - - name: Validate desired state on the server - run: | - export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" - "${RUNNER_TEMP}/kubectl" apply \ - --dry-run=server \ - --validate=false \ - --filename infrastructure.yaml - "${RUNNER_TEMP}/kubectl" apply \ - --dry-run=server \ - --validate=false \ - --filename asciinema.yaml - - - name: Apply and verify - run: | - set -euo pipefail - export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" - - "${RUNNER_TEMP}/kubectl" apply \ - --validate=false \ - --filename infrastructure.yaml - - for attempt in {1..120}; do - IFS='|' read -r generation observed desired current ready updated <<< "$( - "${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \ - get statefulset/postgresql \ - --output=jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.currentReplicas}|{.status.readyReplicas}|{.status.updatedReplicas}' - )" - - echo "PostgreSQL ${attempt}/120: generation ${observed}/${generation}, replicas ${updated}/${desired} updated, ${current}/${desired} current, ${ready}/${desired} ready" - if [[ "${observed}" == "${generation}" \ - && "${updated}" == "${desired}" \ - && "${current}" == "${desired}" \ - && "${ready}" == "${desired}" ]]; then - break - fi - - if [[ "${attempt}" == 120 ]]; then - echo "PostgreSQL rollout did not complete within ten minutes" >&2 - exit 1 - fi - sleep 5 - done - - "${RUNNER_TEMP}/kubectl" apply \ - --validate=false \ - --filename asciinema.yaml - # ConfigMap and Secret-backed environment variables are read only at - # process start, so every successful deployment recreates the sole pod. - "${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \ - rollout restart deployment/asciinema - - for attempt in {1..120}; do - IFS='|' read -r generation observed desired updated ready available unavailable image <<< "$( - "${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \ - get deployment/asciinema \ - --output=jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.updatedReplicas}|{.status.readyReplicas}|{.status.availableReplicas}|{.status.unavailableReplicas}|{.spec.template.spec.containers[0].image}' - )" - - echo "asciinema ${attempt}/120: generation ${observed}/${generation}, replicas ${updated}/${desired} updated, ${ready}/${desired} ready, ${available}/${desired} available, image ${image}" - if [[ "${observed}" == "${generation}" \ - && "${updated}" == "${desired}" \ - && "${ready}" == "${desired}" \ - && "${available}" == "${desired}" \ - && ( -z "${unavailable}" || "${unavailable}" == "0" ) ]]; then - break - fi - - if [[ "${attempt}" == 120 ]]; then - echo "asciinema rollout did not complete within ten minutes" >&2 - exit 1 - fi - sleep 5 - done - - for attempt in {1..30}; do - if curl --fail --silent --show-error --max-time 15 \ - https://asciinema.brunner.ninja/ >/dev/null; then - echo "asciinema deployment is healthy" - exit 0 - fi - echo "Public health check ${attempt}/30 failed; retrying" - sleep 5 - done - - echo "Public health check did not pass" >&2 - exit 1 diff --git a/chart/asciinema/Chart.yaml b/chart/asciinema/Chart.yaml new file mode 100644 index 0000000..dfa1b3f --- /dev/null +++ b/chart/asciinema/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: asciinema +description: Maintained Kubernetes adapter chart for the asciinema server image +type: application +version: 0.0.0 +appVersion: "20260626" diff --git a/chart/asciinema/templates/_helpers.tpl b/chart/asciinema/templates/_helpers.tpl new file mode 100644 index 0000000..a62bccb --- /dev/null +++ b/chart/asciinema/templates/_helpers.tpl @@ -0,0 +1,20 @@ +{{- define "asciinema.name" -}} +{{- default .Chart.Name .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "asciinema.appName" -}} +{{- include "asciinema.name" . }} +{{- end }} + +{{- define "asciinema.postgresqlName" -}} +{{- .Values.postgresql.name | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "asciinema.labels" -}} +app.kubernetes.io/name: {{ include "asciinema.appName" . }} +{{- end }} + +{{- define "asciinema.postgresqlLabels" -}} +app.kubernetes.io/name: postgresql +app.kubernetes.io/part-of: {{ include "asciinema.appName" . }} +{{- end }} diff --git a/chart/asciinema/templates/asciinema.yaml b/chart/asciinema/templates/asciinema.yaml new file mode 100644 index 0000000..f84829e --- /dev/null +++ b/chart/asciinema/templates/asciinema.yaml @@ -0,0 +1,147 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "asciinema.appName" . }} + labels: + {{- include "asciinema.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "asciinema.labels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "asciinema.labels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 30 + securityContext: + runAsNonRoot: true + runAsUser: 10000 + runAsGroup: 0 + fsGroup: 0 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: + type: RuntimeDefault + containers: + - name: asciinema + image: {{ printf "%s:%s" .Values.image.repository (default .Chart.AppVersion .Values.image.tag) | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + envFrom: + - configMapRef: + name: {{ include "asciinema.appName" . }} + env: + - name: HOME + value: /tmp + - name: CACHE_PATH + value: /var/cache/asciinema + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ .Values.secret.existingSecret }} + key: {{ .Values.secret.keys.databaseUrl }} + - name: SECRET_KEY_BASE + valueFrom: + secretKeyRef: + name: {{ .Values.secret.existingSecret }} + key: {{ .Values.secret.keys.secretKeyBase }} + - name: RELEASE_COOKIE + valueFrom: + secretKeyRef: + name: {{ .Values.secret.existingSecret }} + key: {{ .Values.secret.keys.releaseCookie }} + - name: S3_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ .Values.secret.existingSecret }} + key: {{ .Values.secret.keys.s3AccessKeyId }} + - name: S3_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.secret.existingSecret }} + key: {{ .Values.secret.keys.s3SecretAccessKey }} + ports: + - name: http + containerPort: 4000 + protocol: TCP + startupProbe: + httpGet: {path: /, port: http} + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 60 + readinessProbe: + httpGet: {path: /, port: http} + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + tcpSocket: {port: http} + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + readOnlyRootFilesystem: true + volumeMounts: + - name: cache + mountPath: /var/cache/asciinema + - name: tmp + mountPath: /tmp + volumes: + - name: cache + emptyDir: + sizeLimit: {{ .Values.cache.sizeLimit }} + - name: tmp + emptyDir: + sizeLimit: {{ .Values.tmp.sizeLimit }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.name }} + labels: + {{- include "asciinema.labels" . | nindent 4 }} +spec: + selector: + {{- include "asciinema.labels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP +{{- if .Values.ingress.enabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "asciinema.appName" . }} + labels: + {{- include "asciinema.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + rules: + - host: {{ required "ingress.host is required when ingress is enabled" .Values.ingress.host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ .Values.service.name }} + port: + name: http +{{- end }} diff --git a/chart/asciinema/templates/bucket-bootstrap.yaml b/chart/asciinema/templates/bucket-bootstrap.yaml new file mode 100644 index 0000000..4049547 --- /dev/null +++ b/chart/asciinema/templates/bucket-bootstrap.yaml @@ -0,0 +1,43 @@ +{{- if .Values.bucketBootstrap.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "asciinema.appName" . }}-bucket-bootstrap + labels: + {{- include "asciinema.labels" . | nindent 4 }} + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + backoffLimit: 5 + template: + spec: + restartPolicy: OnFailure + automountServiceAccountToken: false + containers: + - name: bucket + image: {{ .Values.bucketBootstrap.image }} + command: + - /bin/sh + - -ec + - >- + aws --endpoint-url "$S3_ENDPOINT" s3api head-bucket --bucket "$S3_BUCKET" >/dev/null 2>&1 || + aws --endpoint-url "$S3_ENDPOINT" s3api create-bucket --bucket "$S3_BUCKET" >/dev/null + env: + - name: S3_ENDPOINT + value: {{ .Values.config.s3.endpoint | quote }} + - name: S3_BUCKET + value: {{ .Values.config.s3.bucket | quote }} + - name: AWS_DEFAULT_REGION + value: {{ .Values.config.s3.region | quote }} + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ .Values.secret.existingSecret }} + key: {{ .Values.secret.keys.s3AccessKeyId }} + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.secret.existingSecret }} + key: {{ .Values.secret.keys.s3SecretAccessKey }} +{{- end }} diff --git a/chart/asciinema/templates/configmap.yaml b/chart/asciinema/templates/configmap.yaml new file mode 100644 index 0000000..fb624cf --- /dev/null +++ b/chart/asciinema/templates/configmap.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "asciinema.appName" . }} + labels: + {{- include "asciinema.labels" . | nindent 4 }} +data: + URL_HOST: {{ .Values.config.urlHost | quote }} + URL_SCHEME: {{ .Values.config.urlScheme | quote }} + TZ: {{ .Values.config.timezone | quote }} + DB_POOL_SIZE: {{ .Values.config.dbPoolSize | quote }} + S3_BUCKET: {{ .Values.config.s3.bucket | quote }} + S3_ENDPOINT: {{ .Values.config.s3.endpoint | quote }} + S3_REGION: {{ .Values.config.s3.region | quote }} + SMTP_HOST: {{ .Values.config.smtp.host | quote }} + SMTP_PORT: {{ .Values.config.smtp.port | quote }} + SMTP_TLS: {{ .Values.config.smtp.tls | quote }} + MAIL_FROM_ADDRESS: {{ .Values.config.smtp.fromAddress | quote }} + MAIL_REPLY_TO_ADDRESS: {{ .Values.config.smtp.replyToAddress | quote }} + CONTACT_EMAIL_ADDRESS: {{ .Values.config.contactEmailAddress | quote }} + ADMIN_PANEL_ON_MAIN_ENDPOINT: {{ .Values.config.adminPanelOnMainEndpoint | quote }} + ADMIN_BIND_ALL: {{ .Values.config.adminBindAll | quote }} + UPLOAD_AUTH_REQUIRED: {{ .Values.config.uploadAuthRequired | quote }} + UNREGISTERED_UPLOAD_COUNT_LIMIT: {{ .Values.config.unregisteredUploadCountLimit | quote }} + DEFAULT_RECORDING_VISIBILITY: {{ .Values.config.defaultRecordingVisibility | quote }} + DEFAULT_STREAM_VISIBILITY: {{ .Values.config.defaultStreamVisibility | quote }} + SIGN_UP_DISABLED: {{ .Values.config.signUpDisabled | quote }} diff --git a/chart/asciinema/templates/postgresql.yaml b/chart/asciinema/templates/postgresql.yaml new file mode 100644 index 0000000..b5c8671 --- /dev/null +++ b/chart/asciinema/templates/postgresql.yaml @@ -0,0 +1,126 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "asciinema.postgresqlName" . }} + labels: + {{- include "asciinema.postgresqlLabels" . | nindent 4 }} +spec: + clusterIP: None + selector: + {{- include "asciinema.postgresqlLabels" . | nindent 4 }} + ports: + - name: postgresql + port: 5432 + targetPort: postgresql + protocol: TCP +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "asciinema.postgresqlName" . }} + labels: + {{- include "asciinema.postgresqlLabels" . | nindent 4 }} +spec: + serviceName: {{ include "asciinema.postgresqlName" . }} + replicas: 1 + selector: + matchLabels: + {{- include "asciinema.postgresqlLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "asciinema.postgresqlLabels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 120 + securityContext: + runAsNonRoot: true + runAsUser: 70 + runAsGroup: 70 + fsGroup: 70 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: + type: RuntimeDefault + containers: + - name: postgresql + image: {{ printf "%s:%s" .Values.postgresql.image.repository .Values.postgresql.image.tag | quote }} + imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }} + env: + - name: POSTGRES_DB + value: {{ .Values.postgresql.database | quote }} + - name: POSTGRES_USER + value: {{ .Values.postgresql.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.secret.existingSecret }} + key: {{ .Values.secret.keys.databasePassword }} + - name: POSTGRES_INITDB_ARGS + value: --auth-host=scram-sha-256 + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - name: postgresql + containerPort: 5432 + protocol: TCP + startupProbe: + exec: + command: [pg_isready, --username={{ .Values.postgresql.username }}, --dbname={{ .Values.postgresql.database }}] + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 60 + readinessProbe: + exec: + command: [pg_isready, --username={{ .Values.postgresql.username }}, --dbname={{ .Values.postgresql.database }}] + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + exec: + command: [pg_isready, --username={{ .Values.postgresql.username }}, --dbname={{ .Values.postgresql.database }}] + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + resources: + {{- toYaml .Values.postgresql.resources | nindent 12 }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + readOnlyRootFilesystem: false + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + - name: run + mountPath: /var/run/postgresql + - name: tmp + mountPath: /tmp + volumes: + - name: run + emptyDir: + medium: Memory + sizeLimit: 16Mi + - name: tmp + emptyDir: + sizeLimit: 64Mi + {{- if .Values.postgresql.persistence.existingClaim }} + - name: data + persistentVolumeClaim: + claimName: {{ .Values.postgresql.persistence.existingClaim }} + {{- end }} + {{- if not .Values.postgresql.persistence.existingClaim }} + volumeClaimTemplates: + - metadata: + name: data + labels: + {{- include "asciinema.postgresqlLabels" . | nindent 10 }} + spec: + accessModes: [ReadWriteOnce] + {{- with .Values.postgresql.persistence.storageClass }} + storageClassName: {{ . }} + {{- end }} + resources: + requests: + storage: {{ .Values.postgresql.persistence.size }} + {{- end }} diff --git a/chart/asciinema/values.schema.json b/chart/asciinema/values.schema.json new file mode 100644 index 0000000..d53a17f --- /dev/null +++ b/chart/asciinema/values.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "fullnameOverride": {"type": "string"}, + "image": {"type": "object"}, + "replicaCount": {"type": "integer", "minimum": 1}, + "secret": {"type": "object"}, + "config": {"type": "object"}, + "service": {"type": "object"}, + "ingress": {"type": "object"}, + "postgresql": {"type": "object"}, + "resources": {"type": "object"}, + "cache": {"type": "object"}, + "tmp": {"type": "object"}, + "bucketBootstrap": {"type": "object"}, + "global": {"type": "object"} + } +} diff --git a/chart/asciinema/values.yaml b/chart/asciinema/values.yaml new file mode 100644 index 0000000..4d73c1a --- /dev/null +++ b/chart/asciinema/values.yaml @@ -0,0 +1,93 @@ +fullnameOverride: "" + +image: + repository: ghcr.io/asciinema/asciinema-server + tag: "" + pullPolicy: IfNotPresent + +replicaCount: 1 + +secret: + existingSecret: asciinema-secrets + keys: + databaseUrl: database-url + databasePassword: database-password + secretKeyBase: secret-key-base + releaseCookie: release-cookie + s3AccessKeyId: s3-access-key-id + s3SecretAccessKey: s3-secret-access-key + +config: + urlHost: asciinema.example.com + urlScheme: https + timezone: UTC + dbPoolSize: "5" + s3: + bucket: asciinema + endpoint: "" + region: us-east-1 + smtp: + host: "" + port: "587" + tls: if_available + fromAddress: "" + replyToAddress: "" + contactEmailAddress: "" + adminPanelOnMainEndpoint: true + adminBindAll: false + uploadAuthRequired: true + unregisteredUploadCountLimit: "0" + defaultRecordingVisibility: unlisted + defaultStreamVisibility: unlisted + signUpDisabled: false + +service: + name: asciinema + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + host: "" + +postgresql: + name: postgresql + image: + repository: postgres + tag: 17.11-alpine + pullPolicy: IfNotPresent + database: asciinema + username: asciinema + persistence: + storageClass: "" + size: 8Gi + existingClaim: "" + resources: + requests: + cpu: 50m + memory: 128Mi + ephemeral-storage: 64Mi + limits: + cpu: "1" + memory: 1Gi + ephemeral-storage: 1Gi + +resources: + requests: + cpu: 50m + memory: 256Mi + ephemeral-storage: 128Mi + limits: + cpu: "1" + memory: 1Gi + ephemeral-storage: 3Gi + +cache: + sizeLimit: 2Gi +tmp: + sizeLimit: 512Mi + +bucketBootstrap: + enabled: false + image: amazon/aws-cli:2.28.22 diff --git a/examples/values.yaml b/examples/values.yaml new file mode 100644 index 0000000..9e74e3b --- /dev/null +++ b/examples/values.yaml @@ -0,0 +1,16 @@ +fullnameOverride: asciinema + +config: + urlHost: asciinema.example.com + s3: + endpoint: http://object-store.example.svc.cluster.local + smtp: + host: smtp.example.com + fromAddress: asciinema@example.com + replyToAddress: admin@example.com + contactEmailAddress: admin@example.com + +ingress: + enabled: true + className: traefik + host: asciinema.example.com diff --git a/infrastructure.yaml b/infrastructure.yaml index e2ae73f..0140ff6 100644 --- a/infrastructure.yaml +++ b/infrastructure.yaml @@ -50,7 +50,7 @@ spec: type: RuntimeDefault containers: - name: postgresql - image: postgres:17.6-alpine + image: postgres:17.11-alpine imagePullPolicy: IfNotPresent env: - name: POSTGRES_DB diff --git a/scripts/chart-version.sh b/scripts/chart-version.sh new file mode 100755 index 0000000..0361693 --- /dev/null +++ b/scripts/chart-version.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +description=$(git describe --tags --long --match 'v[0-9]*' --dirty) +case "$description" in + *-dirty) echo 'Refusing to package a dirty tree.' >&2; exit 1 ;; +esac + +version=${description#v} +case "$version" in + *-0-g*) version=${version%%-0-g*} ;; +esac +printf '%s\n' "$version" diff --git a/test.sh b/test.sh index f28595e..e243cf5 100755 --- a/test.sh +++ b/test.sh @@ -1,61 +1,20 @@ #!/usr/bin/env bash - set -euo pipefail project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +chart="$project_dir/chart/asciinema" -bash -n "${project_dir}/install.sh" -bash -n "${project_dir}/create-ci-kubeconfig.sh" -bash -n "${project_dir}/test.sh" +bash -n "$project_dir/test.sh" "$project_dir/scripts/chart-version.sh" +helm lint "$chart" +helm template asciinema "$chart" --namespace asciinema --values "$project_dir/examples/values.yaml" > /tmp/asciinema-rendered.yaml -if ! grep -Fq 'storageClassName: rook-ceph-block-ec' "${project_dir}/infrastructure.yaml"; then - echo "The dedicated PostgreSQL claim must use retained Ceph block storage." >&2 - exit 1 +grep -Fq 'name: data' /tmp/asciinema-rendered.yaml +grep -Fq 'name: asciinema-secrets' /tmp/asciinema-rendered.yaml +grep -Fq 'kind: StatefulSet' /tmp/asciinema-rendered.yaml +grep -Fq 'kind: Ingress' /tmp/asciinema-rendered.yaml + +if command -v kubeconform >/dev/null 2>&1; then + kubeconform -strict -summary /tmp/asciinema-rendered.yaml +elif command -v docker >/dev/null 2>&1; then + docker run --rm --interactive ghcr.io/yannh/kubeconform:v0.7.0 -strict -summary < /tmp/asciinema-rendered.yaml fi - -if grep -Eqi 'redis|valkey' \ - "${project_dir}/asciinema.yaml" "${project_dir}/bootstrap.yaml"; then - echo "asciinema should not use Redis/Valkey." >&2 - exit 1 -fi - -if grep -Eq 'S3_PUBLIC_ENDPOINT|SplitEndpointS3|asciinema-custom-config' \ - "${project_dir}/asciinema.yaml"; then - echo "asciinema must use the cluster-local RGW endpoint for server-side reads." >&2 - exit 1 -fi - -if ! grep -Fq 'S3_ENDPOINT: http://rook-ceph-rgw-my-store.rook-ceph.svc.cluster.local' \ - "${project_dir}/asciinema.yaml"; then - echo "asciinema must use the cluster-local RGW Service." >&2 - exit 1 -fi - -validate_file() { - local manifest=$1 - shift - - if command -v kubeconform >/dev/null 2>&1; then - kubeconform -strict -summary "$@" "${manifest}" - elif command -v docker >/dev/null 2>&1; then - docker run --rm --interactive \ - ghcr.io/yannh/kubeconform:v0.7.0 \ - -strict -summary "$@" < "${manifest}" - elif command -v podman >/dev/null 2>&1; then - podman run --rm --interactive \ - ghcr.io/yannh/kubeconform:v0.7.0 \ - -strict -summary "$@" < "${manifest}" - else - echo "Required command not found: install kubeconform, Docker, or Podman" >&2 - exit 1 - fi -} - -validate_file "${project_dir}/asciinema.yaml" -validate_file "${project_dir}/bootstrap.yaml" -validate_file "${project_dir}/infrastructure.yaml" -validate_file "${project_dir}/namespace.yaml" -validate_file "${project_dir}/ceph-object-user.yaml" -ignore-missing-schemas -validate_file "${project_dir}/ci-deployer.yaml" - -echo "asciinema scripts and Kubernetes resources are valid."