diff --git a/charts/ckan/templates/ckan/configmap.yaml b/charts/ckan/templates/ckan/configmap.yaml new file mode 100644 index 000000000..641c8d428 --- /dev/null +++ b/charts/ckan/templates/ckan/configmap.yaml @@ -0,0 +1,76 @@ +{{- $name := (printf "%s-%s-configmap" (include "common.names.fullname" .) "ckan") -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $name }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: {{ $name }} + namespace: {{ .Release.Namespace | quote }} +data: + ckan-init.sh: |- + #!/bin/bash + echo "initiate ckan" + if [[ -z "$DATAPUSHER_API_TOKEN" || -z "$EMAIL_API_KEY" ]]; then + + ckan config-tool $CKAN_INI "SECRET_KEY=${SECRET_KEY}" + ckan config-tool $CKAN_INI "api_token.jwt.encode.secret=string:${JWT_SECRET}" + ckan config-tool $CKAN_INI "api_token.jwt.decode.secret=string:${JWT_SECRET}" + + db_command="ckan -c $CKAN_INI db init" + max_retries=3 + attempt=0 + + echo "[prerun] Initializing or upgrading db - start" + + while [ $attempt -lt $max_retries ]; do + $db_command 2>&1 + if [ $? -eq 0 ]; then + echo "[prerun] Initializing or upgrading db - end" + break + else + if grep -q "OperationalError" <<< "$($db_command 2>&1)"; then + echo "[prerun] Database not ready, retrying in 5 seconds..." + sleep 5 + attempt=$((attempt + 1)) + else + echo "[prerun] Error occurred: $(tail -n 1 <(echo $?))" + break + fi + fi + attempt=$((attempt + 1)) + done + if [ $attempt -ge $max_retries ]; then + echo "[prerun] Failed to initialize or upgrade db after $max_retries attempts, exiting..." + exit 1 + fi + + + + if [[ -z "$CKAN_SYSADMIN_NAME" || -z "$CKAN_SYSADMIN_PASSWORD" || -z "$CKAN_SYSADMIN_EMAIL" ]]; then + echo "[prerun] Missing required environment variables: CKAN_SYSADMIN_NAME, CKAN_SYSADMIN_PASSWORD, or CKAN_SYSADMIN_EMAIL" + exit 1 + fi + + EXISTING_USER=$(ckan -c "$CKAN_INI" user show "$CKAN_SYSADMIN_NAME" 2>/dev/null) + + if [[ "$EXISTING_USER" == *"User: None"* ]]; then + echo "[prerun] Creating sysadmin user $CKAN_SYSADMIN_NAME" + + ckan -c "$CKAN_INI" user add "$CKAN_SYSADMIN_NAME" "password=$CKAN_SYSADMIN_PASSWORD" "email=$CKAN_SYSADMIN_EMAIL" + echo "[prerun] Created user $CKAN_SYSADMIN_NAME" + + ckan -c "$CKAN_INI" sysadmin add "$CKAN_SYSADMIN_NAME" + echo "[prerun] Made user $CKAN_SYSADMIN_NAME a sysadmin" + else + echo "[prerun] Sysadmin user $CKAN_SYSADMIN_NAME exists, skipping creation" + fi + + if [[ -z "$DATAPUSHER_API_TOKEN" ]]; then + ckan -c $CKAN_INI user token add ckan_admin datapusherApiKey | tail -n 1 | tr -d '\t' > /api-tokens/datapusherApiKey; + fi + if [[ -z "$EMAIL_API_KEY" ]]; then + ckan -c $CKAN_INI user token add ckan_admin emailApiKey | tail -n 1 | tr -d '\t' > /api-tokens/emailApiKey; + fi + else + echo "ckan already initiated" + fi diff --git a/charts/ckan/templates/ckan/deployment.yaml b/charts/ckan/templates/ckan/deployment.yaml index 7af097e38..c88655c4c 100644 --- a/charts/ckan/templates/ckan/deployment.yaml +++ b/charts/ckan/templates/ckan/deployment.yaml @@ -19,26 +19,22 @@ spec: labels: {{- include "common.labels.matchLabels" . | nindent 8 }} app.kubernetes.io/component: {{ $name }} annotations: - checksum/secret: {{ include "common.utils.checksumTemplate" (dict "path" "/postgresql/secret.yaml" "context" $) }}-{{ include "common.utils.checksumTemplate" (dict "path" "/postgresql/secret.yaml" "context" $) }}-{{ include "common.utils.checksumTemplate" (dict "path" "/solr/secret.yaml" "context" $) }} + checksum/secret: {{ include "common.utils.checksumTemplate" (dict "path" "/ckan/secret.yaml" "context" $) }}-{{ include "common.utils.checksumTemplate" (dict "path" "/postgresql/secret.yaml" "context" $) }}-{{ include "common.utils.checksumTemplate" (dict "path" "/solr/secret.yaml" "context" $) }} spec: - {{- include "common.images.renderPullSecrets" ( dict "images" (list .Values.ckan.image) "context" $) | indent 6 }} automountServiceAccountToken: false + serviceAccountName: {{ printf "%s-%s-serviceaccount" (include "common.names.fullname" $) $name | quote }} volumes: - name: "ckan" persistentVolumeClaim: claimName: {{ $claimName }} + - name: configmap-volume + configMap: + defaultMode: 0777 + name: my-configmap + - name: api-tokens-volume + emptyDir: {} securityContext: {{- toYaml .Values.ckan.podSecurityContext | default dict | nindent 8 }} - {{ if .Values.ckan.persistence -}} - initContainers: - - name: set-volume-ownsership - image: {{ printf "%s/busybox" ($.Values.global.imageRegistry | default (include "ckan.defaultRegistry" (dict))) }}:1.36 - command: ["sh", "-c", "chown -R 92:92 /var/lib/ckan"] # 92 is the uid and gid of ckan user/group - volumeMounts: - - name: ckan - mountPath: /var/lib/ckan - readOnly: false - {{ end }} containers: - name: {{ printf "%s-%s" .Chart.Name $name }} env: @@ -80,6 +76,10 @@ spec: - name: CKAN_SMTP_STARTTLS value: {{ .Values.ckan.smtp.starttls | quote}} {{- end }} + - name: CKAN__LOCALE_DEFAULT + value: {{ .Values.ckan.locales.default | quote }} + - name: CKAN__LOCALES_OFFERED + value: {{ .Values.ckan.locales.offered | quote }} - name: POSTGRES_USER value: postgres - name: POSTGRES_PASSWORD @@ -147,10 +147,35 @@ spec: value: "http://{{ printf "%s-%s" (include "common.names.fullname" $) "datapusher" }}:{{ include "ckan.datapusher.service.port" $ }}" - name: CKAN_DATAPUSHER_FORMATS value: {{ .Values.ckan.datapusher.formats | join " " | quote }} + - name: CKAN__DATAPUSHER__API_TOKEN + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: datapusherApiKey - name: CKAN__DATAPUSHER__CALLBACK_URL_BASE value: "http://{{ printf "%s-%s" (include "common.names.fullname" $) "ckan" }}:{{ include "ckan.ckan.service.port" $ }}/" + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: secretKey + - name: WTF_CSRF_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: wtfCsrfSecretKey + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: jwtSecret - name: CKAN__PLUGINS value: {{ .Values.ckan.plugins | join " " | quote }} + - name: CKAN__VIEWS__DEFAULT_VIEWS + value: {{ .Values.ckan.defaultViews | join " " | quote }} + {{- if .Values.ckan.extraEnvVars }} + {{- include "common.tplvalues.render" (dict "value" .Values.ckan.extraEnvVars "context" $) | nindent 12 }} + {{- end }} securityContext: {{- toYaml .Values.ckan.securityContext | default dict | nindent 12 }} readinessProbe: @@ -182,6 +207,4 @@ spec: resources: {{- toYaml .Values.ckan.resources | nindent 12 }} -#TODO Email notification -#TODO persist datapusher token else allway regen if ckan restart #TODO support CKAN HA diff --git a/charts/ckan/templates/ckan/post-install.yaml b/charts/ckan/templates/ckan/post-install.yaml new file mode 100644 index 000000000..80b8b1380 --- /dev/null +++ b/charts/ckan/templates/ckan/post-install.yaml @@ -0,0 +1,181 @@ +{{- $name := "ckan" -}} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ printf "%s-%s-post-install" (include "common.names.fullname" $) $name | quote }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: {{ $name }} + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + namespace: {{ .Release.Namespace | quote }} +spec: + template: + spec: + serviceAccountName: {{ printf "%s-%s-serviceaccount" (include "common.names.fullname" $) $name | quote }} + restartPolicy: Never + initContainers: + - name: wait-for-postgresql + image: docker.io/postgres:17.1-alpine + command: [ 'sh', '-c', 'until pg_isready -U $CKAN_DB_USER -d $CKAN_DB -h {{ printf "%s-%s" (include "ckan.postgresql.fullname" . ) "primary" }} -p 5432; do echo waiting for database; sleep 2; done;' ] + env: + - name: CKAN_DB_USER + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.postgresql.fullname" . ) }} + key: ckanDatabaseUsername + - name: CKAN_DB + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.postgresql.fullname" . ) }} + key: ckanDatabase + - name: ckan-initiate + image: {{ include "common.images.image" (dict "imageRoot" .Values.ckan.image "global" .Values.global) }} + command: ["sh","-c","/srv/app/ckan-init.sh"] + env: + - name: CKAN_SYSADMIN_NAME + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: sysAdminUsername + - name: CKAN_SYSADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: sysAdminPassword + - name: CKAN_SYSADMIN_EMAIL + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: sysAdminEmail + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.postgresql.fullname" . ) }} + key: postgresPassword + - name: POSTGRES_DB + value: postgres + - name: POSTGRES_HOST + value: {{ printf "%s-%s" (include "ckan.postgresql.fullname" . ) "primary" }} + - name: CKAN_DB_USER + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.postgresql.fullname" . ) }} + key: ckanDatabaseUsername + - name: CKAN_DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.postgresql.fullname" . ) }} + key: ckanDatabasePassword + - name: CKAN_DB + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.postgresql.fullname" . ) }} + key: ckanDatabase + - name: DATASTORE_READONLY_USER + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.postgresql.fullname" . ) }} + key: datastoreUsername + - name: DATASTORE_READONLY_PASSWORD + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.postgresql.fullname" . ) }} + key: datastorePassword + - name: DATASTORE_DB + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.postgresql.fullname" . ) }} + key: datastoreDatabase + - name: CKAN_SQLALCHEMY_URL + value: "postgresql://$(CKAN_DB_USER):$(CKAN_DB_PASSWORD)@{{ printf "%s-%s" (include "ckan.postgresql.fullname" . ) "primary" }}/$(CKAN_DB)" + - name: CKAN_DATASTORE_WRITE_URL + value: "postgresql://$(CKAN_DB_USER):$(CKAN_DB_PASSWORD)@{{ printf "%s-%s" (include "ckan.postgresql.fullname" . ) "primary" }}/$(DATASTORE_DB)" + - name: CKAN_DATASTORE_READ_URL + value: "postgresql://$(DATASTORE_READONLY_USER):$(DATASTORE_READONLY_PASSWORD)@{{ printf "%s-%s" (include "ckan.postgresql.fullname" . ) "read" }}/$(DATASTORE_DB)" + - name: CKAN_SOLR_URL + value: "http://{{ printf "%s-%s" (include "ckan.solr.fullname" . ) "headless" }}:{{ include "ckan.solr.service.port" $ }}/solr/ckan" + {{- if .Values.solr.auth.enabled }} + - name: CKAN_SOLR_USER + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.solr.fullname" . ) }} + key: solrUsername + - name: CKAN_SOLR_PASSWORD + valueFrom: + secretKeyRef: + name: {{ printf "%s-config" (include "ckan.solr.fullname" . ) }} + key: solrPassword + {{- end }} + - name: CKAN_REDIS_URL + value: "redis://{{ printf "%s-%s" (include "ckan.redis.fullname" . ) "headless" }}:{{ include "ckan.redis.service.port" $}}/0" + - name: DATAPUSHER_API_TOKEN + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: datapusherApiKey + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: secretKey + - name: WTF_CSRF_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: wtfCsrfSecretKey + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} + key: jwtSecret + - name: EMAIL_API_KEY + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s-config" (include "common.names.fullname" $) "ckan" }} + key: emailApiKey + volumeMounts: + - name: configmap-volume + mountPath: /srv/app/ckan-init.sh + readOnly: true + subPath: ckan-init.sh + - mountPath: /api-tokens + name: api-tokens-volume + - name: update-secret + image: docker.io/bitnami/kubectl + command: + - "/bin/sh" + - "-c" + - | + if [ "$(ls -A /api-tokens)" ]; then + if [ -f "/api-tokens/datapusherApiKey" ]; then + DATAPUSHER_API_TOKEN=$(cat /api-tokens/datapusherApiKey | tr -d '\n[:space:]' | base64 -w 0 ) && + PATCH='[{"op": "replace", "path": "/data/datapusherApiKey", "value": "'"$DATAPUSHER_API_TOKEN"'"}]' && + if [ $(kubectl get secret {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} -o jsonpath='{.data.datapusherApiKey}' | tr -d '\n[:space:]' | wc -m) -eq 0 ]; + then kubectl patch secrets {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} --type json -p="$PATCH"; fi + fi + if [ -f "/api-tokens/emailApiKey" ]; then + EMAIL_API_KEY=$(cat /api-tokens/emailApiKey | tr -d '\n[:space:]' | base64 -w 0 ) && + PATCH='[{"op": "replace", "path": "/data/emailApiKey", "value": "'"$EMAIL_API_KEY"'"}]' && + if [ $(kubectl get secret {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} -o jsonpath='{.data.emailApiKey}' | tr -d '\n[:space:]' | wc -m) -eq 0 ]; + then kubectl patch secrets {{ printf "%s-%s-config" (include "common.names.fullname" $) $name }} --type json -p="$PATCH"; fi + fi + kubectl rollout restart deployment/{{ printf "%s-%s" (include "common.names.fullname" $) $name | quote }} + fi + volumeMounts: + - mountPath: /api-tokens + name: api-tokens-volume + containers: + - name: postinstall-config + image: docker.io/busybox:1.28 + command: ["sh", "-c", "echo upgrade ready!"] # 92 is the uid and gid of ckan user/group + volumes: + - name: configmap-volume + configMap: + defaultMode: 0777 + name: {{ printf "%s-%s-configmap" (include "common.names.fullname" .) "ckan" }} + - name: api-tokens-volume + emptyDir: {} diff --git a/charts/ckan/templates/ckan/secret.yaml b/charts/ckan/templates/ckan/secret.yaml index 3a68962da..7a27e6761 100644 --- a/charts/ckan/templates/ckan/secret.yaml +++ b/charts/ckan/templates/ckan/secret.yaml @@ -1,5 +1,8 @@ {{- $name := (printf "%s-%s-config" (include "common.names.fullname" .) "ckan") -}} {{- $sysAdminPassword := include "common.secrets.passwords.manage" (dict "secret" $name "length" 42 "strong" false "key" "sysAdminPassword" "providedValues" (list "ckan.sysadmin.password") "skipB64enc" true "context" (dict "Values" .Values "Release" ((dict "IsUpgrade" false "IsInstall" true "Namespace" .Release.Namespace) | mergeOverwrite (deepCopy .Release)))) }} +{{- $secretKey := include "common.secrets.passwords.manage" (dict "secret" $name "length" 42 "strong" false "key" "secretKey" "providedValues" (list "ckan.secretKey") "skipB64enc" true "context" (dict "Values" .Values "Release" ((dict "IsUpgrade" false "IsInstall" true "Namespace" .Release.Namespace) | mergeOverwrite (deepCopy .Release)))) }} +{{- $wtfCsrfSecretKey := include "common.secrets.passwords.manage" (dict "secret" $name "length" 42 "strong" false "key" "wtfCsrfSecretKey" "providedValues" (list "ckan.wtfCsrfSecretKey") "skipB64enc" true "context" (dict "Values" .Values "Release" ((dict "IsUpgrade" false "IsInstall" true "Namespace" .Release.Namespace) | mergeOverwrite (deepCopy .Release)))) }} +{{- $jwtSecret := include "common.secrets.passwords.manage" (dict "secret" $name "length" 42 "strong" false "key" "jwtSecret" "providedValues" (list "ckan.jwtSecret") "skipB64enc" true "context" (dict "Values" .Values "Release" ((dict "IsUpgrade" false "IsInstall" true "Namespace" .Release.Namespace) | mergeOverwrite (deepCopy .Release)))) }} apiVersion: v1 kind: Secret metadata: @@ -11,6 +14,11 @@ stringData: sysAdminUsername: {{ .Values.ckan.sysadmin.name | default "ckan_admin" | quote }} sysAdminPassword: {{ $sysAdminPassword }} sysAdminEmail: {{ .Values.ckan.sysadmin.email | default "admin@test.com" | quote }} + secretKey: {{ $secretKey }} + wtfCsrfSecretKey: {{ $wtfCsrfSecretKey }} + jwtSecret: {{ $jwtSecret }} + datapusherApiKey: {{ "" }} + emailApiKey: {{ "" }} {{- if .Values.ckan.smtp }} smtpPassword: {{ .Values.ckan.smtp.password | quote }} - {{- end }} \ No newline at end of file + {{- end }} diff --git a/charts/ckan/templates/ckan/serviceaccount.yaml b/charts/ckan/templates/ckan/serviceaccount.yaml new file mode 100644 index 000000000..3bd45248f --- /dev/null +++ b/charts/ckan/templates/ckan/serviceaccount.yaml @@ -0,0 +1,43 @@ +{{- $name := "ckan" -}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ printf "%s-%s-secret-updater" (include "common.names.fullname" $) $name | quote }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: {{ $name }} + namespace: {{ .Release.Namespace | quote }} +rules: + - apiGroups: + - "" + - "apps" + resources: + - "secrets" + - "deployments" + resourceNames: + - {{ printf "%s-%s-config" (include "common.names.fullname" $) $name | quote }} + - {{ printf "%s-%s" (include "common.names.fullname" $) $name | quote }} + verbs: ["get","patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ printf "%s-%s-rolebinding" (include "common.names.fullname" $) $name }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: {{ $name }} + namespace: {{ .Release.Namespace | quote }} +subjects: + - kind: ServiceAccount + name: {{ printf "%s-%s-serviceaccount" (include "common.names.fullname" $) $name | quote }} +roleRef: + kind: Role + name: {{ printf "%s-%s-secret-updater" (include "common.names.fullname" $) $name | quote }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ printf "%s-%s-serviceaccount" (include "common.names.fullname" $) $name | quote }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: {{ $name }} + namespace: {{ .Release.Namespace | quote }} +automountServiceAccountToken: true diff --git a/charts/ckan/values.schema.json b/charts/ckan/values.schema.json index 6a36365a3..995e7ff4a 100644 --- a/charts/ckan/values.schema.json +++ b/charts/ckan/values.schema.json @@ -55,6 +55,24 @@ } }, "properties": { + "locales": { + "type": "object", + "additionalProperties": false, + "properties": { + "default":{ + "type": "string" + }, + "offered": { + "type": "string" + } + } + }, + "extraEnvVars": { + "type": ["array", "string"], + "description": "Array with extra environment variables to add to CKAN", + "default": [], + "items": {} + }, "siteId":{ "type": "string", "description": "The search index is linked to the value of the ckan.site_id, so if you have more than one CKAN instance using the same solr_url, they will each have a separate search index as long as their ckan.site_id values are different." @@ -72,6 +90,15 @@ }, "description": "The enabled plugins in the Ckan instance." }, + "defaultViews":{ + "type": "array", + "items": { + "items": { + "type": "string" + } + }, + "description": "The enabled plugins in the Ckan instance." + }, "datapusher": { "type": "object", "additionalProperties": false, diff --git a/charts/ckan/values.yaml b/charts/ckan/values.yaml index 061cba802..73e0544a6 100644 --- a/charts/ckan/values.yaml +++ b/charts/ckan/values.yaml @@ -26,13 +26,19 @@ defaultSecurityContext: &defaultSecurityContext runAsUser: 1000 ckan: + locales: + default: en + offered: en de fr + defaultViews: + - image_view + - datatables_view plugins: - - envvars - image_view - text_view - - recline_view + - datatables_view - datastore - datapusher + - envvars datapusher: formats: - csv @@ -49,8 +55,9 @@ ckan: pullPolicy: IfNotPresent pullSecrets: [] repository: teutonet/oci-images/ckan - tag: 1.0.5@sha256:df97fe51f67295fab60bd02153204e0cd12f5cb3eb8033f6cbc43a391b48a29d + tag: 1.0.10 digest: "" + extraEnvVars: [] ingress: ingressClassName: "" annotations: {}