diff --git a/clusters/prod/apps/workbench/appset-guacamole.yaml b/clusters/prod/apps/workbench/appset-guacamole.yaml new file mode 100644 index 0000000..848f04d --- /dev/null +++ b/clusters/prod/apps/workbench/appset-guacamole.yaml @@ -0,0 +1,48 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: guacamole + namespace: argocd +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - git: + repoURL: https://github.com/PilotDataPlatform/pilot-hdc-platform-gitops.git + revision: main + files: + - path: "clusters/prod/workbench/projects/*.yaml" + syncPolicy: + applicationsSync: create-update + template: + metadata: + name: 'guacamole-{{ .name }}' + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "13" + # NO finalizer — stateful app with PVC + spec: + project: default + source: + repoURL: https://github.com/PilotDataPlatform/pilot-hdc-platform-gitops.git + targetRevision: main + path: clusters/prod/workbench/guacamole-stack + helm: + valueFiles: + - ../../registry.yaml + - values.yaml + parameters: + - name: projectName + value: '{{ .name }}' + - name: domain + value: 'hdc.ebrains.eu' + destination: + server: https://kubernetes.default.svc + namespace: 'project-{{ .name }}' + syncPolicy: + automated: + prune: false + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/clusters/prod/apps/workbench/appset-jupyterhub.yaml b/clusters/prod/apps/workbench/appset-jupyterhub.yaml new file mode 100644 index 0000000..d7bdf6e --- /dev/null +++ b/clusters/prod/apps/workbench/appset-jupyterhub.yaml @@ -0,0 +1,51 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: jupyterhub + namespace: argocd +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - git: + repoURL: https://github.com/PilotDataPlatform/pilot-hdc-platform-gitops.git + revision: main + files: + - path: "clusters/prod/workbench/projects/*.yaml" + syncPolicy: + applicationsSync: create-update + template: + metadata: + name: 'jupyterhub-{{ .name }}' + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "13" + # NO finalizer — stateful app (hub PVC + user PVCs) + spec: + project: default + source: + repoURL: https://github.com/PilotDataPlatform/pilot-hdc-platform-gitops.git + targetRevision: main + path: clusters/prod/workbench/jupyterhub + helm: + releaseName: jupyterhub + valueFiles: + - ../../registry.yaml + - values.yaml + parameters: + - name: projectName + value: '{{ .name }}' + - name: domain + value: 'hdc.ebrains.eu' + - name: jupyterhub.hub.baseUrl + value: '/workbench/{{ .name }}/j/' + destination: + server: https://kubernetes.default.svc + namespace: 'project-{{ .name }}' + syncPolicy: + automated: + prune: false + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/clusters/prod/apps/workbench/appset-project-resources.yaml b/clusters/prod/apps/workbench/appset-project-resources.yaml new file mode 100644 index 0000000..eebe980 --- /dev/null +++ b/clusters/prod/apps/workbench/appset-project-resources.yaml @@ -0,0 +1,47 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: project-resources + namespace: argocd +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - git: + repoURL: https://github.com/PilotDataPlatform/pilot-hdc-platform-gitops.git + revision: main + files: + - path: "clusters/prod/workbench/projects/*.yaml" + syncPolicy: + applicationsSync: create-update + template: + metadata: + name: 'project-resources-{{ .name }}' + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "11" + # NO finalizer — manages PVCs + spec: + project: default + source: + repoURL: https://github.com/PilotDataPlatform/pilot-hdc-platform-gitops.git + targetRevision: main + path: clusters/prod/workbench/project-resources + helm: + releaseName: project-resources + valueFiles: + - ../../registry.yaml + - values.yaml + parameters: + - name: projectName + value: '{{ .name }}' + destination: + server: https://kubernetes.default.svc + namespace: 'project-{{ .name }}' + syncPolicy: + automated: + prune: false + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/clusters/prod/apps/workbench/appset-superset.yaml b/clusters/prod/apps/workbench/appset-superset.yaml new file mode 100644 index 0000000..c292aae --- /dev/null +++ b/clusters/prod/apps/workbench/appset-superset.yaml @@ -0,0 +1,49 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: superset + namespace: argocd +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - git: + repoURL: https://github.com/PilotDataPlatform/pilot-hdc-platform-gitops.git + revision: main + files: + - path: "clusters/prod/workbench/projects/*.yaml" + syncPolicy: + applicationsSync: create-update + template: + metadata: + name: 'superset-{{ .name }}' + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "13" + # NO finalizer — stateful app with PVC + spec: + project: default + source: + repoURL: https://github.com/PilotDataPlatform/pilot-hdc-platform-gitops.git + targetRevision: main + path: clusters/prod/workbench/superset + helm: + releaseName: superset + valueFiles: + - ../../registry.yaml + - values.yaml + parameters: + - name: projectName + value: '{{ .name }}' + - name: domain + value: 'hdc.ebrains.eu' + destination: + server: https://kubernetes.default.svc + namespace: 'project-{{ .name }}' + syncPolicy: + automated: + prune: false + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/clusters/prod/workbench/guacamole-stack/Chart.yaml b/clusters/prod/workbench/guacamole-stack/Chart.yaml new file mode 100644 index 0000000..91bf6ff --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v2 +name: guacamole-stack +version: 0.1.0 +dependencies: + - name: postgresql + version: "15.5.17" + repository: https://pilotdataplatform.github.io/helm-charts/ + alias: guacamole-postgresql diff --git a/clusters/prod/workbench/guacamole-stack/templates/_helpers.tpl b/clusters/prod/workbench/guacamole-stack/templates/_helpers.tpl new file mode 100644 index 0000000..edcee4d --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/templates/_helpers.tpl @@ -0,0 +1,8 @@ +{{/* +Common labels +*/}} +{{- define "guacamole-stack.labels" -}} +helm.sh/chart: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} diff --git a/clusters/prod/workbench/guacamole-stack/templates/guacamole-configmap.yaml b/clusters/prod/workbench/guacamole-stack/templates/guacamole-configmap.yaml new file mode 100644 index 0000000..d9dc94c --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/templates/guacamole-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: guacamole-configuration + labels: + app.kubernetes.io/name: guacamole + {{- include "guacamole-stack.labels" . | nindent 4 }} +data: + guacamole.properties: | + enable-environment-properties: true + postgresql-auto-create-accounts: true diff --git a/clusters/prod/workbench/guacamole-stack/templates/guacamole-deployment.yaml b/clusters/prod/workbench/guacamole-stack/templates/guacamole-deployment.yaml new file mode 100644 index 0000000..fd30deb --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/templates/guacamole-deployment.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: guacamole + labels: + app.kubernetes.io/name: guacamole + {{- include "guacamole-stack.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.guacamole.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: guacamole + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: guacamole + {{- include "guacamole-stack.labels" . | nindent 8 }} + spec: + imagePullSecrets: + - name: docker-registry-secret + containers: + - name: guacamole + image: "{{ .Values.global.imageRegistry }}/{{ .Values.guacamole.image.repository }}:{{ .Values.guacamole.image.tag }}" + imagePullPolicy: {{ .Values.guacamole.image.pullPolicy }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: GUACAMOLE_HOME + value: /etc/guacamole + - name: GUACD_HOSTNAME + value: guacd.{{ .Release.Namespace }}.svc.cluster.local + - name: GUACD_PORT + value: "4822" + - name: POSTGRES_HOSTNAME + value: postgres-guacamole.{{ .Release.Namespace }}.svc.cluster.local + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_DATABASE + value: guacamole_db + - name: POSTGRES_USER + value: guacamole_user + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: guacamole-pg-credentials + key: password + - name: POSTGRES_ABSOLUTE_MAX_CONNECTIONS + value: "{{ .Values.guacamoleProperties.postgresqlAbsoluteMaxConnections }}" + - name: OPENID_AUTHORIZATION_ENDPOINT + value: https://{{ .Values.keycloakDomain }}/realms/hdc/protocol/openid-connect/auth + - name: OPENID_JWKS_ENDPOINT + value: https://{{ .Values.keycloakDomain }}/realms/hdc/protocol/openid-connect/certs + - name: OPENID_ISSUER + value: https://{{ .Values.keycloakDomain }}/realms/hdc + - name: OPENID_CLIENT_ID + value: guacamole-{{ .Values.projectName }} + - name: OPENID_REDIRECT_URI + value: https://{{ .Values.domain }}/workbench/{{ .Values.projectName }}/guacamole/ + - name: OPENID_USERNAME_CLAIM_TYPE + value: {{ .Values.oidc.usernameClaim }} + - name: OPENID_SCOPE + value: {{ .Values.oidc.scope }} + volumeMounts: + - name: guacamole-config + mountPath: /etc/guacamole + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 15 + periodSeconds: 5 + resources: + {{- toYaml .Values.guacamole.resources | nindent 12 }} + volumes: + - name: guacamole-config + configMap: + name: guacamole-configuration diff --git a/clusters/prod/workbench/guacamole-stack/templates/guacamole-service.yaml b/clusters/prod/workbench/guacamole-stack/templates/guacamole-service.yaml new file mode 100644 index 0000000..f3142f2 --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/templates/guacamole-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: guacamole + labels: + app.kubernetes.io/name: guacamole + {{- include "guacamole-stack.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: 8080 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: guacamole + app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/clusters/prod/workbench/guacamole-stack/templates/guacd-deployment.yaml b/clusters/prod/workbench/guacamole-stack/templates/guacd-deployment.yaml new file mode 100644 index 0000000..9ef1517 --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/templates/guacd-deployment.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: guacd + labels: + app.kubernetes.io/name: guacd + {{- include "guacamole-stack.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.guacd.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: guacd + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: guacd + {{- include "guacamole-stack.labels" . | nindent 8 }} + spec: + imagePullSecrets: + - name: docker-registry-secret + containers: + - name: guacd + image: "{{ .Values.global.imageRegistry }}/{{ .Values.guacd.image.repository }}:{{ .Values.guacd.image.tag }}" + imagePullPolicy: {{ .Values.guacd.image.pullPolicy }} + ports: + - name: guacd + containerPort: 4822 + protocol: TCP + livenessProbe: + tcpSocket: + port: guacd + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: guacd + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + {{- toYaml .Values.guacd.resources | nindent 12 }} diff --git a/clusters/prod/workbench/guacamole-stack/templates/guacd-service.yaml b/clusters/prod/workbench/guacamole-stack/templates/guacd-service.yaml new file mode 100644 index 0000000..8370349 --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/templates/guacd-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: guacd + labels: + app.kubernetes.io/name: guacd + {{- include "guacamole-stack.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: 4822 + targetPort: guacd + protocol: TCP + name: guacd + selector: + app.kubernetes.io/name: guacd + app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/clusters/prod/workbench/guacamole-stack/templates/ingress.yaml b/clusters/prod/workbench/guacamole-stack/templates/ingress.yaml new file mode 100644 index 0000000..d2178f5 --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/templates/ingress.yaml @@ -0,0 +1,26 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: guacamole + labels: + app.kubernetes.io/name: guacamole + {{- include "guacamole-stack.labels" . | nindent 4 }} + annotations: + {{- toYaml .Values.ingress.annotations | nindent 4 }} +spec: + ingressClassName: nginx + tls: + - hosts: + - {{ .Values.domain }} + secretName: {{ .Values.domain }}-tls + rules: + - host: {{ .Values.domain }} + http: + paths: + - path: /workbench/{{ .Values.projectName }}/(guacamole(/|$).*) + pathType: ImplementationSpecific + backend: + service: + name: guacamole + port: + number: 8080 diff --git a/clusters/prod/workbench/guacamole-stack/templates/pg-external-secret.yaml b/clusters/prod/workbench/guacamole-stack/templates/pg-external-secret.yaml new file mode 100644 index 0000000..b78a431 --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/templates/pg-external-secret.yaml @@ -0,0 +1,22 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: guacamole-pg-credentials + labels: + {{- include "guacamole-stack.labels" . | nindent 4 }} +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault + target: + name: guacamole-pg-credentials + data: + - secretKey: password + remoteRef: + key: {{ .Values.vaultPgPath }} + property: pg-password + - secretKey: postgres-password + remoteRef: + key: {{ .Values.vaultPgPath }} + property: pg-password diff --git a/clusters/prod/workbench/guacamole-stack/templates/pg-init-configmap.yaml b/clusters/prod/workbench/guacamole-stack/templates/pg-init-configmap.yaml new file mode 100644 index 0000000..dc2a02a --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/templates/pg-init-configmap.yaml @@ -0,0 +1,714 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: guacamole-pg-schema + labels: + {{- include "guacamole-stack.labels" . | nindent 4 }} +data: + initdb.sql: | + -- Apache Guacamole 1.2.0 schema + -- 001-create-schema.sql + 002-create-admin-user.sql + -- Source: guacamole/guacamole:1.2.0 /opt/guacamole/postgresql/schema/ + + -- + -- Connection group types + -- + + CREATE TYPE guacamole_connection_group_type AS ENUM( + 'ORGANIZATIONAL', + 'BALANCING' + ); + + -- + -- Entity types + -- + + CREATE TYPE guacamole_entity_type AS ENUM( + 'USER', + 'USER_GROUP' + ); + + -- + -- Object permission types + -- + + CREATE TYPE guacamole_object_permission_type AS ENUM( + 'READ', + 'UPDATE', + 'DELETE', + 'ADMINISTER' + ); + + -- + -- System permission types + -- + + CREATE TYPE guacamole_system_permission_type AS ENUM( + 'CREATE_CONNECTION', + 'CREATE_CONNECTION_GROUP', + 'CREATE_SHARING_PROFILE', + 'CREATE_USER', + 'CREATE_USER_GROUP', + 'ADMINISTER' + ); + + -- + -- Guacamole proxy (guacd) encryption methods + -- + + CREATE TYPE guacamole_proxy_encryption_method AS ENUM( + 'NONE', + 'SSL' + ); + + -- + -- Table of connection groups + -- + + CREATE TABLE guacamole_connection_group ( + + connection_group_id serial NOT NULL, + parent_id integer, + connection_group_name varchar(128) NOT NULL, + type guacamole_connection_group_type + NOT NULL DEFAULT 'ORGANIZATIONAL', + + max_connections integer, + max_connections_per_user integer, + enable_session_affinity boolean NOT NULL DEFAULT FALSE, + + PRIMARY KEY (connection_group_id), + + CONSTRAINT connection_group_name_parent + UNIQUE (connection_group_name, parent_id), + + CONSTRAINT guacamole_connection_group_ibfk_1 + FOREIGN KEY (parent_id) + REFERENCES guacamole_connection_group (connection_group_id) + ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_connection_group_parent_id + ON guacamole_connection_group(parent_id); + + -- + -- Table of connections + -- + + CREATE TABLE guacamole_connection ( + + connection_id serial NOT NULL, + connection_name varchar(128) NOT NULL, + parent_id integer, + protocol varchar(32) NOT NULL, + + max_connections integer, + max_connections_per_user integer, + + connection_weight integer, + failover_only boolean NOT NULL DEFAULT FALSE, + + proxy_port integer, + proxy_hostname varchar(512), + proxy_encryption_method guacamole_proxy_encryption_method, + + PRIMARY KEY (connection_id), + + CONSTRAINT connection_name_parent + UNIQUE (connection_name, parent_id), + + CONSTRAINT guacamole_connection_ibfk_1 + FOREIGN KEY (parent_id) + REFERENCES guacamole_connection_group (connection_group_id) + ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_connection_parent_id + ON guacamole_connection(parent_id); + + -- + -- Table of base entities (user or user group) + -- + + CREATE TABLE guacamole_entity ( + + entity_id serial NOT NULL, + name varchar(128) NOT NULL, + type guacamole_entity_type NOT NULL, + + PRIMARY KEY (entity_id), + + CONSTRAINT guacamole_entity_name_scope + UNIQUE (type, name) + + ); + + -- + -- Table of users + -- + + CREATE TABLE guacamole_user ( + + user_id serial NOT NULL, + entity_id integer NOT NULL, + + password_hash bytea NOT NULL, + password_salt bytea, + password_date timestamptz NOT NULL, + + disabled boolean NOT NULL DEFAULT FALSE, + expired boolean NOT NULL DEFAULT FALSE, + + access_window_start time, + access_window_end time, + + valid_from date, + valid_until date, + + timezone varchar(64), + + full_name varchar(256), + email_address varchar(256), + organization varchar(256), + organizational_role varchar(256), + + PRIMARY KEY (user_id), + + CONSTRAINT guacamole_user_single_entity + UNIQUE (entity_id), + + CONSTRAINT guacamole_user_entity + FOREIGN KEY (entity_id) + REFERENCES guacamole_entity (entity_id) + ON DELETE CASCADE + + ); + + -- + -- Table of user groups + -- + + CREATE TABLE guacamole_user_group ( + + user_group_id serial NOT NULL, + entity_id integer NOT NULL, + + disabled boolean NOT NULL DEFAULT FALSE, + + PRIMARY KEY (user_group_id), + + CONSTRAINT guacamole_user_group_single_entity + UNIQUE (entity_id), + + CONSTRAINT guacamole_user_group_entity + FOREIGN KEY (entity_id) + REFERENCES guacamole_entity (entity_id) + ON DELETE CASCADE + + ); + + -- + -- Table of user group members + -- + + CREATE TABLE guacamole_user_group_member ( + + user_group_id integer NOT NULL, + member_entity_id integer NOT NULL, + + PRIMARY KEY (user_group_id, member_entity_id), + + CONSTRAINT guacamole_user_group_member_parent + FOREIGN KEY (user_group_id) + REFERENCES guacamole_user_group (user_group_id) ON DELETE CASCADE, + + CONSTRAINT guacamole_user_group_member_entity + FOREIGN KEY (member_entity_id) + REFERENCES guacamole_entity (entity_id) ON DELETE CASCADE + + ); + + -- + -- Table of sharing profiles + -- + + CREATE TABLE guacamole_sharing_profile ( + + sharing_profile_id serial NOT NULL, + sharing_profile_name varchar(128) NOT NULL, + primary_connection_id integer NOT NULL, + + PRIMARY KEY (sharing_profile_id), + + CONSTRAINT sharing_profile_name_primary + UNIQUE (sharing_profile_name, primary_connection_id), + + CONSTRAINT guacamole_sharing_profile_ibfk_1 + FOREIGN KEY (primary_connection_id) + REFERENCES guacamole_connection (connection_id) + ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_sharing_profile_primary_connection_id + ON guacamole_sharing_profile(primary_connection_id); + + -- + -- Table of connection parameters + -- + + CREATE TABLE guacamole_connection_parameter ( + + connection_id integer NOT NULL, + parameter_name varchar(128) NOT NULL, + parameter_value varchar(4096) NOT NULL, + + PRIMARY KEY (connection_id,parameter_name), + + CONSTRAINT guacamole_connection_parameter_ibfk_1 + FOREIGN KEY (connection_id) + REFERENCES guacamole_connection (connection_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_connection_parameter_connection_id + ON guacamole_connection_parameter(connection_id); + + -- + -- Table of sharing profile parameters + -- + + CREATE TABLE guacamole_sharing_profile_parameter ( + + sharing_profile_id integer NOT NULL, + parameter_name varchar(128) NOT NULL, + parameter_value varchar(4096) NOT NULL, + + PRIMARY KEY (sharing_profile_id, parameter_name), + + CONSTRAINT guacamole_sharing_profile_parameter_ibfk_1 + FOREIGN KEY (sharing_profile_id) + REFERENCES guacamole_sharing_profile (sharing_profile_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_sharing_profile_parameter_sharing_profile_id + ON guacamole_sharing_profile_parameter(sharing_profile_id); + + -- + -- Table of user attributes + -- + + CREATE TABLE guacamole_user_attribute ( + + user_id integer NOT NULL, + attribute_name varchar(128) NOT NULL, + attribute_value varchar(4096) NOT NULL, + + PRIMARY KEY (user_id, attribute_name), + + CONSTRAINT guacamole_user_attribute_ibfk_1 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_user_attribute_user_id + ON guacamole_user_attribute(user_id); + + -- + -- Table of user group attributes + -- + + CREATE TABLE guacamole_user_group_attribute ( + + user_group_id integer NOT NULL, + attribute_name varchar(128) NOT NULL, + attribute_value varchar(4096) NOT NULL, + + PRIMARY KEY (user_group_id, attribute_name), + + CONSTRAINT guacamole_user_group_attribute_ibfk_1 + FOREIGN KEY (user_group_id) + REFERENCES guacamole_user_group (user_group_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_user_group_attribute_user_group_id + ON guacamole_user_group_attribute(user_group_id); + + -- + -- Table of connection attributes + -- + + CREATE TABLE guacamole_connection_attribute ( + + connection_id integer NOT NULL, + attribute_name varchar(128) NOT NULL, + attribute_value varchar(4096) NOT NULL, + + PRIMARY KEY (connection_id, attribute_name), + + CONSTRAINT guacamole_connection_attribute_ibfk_1 + FOREIGN KEY (connection_id) + REFERENCES guacamole_connection (connection_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_connection_attribute_connection_id + ON guacamole_connection_attribute(connection_id); + + -- + -- Table of connection group attributes + -- + + CREATE TABLE guacamole_connection_group_attribute ( + + connection_group_id integer NOT NULL, + attribute_name varchar(128) NOT NULL, + attribute_value varchar(4096) NOT NULL, + + PRIMARY KEY (connection_group_id, attribute_name), + + CONSTRAINT guacamole_connection_group_attribute_ibfk_1 + FOREIGN KEY (connection_group_id) + REFERENCES guacamole_connection_group (connection_group_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_connection_group_attribute_connection_group_id + ON guacamole_connection_group_attribute(connection_group_id); + + -- + -- Table of sharing profile attributes + -- + + CREATE TABLE guacamole_sharing_profile_attribute ( + + sharing_profile_id integer NOT NULL, + attribute_name varchar(128) NOT NULL, + attribute_value varchar(4096) NOT NULL, + + PRIMARY KEY (sharing_profile_id, attribute_name), + + CONSTRAINT guacamole_sharing_profile_attribute_ibfk_1 + FOREIGN KEY (sharing_profile_id) + REFERENCES guacamole_sharing_profile (sharing_profile_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_sharing_profile_attribute_sharing_profile_id + ON guacamole_sharing_profile_attribute(sharing_profile_id); + + -- + -- Table of connection permissions + -- + + CREATE TABLE guacamole_connection_permission ( + + entity_id integer NOT NULL, + connection_id integer NOT NULL, + permission guacamole_object_permission_type NOT NULL, + + PRIMARY KEY (entity_id, connection_id, permission), + + CONSTRAINT guacamole_connection_permission_ibfk_1 + FOREIGN KEY (connection_id) + REFERENCES guacamole_connection (connection_id) ON DELETE CASCADE, + + CONSTRAINT guacamole_connection_permission_entity + FOREIGN KEY (entity_id) + REFERENCES guacamole_entity (entity_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_connection_permission_connection_id + ON guacamole_connection_permission(connection_id); + + CREATE INDEX guacamole_connection_permission_entity_id + ON guacamole_connection_permission(entity_id); + + -- + -- Table of connection group permissions + -- + + CREATE TABLE guacamole_connection_group_permission ( + + entity_id integer NOT NULL, + connection_group_id integer NOT NULL, + permission guacamole_object_permission_type NOT NULL, + + PRIMARY KEY (entity_id, connection_group_id, permission), + + CONSTRAINT guacamole_connection_group_permission_ibfk_1 + FOREIGN KEY (connection_group_id) + REFERENCES guacamole_connection_group (connection_group_id) ON DELETE CASCADE, + + CONSTRAINT guacamole_connection_group_permission_entity + FOREIGN KEY (entity_id) + REFERENCES guacamole_entity (entity_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_connection_group_permission_connection_group_id + ON guacamole_connection_group_permission(connection_group_id); + + CREATE INDEX guacamole_connection_group_permission_entity_id + ON guacamole_connection_group_permission(entity_id); + + -- + -- Table of sharing profile permissions + -- + + CREATE TABLE guacamole_sharing_profile_permission ( + + entity_id integer NOT NULL, + sharing_profile_id integer NOT NULL, + permission guacamole_object_permission_type NOT NULL, + + PRIMARY KEY (entity_id, sharing_profile_id, permission), + + CONSTRAINT guacamole_sharing_profile_permission_ibfk_1 + FOREIGN KEY (sharing_profile_id) + REFERENCES guacamole_sharing_profile (sharing_profile_id) ON DELETE CASCADE, + + CONSTRAINT guacamole_sharing_profile_permission_entity + FOREIGN KEY (entity_id) + REFERENCES guacamole_entity (entity_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_sharing_profile_permission_sharing_profile_id + ON guacamole_sharing_profile_permission(sharing_profile_id); + + CREATE INDEX guacamole_sharing_profile_permission_entity_id + ON guacamole_sharing_profile_permission(entity_id); + + -- + -- Table of system permissions + -- + + CREATE TABLE guacamole_system_permission ( + + entity_id integer NOT NULL, + permission guacamole_system_permission_type NOT NULL, + + PRIMARY KEY (entity_id, permission), + + CONSTRAINT guacamole_system_permission_entity + FOREIGN KEY (entity_id) + REFERENCES guacamole_entity (entity_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_system_permission_entity_id + ON guacamole_system_permission(entity_id); + + -- + -- Table of user permissions + -- + + CREATE TABLE guacamole_user_permission ( + + entity_id integer NOT NULL, + affected_user_id integer NOT NULL, + permission guacamole_object_permission_type NOT NULL, + + PRIMARY KEY (entity_id, affected_user_id, permission), + + CONSTRAINT guacamole_user_permission_ibfk_1 + FOREIGN KEY (affected_user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE, + + CONSTRAINT guacamole_user_permission_entity + FOREIGN KEY (entity_id) + REFERENCES guacamole_entity (entity_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_user_permission_affected_user_id + ON guacamole_user_permission(affected_user_id); + + CREATE INDEX guacamole_user_permission_entity_id + ON guacamole_user_permission(entity_id); + + -- + -- Table of user group permissions + -- + + CREATE TABLE guacamole_user_group_permission ( + + entity_id integer NOT NULL, + affected_user_group_id integer NOT NULL, + permission guacamole_object_permission_type NOT NULL, + + PRIMARY KEY (entity_id, affected_user_group_id, permission), + + CONSTRAINT guacamole_user_group_permission_affected_user_group + FOREIGN KEY (affected_user_group_id) + REFERENCES guacamole_user_group (user_group_id) ON DELETE CASCADE, + + CONSTRAINT guacamole_user_group_permission_entity + FOREIGN KEY (entity_id) + REFERENCES guacamole_entity (entity_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_user_group_permission_affected_user_group_id + ON guacamole_user_group_permission(affected_user_group_id); + + CREATE INDEX guacamole_user_group_permission_entity_id + ON guacamole_user_group_permission(entity_id); + + -- + -- Table of connection history records + -- + + CREATE TABLE guacamole_connection_history ( + + history_id serial NOT NULL, + user_id integer DEFAULT NULL, + username varchar(128) NOT NULL, + remote_host varchar(256) DEFAULT NULL, + connection_id integer DEFAULT NULL, + connection_name varchar(128) NOT NULL, + sharing_profile_id integer DEFAULT NULL, + sharing_profile_name varchar(128) DEFAULT NULL, + start_date timestamptz NOT NULL, + end_date timestamptz DEFAULT NULL, + + PRIMARY KEY (history_id), + + CONSTRAINT guacamole_connection_history_ibfk_1 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE SET NULL, + + CONSTRAINT guacamole_connection_history_ibfk_2 + FOREIGN KEY (connection_id) + REFERENCES guacamole_connection (connection_id) ON DELETE SET NULL, + + CONSTRAINT guacamole_connection_history_ibfk_3 + FOREIGN KEY (sharing_profile_id) + REFERENCES guacamole_sharing_profile (sharing_profile_id) ON DELETE SET NULL + + ); + + CREATE INDEX guacamole_connection_history_user_id + ON guacamole_connection_history(user_id); + + CREATE INDEX guacamole_connection_history_connection_id + ON guacamole_connection_history(connection_id); + + CREATE INDEX guacamole_connection_history_sharing_profile_id + ON guacamole_connection_history(sharing_profile_id); + + CREATE INDEX guacamole_connection_history_start_date + ON guacamole_connection_history(start_date); + + CREATE INDEX guacamole_connection_history_end_date + ON guacamole_connection_history(end_date); + + CREATE INDEX guacamole_connection_history_connection_id_start_date + ON guacamole_connection_history(connection_id, start_date); + + -- + -- User login/logout history + -- + + CREATE TABLE guacamole_user_history ( + + history_id serial NOT NULL, + user_id integer DEFAULT NULL, + username varchar(128) NOT NULL, + remote_host varchar(256) DEFAULT NULL, + start_date timestamptz NOT NULL, + end_date timestamptz DEFAULT NULL, + + PRIMARY KEY (history_id), + + CONSTRAINT guacamole_user_history_ibfk_1 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE SET NULL + + ); + + CREATE INDEX guacamole_user_history_user_id + ON guacamole_user_history(user_id); + + CREATE INDEX guacamole_user_history_start_date + ON guacamole_user_history(start_date); + + CREATE INDEX guacamole_user_history_end_date + ON guacamole_user_history(end_date); + + CREATE INDEX guacamole_user_history_user_id_start_date + ON guacamole_user_history(user_id, start_date); + + -- + -- User password history + -- + + CREATE TABLE guacamole_user_password_history ( + + password_history_id serial NOT NULL, + user_id integer NOT NULL, + + password_hash bytea NOT NULL, + password_salt bytea, + password_date timestamptz NOT NULL, + + PRIMARY KEY (password_history_id), + + CONSTRAINT guacamole_user_password_history_ibfk_1 + FOREIGN KEY (user_id) + REFERENCES guacamole_user (user_id) ON DELETE CASCADE + + ); + + CREATE INDEX guacamole_user_password_history_user_id + ON guacamole_user_password_history(user_id); + + -- 002-create-admin-user.sql + -- Create default user "guacadmin" with password "guacadmin" + INSERT INTO guacamole_entity (name, type) VALUES ('guacadmin', 'USER'); + INSERT INTO guacamole_user (entity_id, password_hash, password_salt, password_date) + SELECT + entity_id, + decode('CA458A7D494E3BE824F5E1E175A1556C0F8EEF2C2D7DF3633BEC4A29C4411960', 'hex'), + decode('FE24ADC5E11E2B25288D1704ABE67A79E342ECC26064CE69C5B3177795A82264', 'hex'), + CURRENT_TIMESTAMP + FROM guacamole_entity WHERE name = 'guacadmin' AND guacamole_entity.type = 'USER'; + + -- Grant guacadmin all system permissions + INSERT INTO guacamole_system_permission (entity_id, permission) + SELECT entity_id, permission::guacamole_system_permission_type + FROM ( + VALUES + ('guacadmin', 'CREATE_CONNECTION'), + ('guacadmin', 'CREATE_CONNECTION_GROUP'), + ('guacadmin', 'CREATE_SHARING_PROFILE'), + ('guacadmin', 'CREATE_USER'), + ('guacadmin', 'CREATE_USER_GROUP'), + ('guacadmin', 'ADMINISTER') + ) permissions (username, permission) + JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER'; + + -- Grant admin permission to read/update/administer self + INSERT INTO guacamole_user_permission (entity_id, affected_user_id, permission) + SELECT guacamole_entity.entity_id, guacamole_user.user_id, permission::guacamole_object_permission_type + FROM ( + VALUES + ('guacadmin', 'guacadmin', 'READ'), + ('guacadmin', 'guacadmin', 'UPDATE'), + ('guacadmin', 'guacadmin', 'ADMINISTER') + ) permissions (username, affected_username, permission) + JOIN guacamole_entity ON permissions.username = guacamole_entity.name AND guacamole_entity.type = 'USER' + JOIN guacamole_entity affected ON permissions.affected_username = affected.name AND guacamole_entity.type = 'USER' + JOIN guacamole_user ON guacamole_user.entity_id = affected.entity_id; + + -- Grant guacamole_user access to all tables/sequences + -- User created by bitnami chart from existingSecret — no CREATE USER here + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO guacamole_user; + GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO guacamole_user; diff --git a/clusters/prod/workbench/guacamole-stack/values.yaml b/clusters/prod/workbench/guacamole-stack/values.yaml new file mode 100644 index 0000000..8872485 --- /dev/null +++ b/clusters/prod/workbench/guacamole-stack/values.yaml @@ -0,0 +1,101 @@ +projectName: "" +domain: "hdc.ebrains.eu" +keycloakDomain: "iam.hdc.ebrains.eu" + +# Image tags pinned here (not in versions.yaml) — upstream Apache releases, not HDC-built +guacd: + image: + repository: hdc-services-external/guacamole/guacd + tag: "1.2.0" + pullPolicy: IfNotPresent + replicaCount: 1 + resources: + limits: + cpu: 500m + memory: 256Mi + requests: + cpu: 10m + memory: 64Mi + +guacamole: + image: + repository: hdc-services-external/guacamole/guacamole + tag: "1.2.0" + pullPolicy: IfNotPresent + replicaCount: 1 + resources: + limits: + cpu: "1" + memory: 1Gi + requests: + cpu: 10m + memory: 128Mi + +guacamoleProperties: + postgresqlAbsoluteMaxConnections: 100 + +oidc: + scope: "openid username email profile groups" + usernameClaim: "username" + +ingress: + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/affinity: cookie + nginx.ingress.kubernetes.io/rewrite-target: /$1 + nginx.ingress.kubernetes.io/proxy-body-size: 20m + nginx.ingress.kubernetes.io/proxy-buffering: "on" + nginx.ingress.kubernetes.io/proxy-buffer-size: 512k + nginx.ingress.kubernetes.io/proxy-buffers-number: "8" + +vaultPgPath: secret/data/guacamole + +guacamole-postgresql: + fullnameOverride: postgres-guacamole + global: + imagePullSecrets: + - docker-registry-secret + postgresql: + auth: + database: guacamole_db + username: guacamole_user + existingSecret: guacamole-pg-credentials + secretKeys: + adminPasswordKey: postgres-password + userPasswordKey: password + image: + repository: hdc-services-external/bitnami/postgresql + architecture: standalone + primary: + pgHbaConfiguration: |- + local all all trust + host all all 127.0.0.1/32 trust + host all all ::1/128 trust + host all all 0.0.0.0/0 md5 + host all all ::/0 md5 + extendedConfiguration: |- + password_encryption = md5 + initdb: + scripts: + 00-init-guacamole.sh: | + #!/bin/bash + set -e + export PGPASSWORD=$POSTGRES_PASSWORD + psql -U postgres -d guacamole_db -f /tmp/guacamole-schema/initdb.sql + extraVolumeMounts: + - name: guacamole-schema + mountPath: /tmp/guacamole-schema + extraVolumes: + - name: guacamole-schema + configMap: + name: guacamole-pg-schema + persistence: + size: 2Gi + storageClass: "csi-cinder-high-speed" + resources: + limits: + cpu: 500m + memory: 256Mi + requests: + cpu: 10m + memory: 64Mi diff --git a/clusters/prod/workbench/jupyterhub/Chart.yaml b/clusters/prod/workbench/jupyterhub/Chart.yaml new file mode 100644 index 0000000..f0cbbb6 --- /dev/null +++ b/clusters/prod/workbench/jupyterhub/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: jupyterhub-stack +version: 0.1.0 +dependencies: + - name: jupyterhub + version: "3.3.8" + repository: https://pilotdataplatform.github.io/helm-charts/ diff --git a/clusters/prod/workbench/jupyterhub/templates/_helpers.tpl b/clusters/prod/workbench/jupyterhub/templates/_helpers.tpl new file mode 100644 index 0000000..f34bae8 --- /dev/null +++ b/clusters/prod/workbench/jupyterhub/templates/_helpers.tpl @@ -0,0 +1,8 @@ +{{/* +Common labels +*/}} +{{- define "jupyterhub-stack.labels" -}} +helm.sh/chart: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} diff --git a/clusters/prod/workbench/jupyterhub/templates/keycloak-external-secret.yaml b/clusters/prod/workbench/jupyterhub/templates/keycloak-external-secret.yaml new file mode 100644 index 0000000..d6c5b96 --- /dev/null +++ b/clusters/prod/workbench/jupyterhub/templates/keycloak-external-secret.yaml @@ -0,0 +1,22 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: jupyterhub-keycloak + labels: + {{- include "jupyterhub-stack.labels" . | nindent 4 }} +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault + target: + name: jupyterhub-keycloak + data: + - secretKey: KEYCLOAK_CLIENT_ID + remoteRef: + key: secret/data/jupyterhub/{{ .Values.projectName }} + property: keycloak-client-id + - secretKey: KEYCLOAK_CLIENT_SECRET + remoteRef: + key: secret/data/jupyterhub/{{ .Values.projectName }} + property: keycloak-client-secret diff --git a/clusters/prod/workbench/jupyterhub/values.yaml b/clusters/prod/workbench/jupyterhub/values.yaml new file mode 100644 index 0000000..6e38e9f --- /dev/null +++ b/clusters/prod/workbench/jupyterhub/values.yaml @@ -0,0 +1,210 @@ +projectName: "" +domain: "hdc.ebrains.eu" + +jupyterhub: + fullnameOverride: "" + + imagePullSecrets: + - name: docker-registry-secret + + hub: + image: + name: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/jupyterhub/k8s-hub + tag: "3.3.8" + resources: + requests: + cpu: 50m + memory: 100Mi + limits: + cpu: 200m + memory: 250Mi + db: + type: sqlite-pvc + pvc: + storageClassName: csi-cinder-high-speed + storage: 1Gi + + config: + JupyterHub: + authenticator_class: oauthenticator.generic.GenericOAuthenticator + GenericOAuthenticator: + login_service: keycloak + authorize_url: https://iam.hdc.ebrains.eu/realms/hdc/protocol/openid-connect/auth + token_url: http://keycloak.keycloak/realms/hdc/protocol/openid-connect/token + userdata_url: https://iam.hdc.ebrains.eu/realms/hdc/protocol/openid-connect/userinfo + enable_auth_state: true + userdata_token_method: GET + userdata_params: + state: state + username_claim: preferred_username + allow_all: true + OAuthenticator: + scope: + - "openid" + - "profile" + - "email" + - "roles" + - "web-origins" + - "groups" + + extraEnv: + KEYCLOAK_CLIENT_ID: + valueFrom: + secretKeyRef: + name: jupyterhub-keycloak + key: KEYCLOAK_CLIENT_ID + KEYCLOAK_CLIENT_SECRET: + valueFrom: + secretKeyRef: + name: jupyterhub-keycloak + key: KEYCLOAK_CLIENT_SECRET + + extraConfig: + oauth_dynamic: | + import os + c.GenericOAuthenticator.client_id = os.environ.get("KEYCLOAK_CLIENT_ID", "") + c.GenericOAuthenticator.client_secret = os.environ.get("KEYCLOAK_CLIENT_SECRET", "") + base_url = c.JupyterHub.base_url + c.GenericOAuthenticator.oauth_callback_url = f"https://hdc.ebrains.eu{base_url}hub/oauth_callback" + spawner_config: | + c.Spawner.cmd = ['start.sh', 'jupyterhub-singleuser', '--allow-root'] + c.KubeSpawner.args = ['--allow-root'] + def notebook_dir_hook(spawner): + spawner.environment.update({ + 'NB_USER': spawner.user.name, + 'NB_UID': '1000', + 'IPYTHONDIR': '/tmp', + 'PATH': '/opt/conda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/shared', + 'base_url': 'https://hdc.ebrains.eu/', + 'url_bff': 'https://hdc.ebrains.eu/cli', + 'url_keycloak': 'https://iam.hdc.ebrains.eu/realms/hdc/protocol/openid-connect', + 'keycloak_device_client_id': 'cli' + }) + c.Spawner.pre_spawn_hook = notebook_dir_hook + + proxy: + service: + type: ClusterIP # behind ingress-nginx, no LoadBalancer needed + chp: + image: + name: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/jupyterhub/configurable-http-proxy + tag: "4.6.1" + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 200m + memory: 128Mi + # secretToken auto-generated by chart (lookup + random). No hardcoded value in public repo. + traefik: + image: + name: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/traefik + tag: "v2.11.0" + secretSync: + image: + name: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/jupyterhub/k8s-secret-sync + tag: "3.3.8" + + ingress: + enabled: true + ingressClassName: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/proxy-body-size: 20m + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/affinity: cookie + hosts: + - hdc.ebrains.eu + pathType: Prefix + tls: + - secretName: hdc.ebrains.eu-tls + hosts: + - hdc.ebrains.eu + + singleuser: + image: + name: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/jupyter/minimal-notebook + tag: "2023-10-20" + networkTools: + image: + name: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/jupyterhub/k8s-network-tools + tag: "3.3.8" + uid: 0 + extraEnv: + CHOWN_HOME: 'yes' + extraFiles: + .condarc: + mountPath: /home/{username}/.condarc + stringData: | + envs_dirs: + - $HOME/.conda_envs/ + storage: + capacity: 1Gi + homeMountPath: /home/{username} + dynamic: + storageClass: csi-cinder-high-speed + extraVolumes: + - name: jupyter-shared + persistentVolumeClaim: + claimName: shared-tools + extraVolumeMounts: + - name: jupyter-shared + mountPath: /opt/shared + readOnly: true + profileList: + - display_name: "Minimal environment" + description: "To avoid too much bells and whistles: Python." + default: true + - display_name: "Datascience environment" + description: "If you want the additional bells and whistles: Python, R, and Julia." + kubespawner_override: + image: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/jupyter/datascience-notebook:2023-10-20 + cpu: + limit: 1 + guarantee: 0.5 + memory: + limit: 4G + guarantee: 1G + cloudMetadata: + blockWithIptables: false # requires privileged initContainer + networkPolicy: + enabled: true + egress: [] + egressAllowRules: + cloudMetadataServer: false + dnsPortsCloudMetadataServer: true + dnsPortsKubeSystemNamespace: true + dnsPortsPrivateIPs: true + nonPrivateIPs: true + privateIPs: true # needed for cluster-internal API calls + + scheduling: + userScheduler: + enabled: false + image: + name: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/kube-scheduler + tag: "v1.26.15" + userPlaceholder: + image: + name: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/pause + tag: "3.9" + + prePuller: + hook: + enabled: false # skip — saves mirroring k8s-image-awaiter + image: + name: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/jupyterhub/k8s-image-awaiter + tag: "3.3.8" + continuous: + enabled: false + pause: + image: + name: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/pause + tag: "3.9" + + cull: + enabled: true + timeout: 3600 + every: 600 diff --git a/clusters/prod/workbench/project-resources/Chart.yaml b/clusters/prod/workbench/project-resources/Chart.yaml new file mode 100644 index 0000000..5d28508 --- /dev/null +++ b/clusters/prod/workbench/project-resources/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: project-resources +version: 0.1.0 diff --git a/clusters/prod/workbench/project-resources/templates/_helpers.tpl b/clusters/prod/workbench/project-resources/templates/_helpers.tpl new file mode 100644 index 0000000..4426183 --- /dev/null +++ b/clusters/prod/workbench/project-resources/templates/_helpers.tpl @@ -0,0 +1,8 @@ +{{/* +Common labels +*/}} +{{- define "project-resources.labels" -}} +helm.sh/chart: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} diff --git a/clusters/prod/workbench/project-resources/templates/docker-registry-secret.yaml b/clusters/prod/workbench/project-resources/templates/docker-registry-secret.yaml new file mode 100644 index 0000000..73182fd --- /dev/null +++ b/clusters/prod/workbench/project-resources/templates/docker-registry-secret.yaml @@ -0,0 +1,26 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: docker-registry-secret + labels: + {{- include "project-resources.labels" . | nindent 4 }} +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault + target: + name: docker-registry-secret + template: + type: kubernetes.io/dockerconfigjson + data: + .dockerconfigjson: '{"auths":{"{{ .Values.global.imageRegistry }}":{"username":"{{`{{ .username }}`}}","password":"{{`{{ .password }}`}}","auth":"{{`{{ printf "%s:%s" .username .password | b64enc }}`}}"}}}' + data: + - secretKey: username + remoteRef: + key: {{ .Values.registryVaultPath }} + property: username + - secretKey: password + remoteRef: + key: {{ .Values.registryVaultPath }} + property: password diff --git a/clusters/prod/workbench/project-resources/templates/shared-tools-pvc.yaml b/clusters/prod/workbench/project-resources/templates/shared-tools-pvc.yaml new file mode 100644 index 0000000..fcbcd8e --- /dev/null +++ b/clusters/prod/workbench/project-resources/templates/shared-tools-pvc.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: shared-tools + labels: + {{- include "project-resources.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteMany + storageClassName: nfs-client + resources: + requests: + storage: 1Gi diff --git a/clusters/prod/workbench/project-resources/values.yaml b/clusters/prod/workbench/project-resources/values.yaml new file mode 100644 index 0000000..5eba5a2 --- /dev/null +++ b/clusters/prod/workbench/project-resources/values.yaml @@ -0,0 +1 @@ +projectName: "" diff --git a/clusters/prod/workbench/projects/indoctestproject.yaml b/clusters/prod/workbench/projects/indoctestproject.yaml new file mode 100644 index 0000000..e1400f7 --- /dev/null +++ b/clusters/prod/workbench/projects/indoctestproject.yaml @@ -0,0 +1 @@ +name: indoctestproject diff --git a/clusters/prod/workbench/superset/Chart.yaml b/clusters/prod/workbench/superset/Chart.yaml new file mode 100644 index 0000000..c8fbe01 --- /dev/null +++ b/clusters/prod/workbench/superset/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: superset-stack +version: 0.1.0 +dependencies: + - name: superset + version: "0.8.8" + repository: https://pilotdataplatform.github.io/helm-charts/ diff --git a/clusters/prod/workbench/superset/templates/_helpers.tpl b/clusters/prod/workbench/superset/templates/_helpers.tpl new file mode 100644 index 0000000..8c787f0 --- /dev/null +++ b/clusters/prod/workbench/superset/templates/_helpers.tpl @@ -0,0 +1,8 @@ +{{/* +Common labels +*/}} +{{- define "superset-stack.labels" -}} +helm.sh/chart: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} diff --git a/clusters/prod/workbench/superset/templates/ingress.yaml b/clusters/prod/workbench/superset/templates/ingress.yaml new file mode 100644 index 0000000..15075e1 --- /dev/null +++ b/clusters/prod/workbench/superset/templates/ingress.yaml @@ -0,0 +1,31 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: superset + labels: + app.kubernetes.io/name: superset + {{- include "superset-stack.labels" . | nindent 4 }} + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/affinity: cookie + nginx.ingress.kubernetes.io/proxy-body-size: 20m + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, OPTIONS" + nginx.ingress.kubernetes.io/cors-allow-origin: "https://{{ .Values.domain }}" +spec: + ingressClassName: nginx + tls: + - hosts: + - {{ .Values.projectName }}-superset.{{ .Values.domain }} + secretName: {{ .Values.projectName }}-superset-tls + rules: + - host: {{ .Values.projectName }}-superset.{{ .Values.domain }} + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: superset + port: + number: 8088 diff --git a/clusters/prod/workbench/superset/templates/keycloak-external-secret.yaml b/clusters/prod/workbench/superset/templates/keycloak-external-secret.yaml new file mode 100644 index 0000000..8d8643b --- /dev/null +++ b/clusters/prod/workbench/superset/templates/keycloak-external-secret.yaml @@ -0,0 +1,26 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: superset-keycloak + labels: + {{- include "superset-stack.labels" . | nindent 4 }} +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault + target: + name: superset-keycloak + data: + - secretKey: KEYCLOAK_CLIENT_ID + remoteRef: + key: secret/data/superset/{{ .Values.projectName }} + property: keycloak-client-id + - secretKey: KEYCLOAK_SECRET + remoteRef: + key: secret/data/superset/{{ .Values.projectName }} + property: keycloak-client-secret + - secretKey: SUPERSET_SECRET_KEY + remoteRef: + key: secret/data/superset/{{ .Values.projectName }} + property: superset-secret-key diff --git a/clusters/prod/workbench/superset/values.yaml b/clusters/prod/workbench/superset/values.yaml new file mode 100644 index 0000000..b47eccf --- /dev/null +++ b/clusters/prod/workbench/superset/values.yaml @@ -0,0 +1,197 @@ +projectName: "" +domain: "hdc.ebrains.eu" + +superset: + fullnameOverride: superset + + # Upstream Apache releases, pinned here — not in versions.yaml (matches CSCS 0.8.8) + image: + repository: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/apache/superset + tag: "2.0.1" + pullPolicy: IfNotPresent + + initImage: + repository: n47w5524.c1.de1.container-registry.ovh.net/hdc-services-external/jwilder/dockerize + tag: "working-version-2017" + pullPolicy: IfNotPresent + + imagePullSecrets: + - name: docker-registry-secret + + runAsUser: 0 + + bootstrapScript: | + #!/bin/bash + rm -rf /var/lib/apt/lists/* && \ + pip install \ + psycopg2-binary==2.9.1 \ + authlib==0.15.5 \ + redis==3.5.3 && \ + if [ ! -f ~/bootstrap ]; then echo "Running Superset with uid {{ .Values.runAsUser }}" > ~/bootstrap; fi + + envFromSecrets: ["superset-keycloak"] + + configOverrides: + enable_oauth: | + import os + ENABLE_PROXY_FIX = True + + from flask_appbuilder.security.manager import AUTH_OAUTH + AUTH_TYPE = AUTH_OAUTH + OAUTH_PROVIDERS = [ + { + "name": "keycloak", + "icon": "fa-key", + "token_key": "access_token", + "remote_app": { + "client_id": os.environ.get("KEYCLOAK_CLIENT_ID"), + "client_secret": os.environ.get("KEYCLOAK_SECRET"), + "api_base_url": "https://iam.hdc.ebrains.eu/realms/hdc/protocol/", + "client_kwargs": {"scope": "email openid profile"}, + "request_token_url": None, + "access_token_url": "https://iam.hdc.ebrains.eu/realms/hdc/protocol/openid-connect/token", + "authorize_url": "https://iam.hdc.ebrains.eu/realms/hdc/protocol/openid-connect/auth", + }, + } + ] + + AUTH_ROLE_ADMIN = 'Admin' + AUTH_ROLE_PUBLIC = 'Public' + AUTH_USER_REGISTRATION = True + AUTH_USER_REGISTRATION_ROLE = "Gamma" + + session_lifetime: | + from flask import session + from flask import Flask + from datetime import timedelta + + def make_session_permanent(): + session.permanent = True + + PERMANENT_SESSION_LIFETIME = timedelta(minutes=5) + def FLASK_APP_MUTATOR(app: Flask) -> None: + app.before_request_funcs.setdefault(None, []).append(make_session_permanent) + + secret_key: | + import os + SECRET_KEY = os.environ.get("SUPERSET_SECRET_KEY", "CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET") + + # Upstream ingress disabled — host values aren't tpl-processed + ingress: + enabled: false + + init: + adminUser: + username: superset-admin + firstname: Superset + lastname: Admin + email: admin@superset.com + password: admin + initContainers: + - name: wait-for-postgres + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: "{{ .Values.initImage.pullPolicy }}" + envFrom: + - secretRef: + name: "{{ tpl .Values.envFromSecret . }}" + command: + - /usr/local/bin/dockerize + args: + - -wait + - "tcp://$(DB_HOST):$(DB_PORT)" + - -timeout + - 120s + + supersetNode: + initContainers: + - name: wait-for-postgres + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: "{{ .Values.initImage.pullPolicy }}" + envFrom: + - secretRef: + name: "{{ tpl .Values.envFromSecret . }}" + command: + - /usr/local/bin/dockerize + args: + - -wait + - tcp://$(DB_HOST):$(DB_PORT) + resources: + requests: + cpu: 10m + memory: 128Mi + limits: + cpu: "1" + memory: 1Gi + + supersetWorker: + initContainers: + - name: wait-for-postgres-redis + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: "{{ .Values.initImage.pullPolicy }}" + envFrom: + - secretRef: + name: "{{ tpl .Values.envFromSecret . }}" + command: + - /usr/local/bin/dockerize + - -wait + - tcp://$(DB_HOST):$(DB_PORT) + - -wait + - tcp://$(REDIS_HOST):$(REDIS_PORT) + resources: + requests: + cpu: 10m + memory: 128Mi + limits: + cpu: 500m + memory: 1Gi + + supersetCeleryBeat: + enabled: false + + supersetCeleryFlower: + enabled: false + + supersetWebsockets: + enabled: false + + postgresql: + enabled: true + global: + imageRegistry: n47w5524.c1.de1.container-registry.ovh.net + imagePullSecrets: + - docker-registry-secret + image: + repository: hdc-services-external/bitnami/postgresql + primary: + persistence: + size: 2Gi + storageClass: "csi-cinder-high-speed" + resources: + requests: + cpu: 10m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + + redis: + enabled: true + global: + imageRegistry: n47w5524.c1.de1.container-registry.ovh.net + imagePullSecrets: + - docker-registry-secret + image: + repository: hdc-services-external/bitnami/redis + architecture: standalone + auth: + enabled: false + master: + persistence: + enabled: false + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 250m + memory: 128Mi