Add support for initializing the pgAdmin4 Kubernetes Helm chart. #9225

pull/9236/head
Koren Peretz 2025-10-08 12:11:00 +03:00 committed by GitHub
parent f3567518d3
commit d03a0cdce6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 627 additions and 0 deletions

11
pkg/helm/Chart.yaml Normal file

File diff suppressed because one or more lines are too long

41
pkg/helm/README.md Normal file
View File

@ -0,0 +1,41 @@
# PgAdmin4 K8S Helm Chart
Its been a struggle to deploy pgadmin4 container on different restricted k8s distributions for example openshift, gke or vanilla k8s pod security standards.
This helm chart follows best security measures and practices and compatible with all different security contexts and restrictions.
Further explanation about the security implementation can be read here: https://korenp1.github.io
The helm chart also implements most pgadmin4 features, for instance, config_local.py, predefined server definitions or preferences.
The majority of features and values are covered in the helm chart but always can be more customable or tpl'able, open for contributions.
### Package && Push
The chart should dump its version and appVersion in the Chart.yaml file every release and pushed to docker.io/dpage repository.
`helm package . && helm push pgadmin4-helm-<VERSION>.tgz oci://docker.io/dpage`
### Installation Example:
`helm install mypgadmin4 oci://docker.io/dpage/pgadmin4-helm --set ingress.enabled=true`
### Important Values
| Value | Description | Default |
| --------- | ----------- | ------- |
| `containerPort` | Internal PgAdmin4 Port | `5050` |
| `image.registry` | Image registry | `"docker.io"` |
| `image.repository` | Image Repository | `"dpage/pgadmin4"` |
| `image.tag` | Image tag (If empty, will use .Chart.AppVersion) | `""` |
| `auth.email` | Admin Email | `"admin@pgadmin.org"` |
| `auth.password` | Admin password (If both auth.password and auth.existingSecret are empty, the password will be randomly generated) | `""` |
| `auth.existingSecret` | Existing secret name for admin password (If both auth.password and auth.existingSecret are empty, the password will be randomly generated) | `""` |
| `extraEnvVars` | Extra environment variables | `[]` |
| `config_local.enabled` | Whether to mount config_local.py file | `false` |
| `config_local.data` | config_local.py configuration content | `""` |
| `config_local.existingSecret` | Existing secret name containing config_local.py file | `""` |
| `serverDefinitions.enabled` | Whether to mount servers.json | `false` |
| `serverDefinitions.data` | Server definitions to import | `{}` |
| `preferences.enabled` | Whether to mount preferences.json | `false` |
| `preferences.data` | Preferences to load | `{}` |
| `resources.*` | Allocated requests and limits resources | `{"requests": {...}, "limits": {...}}` |
| `persistence.enabled` | PVC resource creation | `false` |
| `service.type` | Service type | `"ClusterIP"` |
| `service.loadBalancerIP` | Load balancer IP (Only if service.type is LoadBalancer) | `""` |
| `ingress.enabled` | Ingress resource creation | `false` |
| `ingress.hostname` | Ingress resource hostname | `"pgadmin4.local"` |

View File

@ -0,0 +1,11 @@
Thanks for using PgAdmin4 Helm Chart :)
Image: {{ template "pgadmin4.image" . }}
Credentials:
Email: {{ .Values.auth.email }}
Password: {{ printf "kubectl get secret %s -n %s -o jsonpath='{.data.%s}' | base64 -d" (include "pgadmin4.fullname" .) .Release.Namespace .Values.auth.passwordKey }}
Ingress: {{ ternary (tpl .Values.ingress.hostname .) "DISABLED" .Values.ingress.enabled }}
GOOD LUCK!

View File

@ -0,0 +1,28 @@
{{- define "pgadmin4.fullname" -}}
{{- tpl .Values.fullname . -}}
{{- end -}}
{{- define "pgadmin4.image" -}}
{{- printf "%s/%s:%s" (default .Values.image.registry .Values.global.imageRegistry) .Values.image.repository (default .Chart.AppVersion .Values.image.tag | toString) -}}
{{- end -}}
{{- define "pgadmin4.serviceAccountName" -}}
{{- default (ternary (include "pgadmin4.fullname" .) "default" .Values.serviceAccount.create) .Values.serviceAccount.name -}}
{{- end -}}
{{- define "renderSecurityContext" -}}
{{- $securityContext := omit .securityContext "enabled" -}}
{{- if or (eq .context.Values.global.compatibility.openshift.adaptSecurityContext "force") (and (eq .context.Values.global.compatibility.openshift.adaptSecurityContext "auto") (ternary "true" "" (.context.Capabilities.APIVersions.Has "security.openshift.io/v1"))) -}}
{{- $securityContext = omit $securityContext "fsGroup" "runAsUser" "runAsGroup" -}}
{{- if not $securityContext.seLinuxOptions }}
{{- $securityContext = omit $securityContext "seLinuxOptions" -}}
{{- end -}}
{{- end -}}
{{- if $securityContext.privileged }}
{{- $securityContext = omit $securityContext "capabilities" -}}
{{- end -}}
{{- if not .context.Values.global.compatibility.appArmor.enabled }}
{{- $securityContext = omit $securityContext "appArmorProfile" -}}
{{- end -}}
{{- $securityContext | toYaml -}}
{{- end -}}

View File

@ -0,0 +1,14 @@
{{- if .Values.preferences.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "pgadmin4.fullname" . }}-preferences
{{- with .Values.commonLabels }}
labels: {{ . | toYaml | nindent 4 }}
{{- end }}
{{- with .Values.commonAnnotations }}
annotations: {{ . | toYaml | nindent 4 }}
{{- end }}
data:
preferences.json: | {{ dict "preferences" .Values.preferences.data | toPrettyJson | nindent 4 }}
{{- end }}

View File

@ -0,0 +1,14 @@
{{- if .Values.serverDefinitions.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "pgadmin4.fullname" . }}-server-definitions
{{- with .Values.commonLabels }}
labels: {{ . | toYaml | nindent 4 }}
{{- end }}
{{- with .Values.commonAnnotations }}
annotations: {{ . | toYaml | nindent 4 }}
{{- end }}
data:
servers.json: | {{ tpl (dict "Servers" .Values.serverDefinitions.data | toYaml) . | fromYaml | toPrettyJson | nindent 4 }}
{{- end }}

View File

@ -0,0 +1,237 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ template "pgadmin4.fullname" . }}
{{- with .Values.commonLabels }}
labels: {{ . | toYaml | nindent 4 }}
{{- end }}
{{- with .Values.commonAnnotations }}
annotations: {{ . | toYaml | nindent 4 }}
{{- end }}
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: {{ default "pgadmin4" .Values.commonLabels.app }}
{{- with omit .Values.commonLabels "app" }}
{{- . | toYaml | nindent 6 }}
{{- end }}
template:
metadata:
labels:
app: {{ default "pgadmin4" .Values.commonLabels.app }}
{{- with omit .Values.commonLabels "app" }}
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- if or (not (empty .Values.commonAnnotations)) (not .Values.existingSecret) .Values.preferences.enabled .Values.serverDefinitions.enabled }}
annotations:
{{- with .Values.commonAnnotations }}
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- if not .Values.existingSecret }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- end }}
{{- if and .Values.config_local.enabled (empty .Values.config_local.existingSecret) }}
checksum/secret-config: {{ include (print $.Template.BasePath "/secret-config.yaml") . | sha256sum }}
{{- end }}
{{- if .Values.serverDefinitions.enabled }}
checksum/configmap-server-definitions: {{ include (print $.Template.BasePath "/configmap-serverDefinitions.yaml") . | sha256sum }}
{{- end }}
{{- if .Values.preferences.enabled }}
checksum/configmap-preferences: {{ include (print $.Template.BasePath "/configmap-preferences.yaml") . | sha256sum }}
{{- end }}
{{- end }}
spec:
{{- if or .Values.global.imagePullSecrets .Values.image.pullSecrets }}
imagePullSecrets: {{- concat .Values.global.imagePullSecrets .Values.image.pullSecrets | toYaml | nindent 8 }}
{{- end }}
serviceAccountName: {{ template "pgadmin4.serviceAccountName" . }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
{{- if .Values.podSecurityContext.enabled }}
securityContext: {{- include "renderSecurityContext" (dict "securityContext" .Values.podSecurityContext "context" .) | nindent 8 }}
enableServiceLinks: {{ .Values.enableServiceLinks }}
{{- end }}
volumes:
- name: empty-dir
emptyDir: {}
{{- if .Values.persistence.enabled }}
- name: data
persistentVolumeClaim:
claimName: {{ template "pgadmin4.fullname" . }}
{{- end }}
{{- if .Values.config_local.enabled }}
- name: config-local
secret:
secretName: {{ default (printf "%s-config" (include "pgadmin4.fullname" .)) .Values.config_local.existingSecret }}
items:
- key: {{ .Values.config_local.configKey }}
path: {{ .Values.config_local.configKey }}
{{- end }}
{{- if .Values.serverDefinitions.enabled }}
- name: server-definitions
configMap:
name: {{ template "pgadmin4.fullname" . }}-server-definitions
items:
- key: servers.json
path: servers.json
{{- end }}
{{- if .Values.preferences.enabled }}
- name: preferences
configMap:
name: {{ template "pgadmin4.fullname" . }}-preferences
items:
- key: preferences.json
path: preferences.json
{{- end }}
{{- with .Values.extraVolumes }}
{{- . | toYaml | nindent 8 }}
{{- end }}
containers:
- name: pgadmin4
{{- if .Values.containerSecurityContext.enabled }}
securityContext: {{- include "renderSecurityContext" (dict "securityContext" .Values.containerSecurityContext "context" .) | nindent 12 }}
{{- end }}
image: {{ template "pgadmin4.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
protocol: TCP
containerPort: {{ .Values.containerPort }}
resources:
requests:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
env:
- name: PGADMIN_DEFAULT_EMAIL
value: {{ .Values.auth.email }}
- name: PGADMIN_DEFAULT_PASSWORD
valueFrom:
secretKeyRef:
name: {{ default (include "pgadmin4.fullname" .) .Values.auth.existingSecret }}
key: {{ .Values.auth.passwordKey }}
- name: PGADMIN_LISTEN_PORT
value: {{ .Values.containerPort | quote }}
- name: PGADMIN_DISABLE_POSTFIX
value: {{ .Values.disablePostfix | quote }}
{{- with .Values.extraEnvVars }}
{{- tpl (. | toYaml) $ | nindent 14 }}
{{- end }}
volumeMounts:
- mountPath: /var/lib/pgadmin
{{- if .Values.persistence.enabled }}
name: data
{{- else }}
name: empty-dir
subPath: data
{{- end }}
- name: empty-dir
mountPath: /pgadmin4/config_distro.py
subPath: config_distro.py
- name: empty-dir
mountPath: /usr/bin/python3
subPath: python3
- name: empty-dir
mountPath: /tmp
subPath: tmp
- name: empty-dir
mountPath: /var/log/pgadmin
subPath: logs
{{- if .Values.config_local.enabled }}
- name: config-local
mountPath: /pgadmin4/config_local.py
subPath: {{ .Values.config_local.configKey }}
{{- end }}
{{- if .Values.serverDefinitions.enabled }}
- name: server-definitions
mountPath: /pgadmin4/servers.json
subPath: servers.json
{{- end }}
{{- if .Values.preferences.enabled }}
- name: preferences
mountPath: /pgadmin4/preferences.json
subPath: preferences.json
{{- end }}
{{- with .Values.extraVolumes }}
{{- . | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.startupProbe.enabled }}
startupProbe: {{- omit .Values.startupProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.readinessProbe.enabled }}
readinessProbe: {{- omit .Values.readinessProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.livenessProbe.enabled }}
livenessProbe: {{- omit .Values.livenessProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
initContainers:
- name: modify-config-distro-py-permissions
image: {{ template "pgadmin4.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["sh", "-x", "-c"]
args: ['cp /pgadmin4/config_distro.py . && chmod 777 config_distro.py']
workingDir: /emptyDir
volumeMounts:
- name: empty-dir
mountPath: /emptyDir
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 50m
memory: 64Mi
securityContext:
seLinuxOptions: {}
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
privileged: false
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
{{- if .Values.global.compatibility.appArmor.enabled }}
appArmorProfile:
type: RuntimeDefault
{{- end }}
windowsOptions:
hostProcess: false
- name: unset-python3-cli-net-cap
image: {{ template "pgadmin4.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["sh", "-x", "-c"]
args: ['ls /usr/bin/python3.* | sort -V -r | head -n 1 | xargs -I {} cp {} python3']
workingDir: /emptyDir
volumeMounts:
- name: empty-dir
mountPath: /emptyDir
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 50m
memory: 64Mi
securityContext:
seLinuxOptions: {}
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
privileged: false
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
{{- if .Values.global.compatibility.appArmor.enabled }}
appArmorProfile:
type: RuntimeDefault
{{- end }}
windowsOptions:
hostProcess: false

View File

@ -0,0 +1,4 @@
{{- range .Values.extraDeploy }}
---
{{ tpl (. | toYaml) $ }}
{{- end }}

View File

@ -0,0 +1,24 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ template "pgadmin4.fullname" . }}
{{- with .Values.commonLabels }}
labels: {{ . | toYaml | nindent 4 }}
{{- end }}
{{- if or .Values.ingress.annotations .Values.commonAnnotations }}
annotations: {{ merge .Values.ingress.annotations .Values.commonAnnotations | toYaml | nindent 4 }}
{{- end }}
spec:
rules:
- host: {{ tpl .Values.ingress.hostname . }}
http:
paths:
- backend:
service:
name: {{ template "pgadmin4.fullname" . }}
port:
name: http
path: /
pathType: Prefix
{{- end }}

View File

@ -0,0 +1,20 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ template "pgadmin4.fullname" . }}
{{- with .Values.commonLabels }}
labels: {{ . | toYaml | nindent 4 }}
{{- end }}
{{- with .Values.commonAnnotations }}
annotations: {{ . | toYaml | nindent 4 }}
{{- end }}
spec:
{{- if or .Values.global.defaultStorageClass .Values.persistence.storageClass }}
storageClassName: {{ default .Values.global.defaultStorageClass .Values.persistence.storageClass }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size }}
accessModes: {{ .Values.persistence.accessModes }}
{{- end }}

View File

@ -0,0 +1,15 @@
{{- if and .Values.config_local.enabled (empty .Values.config_local.existingSecret) }}
apiVersion: v1
kind: Secret
metadata:
name: {{ template "pgadmin4.fullname" . }}-config
{{- with .Values.commonLabels }}
labels: {{ . | toYaml | nindent 4 }}
{{- end }}
{{- with .Values.commonAnnotations }}
annotations: {{ . | toYaml | nindent 4 }}
{{- end }}
type: Opaque
stringData:
{{ .Values.config_local.configKey }}: | {{ tpl .Values.config_local.data . | nindent 4 }}
{{- end }}

View File

@ -0,0 +1,15 @@
{{- if empty .Values.auth.existingSecret }}
apiVersion: v1
kind: Secret
metadata:
name: {{ template "pgadmin4.fullname" . }}
{{- with .Values.commonLabels }}
labels: {{ . | toYaml | nindent 4 }}
{{- end }}
{{- with .Values.commonAnnotations }}
annotations: {{ . | toYaml | nindent 4 }}
{{- end }}
type: Opaque
stringData:
{{ .Values.auth.passwordKey }}: {{ coalesce .Values.auth.password (dig "data" .Values.auth.passwordKey "" (lookup "v1" "Secret" .Release.Namespace (include "pgadmin4.fullname" .)) | b64dec) (randAlphaNum 12) | quote }}
{{- end }}

View File

@ -0,0 +1,25 @@
apiVersion: v1
kind: Service
metadata:
name: {{ template "pgadmin4.fullname" . }}
{{- with .Values.commonLabels }}
labels: {{ . | toYaml | nindent 4 }}
{{- end }}
{{- if or .Values.service.annotations .Values.commonAnnotations }}
annotations: {{ merge .Values.service.annotations .Values.commonAnnotations | toYaml | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if and (eq .Values.service.type "LoadBalancer") (not (empty .Values.service.loadBalancerIP)) }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}
selector:
app: {{ default "pgadmin4" .Values.commonLabels.app }}
{{- with omit .Values.commonLabels "app" }}
{{- . | toYaml | nindent 4 }}
{{- end }}
ports:
- name: http
protocol: TCP
targetPort: http
port: {{ .Values.service.port }}

View File

@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ template "pgadmin4.serviceAccountName" . }}
{{- with .Values.commonLabels }}
labels: {{ . | toYaml | nindent 4 }}
{{- end }}
{{- with .Values.commonAnnotations }}
annotations: {{ . | toYaml | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
{{- end }}

155
pkg/helm/values.yaml Normal file
View File

@ -0,0 +1,155 @@
global:
imageRegistry: ""
defaultStorageClass: ""
imagePullSecrets: []
compatibility:
openshift:
adaptSecurityContext: auto
appArmor:
enabled: false
fullname: pgadmin4
image:
registry: docker.io
repository: dpage/pgadmin4
tag: ""
pullPolicy: IfNotPresent
pullSecrets: []
commonLabels:
app: pgadmin4
commonAnnotations: {}
extraDeploy: []
replicas: 1
containerPort: 5050
disablePostfix: true
enableServiceLinks: false
auth:
email: admin@pgadmin.org
password: ""
existingSecret: ""
passwordKey: password
extraEnvVars: []
# - name: PGADMIN_CONFIG_LOGIN_BANNER
# value: '"Welcome to pgAdmin4 :)"'
extraVolumes: []
extraVolumeMounts: []
config_local:
enabled: false
existingSecret: ""
configKey: config_local.py
data: ""
# data: |
# DEBUG = True
# JSON_LOGGER = True
# AUTHENTICATION_SOURCES = ['oauth2', 'internal']
serverDefinitions:
enabled: false
data: {}
# "1":
# Name: Minimally Defined Server
# Group: Servers
# Port: 5432
# Username: postgres
# Host: myHost
# MaintenanceDB: postgres
# ConnectionParameters:
# sslmode: prefer
# connect_timeout: 10
preferences:
enabled: false
data: {}
# misc:user_interface:theme: dark
# file_manager:options:file_upload_size: 500
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 250m
memory: 512Mi
persistence:
enabled: true
size: 1Gi
storageClass: ""
accessModes: ["ReadWriteOnce"]
service:
type: ClusterIP
port: 80
annotations: {}
loadBalancerIP: ""
serviceAccount:
create: true
name: ""
automountServiceAccountToken: false
ingress:
enabled: false
hostname: pgadmin4.local
annotations: {}
startupProbe:
enabled: false
httpGet:
path: /misc/ping
port: http
initialDelaySeconds: null
periodSeconds: null
timeoutSeconds: null
failureThreshold: null
readinessProbe:
enabled: true
httpGet:
path: /misc/ping
port: http
initialDelaySeconds: null
periodSeconds: null
timeoutSeconds: null
failureThreshold: null
livenessProbe:
enabled: false
httpGet:
path: /misc/ping
port: http
initialDelaySeconds: null
periodSeconds: null
timeoutSeconds: null
failureThreshold: null
podSecurityContext:
enabled: true
fsGroupChangePolicy: Always
sysctls: []
supplementalGroups: []
fsGroup: 1001
containerSecurityContext:
enabled: true
seLinuxOptions: {}
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
privileged: false
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
appArmorProfile:
type: RuntimeDefault
windowsOptions:
hostProcess: false