diff --git a/.gitignore b/.gitignore index 0ec82f551..288255c76 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ yarn-error.log* conda-store.sqlite *.lockb +*.local.yaml diff --git a/conda-store-server/conda_store_server/_internal/server/app.py b/conda-store-server/conda_store_server/_internal/server/app.py index ba0a0de9a..2e86caf13 100644 --- a/conda-store-server/conda_store_server/_internal/server/app.py +++ b/conda-store-server/conda_store_server/_internal/server/app.py @@ -261,7 +261,7 @@ async def http_exception_handler(request, exc): return JSONResponse( { "status": "error", - "message": exc.detail, + "message": exc, }, status_code=exc.status_code, ) diff --git a/conda-store-server/install/conda-store/.helmignore b/conda-store-server/install/conda-store/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/conda-store-server/install/conda-store/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/conda-store-server/install/conda-store/Chart.yaml b/conda-store-server/install/conda-store/Chart.yaml new file mode 100644 index 000000000..f1ecd8d24 --- /dev/null +++ b/conda-store-server/install/conda-store/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: conda-store +description: A Helm chart for deploying conda-store to kubernetes +type: application +version: 0.1.0 +icon: https://raw.githubusercontent.com/conda-incubator/conda-store/refs/heads/main/docusaurus-docs/community/images/logos/conda-store-logo-vertical-lockup.svg +home: https://github.com/conda-incubator/conda-store diff --git a/conda-store-server/install/conda-store/README.md b/conda-store-server/install/conda-store/README.md new file mode 100644 index 000000000..9083c4f8d --- /dev/null +++ b/conda-store-server/install/conda-store/README.md @@ -0,0 +1,78 @@ +# Conda Store Helm Chart + +In order to install conda store with helm, you must +have the following things set up: + +## Services +* S3/minio +* DB (eg. Postgres, MySql, etc) +* Redis + +## Secrets +* following secrets: + * conda-store-postgres-secret + * conda-store-minio-secret + * conda-store-redis-secret + +For example +``` +--- +apiVersion: v1 +data: + username: YWRtaW4= + pasword: cGFzc3dvcmQ= +kind: Secret +metadata: + name: conda-store-postgres-secret +type: Opaque + +--- +apiVersion: v1 +data: + username: YWRtaW4= + pasword: cGFzc3dvcmQ= +kind: Secret +metadata: + name: conda-store-minio-secret +type: Opaque + +--- +apiVersion: v1 +data: + pasword: cGFzc3dvcmQ= +kind: Secret +metadata: + name: conda-store-redis-secret +type: Opaque +``` + +## Volumes + +Conda store requires a persistent volume for installing conda environments into. Users should provide a PersistentVolume that binds to the conda-store-worker PVC. For example: + +``` +apiVersion: v1 +kind: PersistentVolume +metadata: + name: conda-store-environments +spec: + capacity: + storage: 2Gi + accessModes: + - ReadWriteMany + volumeMode: Filesystem + storageClassName: local-storage + local: # required for local-storage + path: /tmp/mnt-vol-kub + claimRef: + namespace: conda-store + name: conda-store-worker-claim + nodeAffinity: # required for local-storage + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - mynode +``` diff --git a/conda-store-server/install/conda-store/templates/_helpers.tpl b/conda-store-server/install/conda-store/templates/_helpers.tpl new file mode 100644 index 000000000..893ae820f --- /dev/null +++ b/conda-store-server/install/conda-store/templates/_helpers.tpl @@ -0,0 +1,78 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "condaStore.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "condaStore.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "condaStore.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "condaStore.labels" -}} +helm.sh/chart: {{ include "condaStore.chart" . }} +{{ include "condaStore.selectorLabels" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "condaStore.selectorLabels" -}} +app.kubernetes.io/name: {{ include "condaStore.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Set's the worker resources if the user has set any. +*/}} +{{- define "worker.resources" -}} + {{- if .Values.worker.resources -}} + resources: +{{ toYaml .Values.worker.resources | indent 12}} + {{ end }} +{{- end -}} + +{{/* +Set's the uiServer resources if the user has set any. +*/}} +{{- define "uiServer.resources" -}} + {{- if .Values.uiServer.resources -}} + resources: +{{ toYaml .Values.uiServer.resources | indent 12}} + {{ end }} +{{- end -}} + +{{/* +Set's the apiServer resources if the user has set any. +*/}} +{{- define "apiServer.resources" -}} + {{- if .Values.apiServer.resources -}} + resources: +{{ toYaml .Values.apiServer.resources | indent 12}} + {{ end }} +{{- end -}} diff --git a/conda-store-server/install/conda-store/templates/api-server-config-map.yaml b/conda-store-server/install/conda-store/templates/api-server-config-map.yaml new file mode 100644 index 000000000..74d26b5d6 --- /dev/null +++ b/conda-store-server/install/conda-store/templates/api-server-config-map.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +data: + conda_store_config.py: | + {{ .Values.apiServer.condaStoreConfig | nindent 4 }} +kind: ConfigMap +metadata: + name: "{{ template "condaStore.fullname" . }}-api-server" + namespace: {{ .Values.global.namespace }} + labels: + {{- include "condaStore.labels" . | nindent 8 }} diff --git a/conda-store-server/install/conda-store/templates/api-server-deployment.yaml b/conda-store-server/install/conda-store/templates/api-server-deployment.yaml new file mode 100644 index 000000000..75c923484 --- /dev/null +++ b/conda-store-server/install/conda-store/templates/api-server-deployment.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ template "condaStore.fullname" . }}-api-server" + namespace: {{ .Values.global.namespace }} + labels: + app: "{{ template "condaStore.fullname" . }}-api-server" +{{ include "condaStore.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.apiServer.replicas }} + selector: + matchLabels: + app: "{{ template "condaStore.fullname" . }}-api-server" + template: + metadata: + labels: + app: "{{ template "condaStore.fullname" . }}-api-server" +{{ include "condaStore.labels" . | nindent 8 }} + spec: + containers: + - name: "{{ template "condaStore.fullname" . }}-api-server" + image: "{{ .Values.apiServer.image.repository }}:{{ .Values.apiServer.image.tag }}" + imagePullPolicy: {{ .Values.apiServer.image.pullPolicy }} + args: + - "conda-store-server" + - "--config" + - "/etc/conda-store/conda_store_config.py" + ports: + - containerPort: {{ .Values.apiServer.port }} + livenessProbe: + httpGet: + path: /api/v1/ + port: 8080 + initialDelaySeconds: {{ .Values.apiServer.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.apiServer.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.apiServer.livenessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.apiServer.livenessProbe.failureThreshold }} + readinessProbe: + httpGet: + path: /api/v1/ + port: 8080 + initialDelaySeconds: {{ .Values.apiServer.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.apiServer.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.apiServer.readinessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.apiServer.readinessProbe.failureThreshold }} + {{ template "apiServer.resources" . }} + env: + - name: POSTGRES_USERNAME + valueFrom: + secretKeyRef: + name: conda-store-postgres-secret + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: conda-store-postgres-secret + key: password + - name: MINIO_USERNAME + valueFrom: + secretKeyRef: + name: conda-store-minio-secret + key: username + - name: MINIO_PASSWORD + valueFrom: + secretKeyRef: + name: conda-store-minio-secret + key: password + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: conda-store-redis-secret + key: password + volumeMounts: + - name: config + mountPath: "/etc/conda-store/" + readOnly: true + restartPolicy: Always + volumes: + - name: config + configMap: + name: "{{ template "condaStore.fullname" . }}-api-server" diff --git a/conda-store-server/install/conda-store/templates/api-server-service.yaml b/conda-store-server/install/conda-store/templates/api-server-service.yaml new file mode 100644 index 000000000..783187cb2 --- /dev/null +++ b/conda-store-server/install/conda-store/templates/api-server-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ template "condaStore.fullname" . }}-api-server" + namespace: {{ .Values.global.namespace }} + labels: +{{ include "condaStore.labels" . | nindent 4 }} +spec: + type: NodePort + ports: + - port: 8080 + nodePort: {{ .Values.apiServer.nodePort }} + targetPort: {{ .Values.apiServer.port }} + protocol: TCP + name: http + selector: + app: "{{ template "condaStore.fullname" . }}-api-server" diff --git a/conda-store-server/install/conda-store/templates/ui-server-deployment.yaml b/conda-store-server/install/conda-store/templates/ui-server-deployment.yaml new file mode 100644 index 000000000..3e73309f9 --- /dev/null +++ b/conda-store-server/install/conda-store/templates/ui-server-deployment.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ template "condaStore.fullname" . }}-ui-server" + namespace: {{ .Values.global.namespace }} + labels: +{{ include "condaStore.labels" . | nindent 4 }} + app: "{{ template "condaStore.fullname" . }}-ui-server" +spec: + replicas: {{ .Values.uiServer.replicas }} + selector: + matchLabels: + app: "{{ template "condaStore.fullname" . }}-ui-server" + template: + metadata: + labels: +{{ include "condaStore.labels" . | nindent 8 }} + app: "{{ template "condaStore.fullname" . }}-ui-server" + spec: + containers: + - name: "{{ template "condaStore.fullname" . }}-ui-server" + image: "{{ .Values.uiServer.image.repository }}:{{ .Values.uiServer.image.tag }}" + imagePullPolicy: {{ .Values.uiServer.image.pullPolicy }} + env: + - name: REACT_APP_STYLE_TYPE + value: green-accent + - name: REACT_APP_API_URL + value: {{ .Values.uiServer.appEnv.apiUrl }} + - name: REACT_APP_LOGIN_PAGE_URL + value: {{ .Values.uiServer.appEnv.loginPageUrl }} + - name: REACT_APP_LOGOUT_PAGE_URL + value: {{ .Values.uiServer.appEnv.logoutPageUrl }} + - name: REACT_APP_URL_BASENAME + value: {{ .Values.uiServer.appEnv.urlBasename }} + - name: DEBUG + value: express:* + - name: PORT + value: "8080" + ports: + - containerPort: {{ .Values.uiServer.port }} + livenessProbe: + httpGet: + # TODO: it would be nice to have a "ok" endpoint on the ui + path: /status + port: 8080 + initialDelaySeconds: {{ .Values.uiServer.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.uiServer.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.uiServer.livenessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.uiServer.livenessProbe.failureThreshold }} + readinessProbe: + httpGet: + path: /status + port: 8080 + initialDelaySeconds: {{ .Values.uiServer.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.uiServer.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.uiServer.readinessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.uiServer.readinessProbe.failureThreshold }} + {{ template "uiServer.resources" . }} + restartPolicy: Always diff --git a/conda-store-server/install/conda-store/templates/ui-server-service.yaml b/conda-store-server/install/conda-store/templates/ui-server-service.yaml new file mode 100644 index 000000000..53bb0f925 --- /dev/null +++ b/conda-store-server/install/conda-store/templates/ui-server-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ template "condaStore.fullname" . }}-ui-server" + namespace: {{ .Values.global.namespace }} + labels: +{{ include "condaStore.labels" . | nindent 4 }} +spec: + type: NodePort + ports: + - port: 8080 + nodePort: {{ .Values.uiServer.nodePort }} + targetPort: {{ .Values.uiServer.port }} + protocol: TCP + name: http + selector: + app: "{{ template "condaStore.fullname" . }}-ui-server" diff --git a/conda-store-server/install/conda-store/templates/worker-config-map.yaml b/conda-store-server/install/conda-store/templates/worker-config-map.yaml new file mode 100644 index 000000000..df89da045 --- /dev/null +++ b/conda-store-server/install/conda-store/templates/worker-config-map.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +data: + conda_store_config.py: | + {{ .Values.worker.condaStoreConfig | nindent 4 }} +kind: ConfigMap +metadata: + name: "{{ template "condaStore.fullname" . }}-worker" + namespace: {{ .Values.global.namespace }} + labels: + {{- include "condaStore.labels" . | nindent 8 }} diff --git a/conda-store-server/install/conda-store/templates/worker-deployment.yaml b/conda-store-server/install/conda-store/templates/worker-deployment.yaml new file mode 100644 index 000000000..facde410d --- /dev/null +++ b/conda-store-server/install/conda-store/templates/worker-deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ template "condaStore.fullname" . }}-worker" + namespace: {{ .Values.global.namespace }} + labels: +{{ include "condaStore.labels" . | nindent 4 }} + app: "{{ template "condaStore.fullname" . }}-worker" +spec: + replicas: {{ .Values.worker.replicas }} + selector: + matchLabels: + app: "{{ template "condaStore.fullname" . }}-worker" + template: + metadata: + labels: +{{ include "condaStore.labels" . | nindent 8 }} + app: "{{ template "condaStore.fullname" . }}-worker" + spec: + containers: + - name: "{{ template "condaStore.fullname" . }}-worker" + image: "{{ .Values.worker.image.repository }}:{{ .Values.worker.image.tag }}" + imagePullPolicy: {{ .Values.worker.image.pullPolicy }} + args: + - "conda-store-worker" + - "--config" + - "/etc/conda-store/conda_store_config.py" + ports: + - containerPort: {{ .Values.worker.port }} + {{ template "worker.resources" . }} + env: + - name: POSTGRES_USERNAME + valueFrom: + secretKeyRef: + name: conda-store-postgres-secret + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: conda-store-postgres-secret + key: password + - name: MINIO_USERNAME + valueFrom: + secretKeyRef: + name: conda-store-minio-secret + key: username + - name: MINIO_PASSWORD + valueFrom: + secretKeyRef: + name: conda-store-minio-secret + key: password + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: conda-store-redis-secret + key: password + volumeMounts: + - name: config + mountPath: "/etc/conda-store/" + readOnly: true + - name: storage + mountPath: "/opt/conda-store/" + securityContext: + runAsUser: 0 + runAsGroup: 0 + restartPolicy: Always + volumes: + - name: config + configMap: + name: "{{ template "condaStore.fullname" . }}-worker" + - name: storage + persistentVolumeClaim: + claimName: "{{ template "condaStore.fullname" . }}-worker-claim" diff --git a/conda-store-server/install/conda-store/templates/worker-pv-claim.yaml b/conda-store-server/install/conda-store/templates/worker-pv-claim.yaml new file mode 100644 index 000000000..5d4d6e748 --- /dev/null +++ b/conda-store-server/install/conda-store/templates/worker-pv-claim.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + helm.sh/chart: {{ include "condaStore.chart" . }} + app.kubernetes.io/name: {{ include "condaStore.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + name: "{{ template "condaStore.fullname" . }}-worker-claim" + namespace: {{ .Values.global.namespace }} +spec: + resources: + requests: + storage: {{ .Values.worker.environmentsVolumeSize }} + accessModes: + - ReadWriteMany + storageClassName: "" + volumeName: {{ .Values.worker.environmentsVolume }} diff --git a/conda-store-server/install/conda-store/values.yaml b/conda-store-server/install/conda-store/values.yaml new file mode 100644 index 000000000..f2c3128c4 --- /dev/null +++ b/conda-store-server/install/conda-store/values.yaml @@ -0,0 +1,204 @@ +podAnnotations: {} +podLabels: {} + +global: + # The namespace to deploy to. Defaults to the `helm` installation namespace. + namespace: "" + +apiServer: + # Node port for the API Server K8 service + nodePort: 30501 + + # Port the service will be accessible on. To port forward this service + # try `kubectl port-forward -n conda-store service/conda-store-api-server :8080` + port: 8081 + + # Number of API Servers replica pods to start up + replicas: 1 + + # Image to use for the api server + image: + repository: quansight/conda-store-server + pullPolicy: IfNotPresent + tag: 2024.6.1 + + # Config for the conda-store-server. For all available options, see the + # docs at https://conda.store/conda-store/references/configuration-options + # The following env vars are injected into the container if required: + # * POSTGRES_USERNAME + # * POSTGRES_PASSWORD + # * MINIO_USERNAME + # * MINIO_PASSWORD + # * REDIS_PASSWORD + # Provided is a good starting point for the config + condaStoreConfig: | + c.CondaStoreServer.enable_ui = False + c.CondaStoreServer.enable_api = True + c.CondaStoreServer.enable_registry = False + c.CondaStoreServer.address = "0.0.0.0" + c.CondaStoreServer.port = 8080 + c.CondaStoreServer.url_prefix = "/" + + c.CondaStore.database_url = f"postgresql+psycopg2://{os.environ.get('POSTGRES_USERNAME')}:{os.environ.get('POSTGRES_PASSWORD')}@postgres/conda-store" + c.CondaStore.redis_url = ( + f"redis://:{os.environ.get('REDIS_PASSWORD')}@redis:6379/0" + ) + c.CondaStore.default_uid = 1000 + c.CondaStore.default_gid = 100 + c.CondaStore.default_permissions = "555" + c.CondaStore.storage_class = S3Storage + + c.S3Storage.internal_endpoint = "minio:9000" + c.S3Storage.internal_secure = False + c.S3Storage.external_endpoint = "localhost:9000" + c.S3Storage.external_secure = False + c.S3Storage.access_key = os.environ.get('MINIO_USERNAME') + c.S3Storage.secret_key = os.environ.get('MINIO_PASSWORD') + + c.RBACAuthorizationBackend.role_mappings_version = 2 + + # This is to setup the liveness and readiness probes more information can be + # found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + livenessProbe: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + + resources: {} + # resources: + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +uiServer: + # Node port for the UI Server K8 service + nodePort: 30500 + + # Port the service will be accessible on. To port forward this service + # try `kubectl port-forward -n conda-store service/conda-store-ui-server :8080` + port: 8080 + + # Number of API Servers replica pods to start up + replicas: 1 + + # Image to use for the ui server + image: + repository: quansight/conda-store-server + pullPolicy: IfNotPresent + tag: 2024.6.1 + + appEnv: + # publically accessible URL for the conda store server api instance + apiUrl: + + # publically accessible URL to access the login page + loginPageUrl: + + # publically accessible URL to access the logout page + logoutPageUrl: + + # base url prefix to access the api. Should match the api server config option + # `c.CondaStoreServer.url_prefix` + urlBasename: "/" + + # This is to setup the liveness and readiness probes more information can be + # found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + livenessProbe: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + + resources: {} + # resources: + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +worker: + # Number of workers that are to be in the worker deployment + replicas: 2 + + # Image for the worker container + image: + repository: quansight/conda-store-server + pullPolicy: IfNotPresent + tag: 2024.6.1 + + # Volume attached to the worker volumes that contains conda environments + # and packages. This will bind to the worker with a PVC. To bind your + # PV to the PVC ensure to set the claim ref on your volume. For example + # ``` + # spec: + # accessModes: + # - ReadWriteMany + # storageClassName: "" + # claimRef: + # name: conda-store-worker-claim + # ``` + environmentsVolume: "" + + # Size of the environmentsVolume + environmentsVolumeSize: 2Gi + + # Config for the conda-store-server. For all available options, see the + # docs at https://conda.store/conda-store/references/configuration-options + # The following env vars are injected into the container if required: + # * POSTGRES_USERNAME + # * POSTGRES_PASSWORD + # * MINIO_USERNAME + # * MINIO_PASSWORD + # * REDIS_PASSWORD + # Provided is a good starting point for the config + condaStoreConfig: | + c.CondaStore.database_url = f"postgresql+psycopg2://{os.environ.get('POSTGRES_USERNAME')}:{os.environ.get('POSTGRES_PASSWORD')}@postgres/conda-store" + c.CondaStore.redis_url = ( + f"redis://:{os.environ.get('REDIS_PASSWORD')}@redis:6379/0" + ) + c.CondaStore.default_uid = 1000 + c.CondaStore.default_gid = 100 + c.CondaStore.default_permissions = "555" + c.CondaStore.storage_class = S3Storage + c.CondaStore.store_directory = "/opt/conda-store/conda-store/" + + c.S3Storage.internal_endpoint = "minio:9000" + c.S3Storage.internal_secure = False + c.S3Storage.external_endpoint = "localhost:9000" + c.S3Storage.external_secure = False + c.S3Storage.access_key = os.environ.get('MINIO_USERNAME') + c.S3Storage.secret_key = os.environ.get('MINIO_PASSWORD') + + c.CondaStoreServer.enable_ui = False + c.CondaStoreServer.enable_api = False + c.CondaStoreServer.enable_registry = False + c.CondaStoreServer.enable_metrics = False + + c.CondaStoreWorker.log_level = logging.INFO + c.CondaStoreWorker.watch_paths = ["/opt/environments"] + c.CondaStoreWorker.concurrency = 4 + + resources: {} + # resources: + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi diff --git a/examples/helm/README.md b/examples/helm/README.md new file mode 100644 index 000000000..56b0e66e0 --- /dev/null +++ b/examples/helm/README.md @@ -0,0 +1,104 @@ +# Install conda-store with Helm + +## Setup + +### Setup cluster and install dependencies + +Start a minkiube cluster + +``` +$ minikube start +``` + +Install the dependencies including: +* minio +* postgres +* redis +* setup secrets + +``` +$ kubectl create namespace conda-store +$ kubectl apply -k dependencies +``` + +View your new service: +``` +$ kubectl get po -n conda-store +``` + +View your new secrets: +``` +$ kubectl get secret -n conda-store +``` + +## Generate vaules.local.yaml + +Use gomplate to generate the values.local.yaml file. + +If you don't already have gomplate installed, follow [these install instructions](https://docs.gomplate.ca/installing/) + +Then, generate the file + +``` +$ cd tmpl + +$ gomplate -d api-server-config.py -d ui-server-config.py -d worker-config.py -d defaults.yaml -f values.local.yaml.tmpl -o ../values.local.yaml +``` + +## Helm install + +``` +$ helm install conda-store ../../conda-store-server/install/conda-store/ --values values.local.yaml +``` + +Expose the required ports +``` +# conda-store UI +$ kubectl port-forward -n conda-store service/conda-store-ui-server 8080:8080 + +# conda-store api +$ kubectl port-forward -n conda-store service/conda-store-api-server 8081:8080 + +# minio +$ kubectl port-forward -n conda-store service/minio 9000:9000 +``` + +### Check services are up + +Check the deployments +``` +$ kubectl get deployment -n conda-store +NAME READY UP-TO-DATE AVAILABLE AGE +conda-store-api-server 1/1 1 1 15s +conda-store-ui-server 1/1 1 1 15s +conda-store-worker 1/1 1 1 15s +minio 1/1 1 1 37h +postgres 1/1 1 1 37h +redis 1/1 1 1 37h +``` + +Curl the endpoints +``` +# api server +$ curl http://localhost:8081/api/v1/ +{"status":"ok","data":{"version":"2024.6.1"},"message":null}% + +# ui server +$ curl http://localhost:8080 + + + + . . . +``` + +### Helpful commands for inspecting the system + +Connect to the database pod with psql +``` +$ kub exec --stdin --tty postgres-68cd794f77-z7qzk -n conda-store -- psql -U admin conda-store +``` + +Connect to the worker pod with a shell +``` +$ kub exec --stdin --tty conda-store-worker-9cc75dd7d-87nz8 -n conda-store -- /bin/bash +``` diff --git a/examples/helm/dependencies/kustomization.yaml b/examples/helm/dependencies/kustomization.yaml new file mode 100644 index 000000000..25cec1ee6 --- /dev/null +++ b/examples/helm/dependencies/kustomization.yaml @@ -0,0 +1,13 @@ +# Copyright (c) conda-store development team. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: conda-store +resources: + - minio.yaml + - postgres.yaml + - redis.yaml + - secrets.yaml + - volume-local.yaml diff --git a/examples/helm/dependencies/minio.yaml b/examples/helm/dependencies/minio.yaml new file mode 100644 index 000000000..633e52498 --- /dev/null +++ b/examples/helm/dependencies/minio.yaml @@ -0,0 +1,70 @@ +# Copyright (c) conda-store development team. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +apiVersion: v1 +kind: Service +metadata: + name: minio +spec: + type: NodePort + ports: + - name: "9000" + nodePort: 30900 + port: 9000 + targetPort: 9000 + selector: + app: minio +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: minio-claim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: minio + labels: + app: minio +spec: + replicas: 1 + selector: + matchLabels: + app: minio + strategy: + type: Recreate + template: + metadata: + labels: + app: minio + spec: + containers: + - name: minio + image: minio/minio:RELEASE.2021-08-25T00-41-18Z + args: + - "-c" + - "mkdir -p /data/conda-store && /usr/bin/minio server /data" + command: + - "sh" + env: + - name: MINIO_ACCESS_KEY + value: admin + - name: MINIO_SECRET_KEY + value: password + ports: + - containerPort: 9000 + volumeMounts: + - mountPath: /data + name: minio-claim + restartPolicy: Always + volumes: + - name: minio-claim + persistentVolumeClaim: + claimName: minio-claim diff --git a/examples/helm/dependencies/postgres.yaml b/examples/helm/dependencies/postgres.yaml new file mode 100644 index 000000000..a6a230df8 --- /dev/null +++ b/examples/helm/dependencies/postgres.yaml @@ -0,0 +1,69 @@ +# Copyright (c) conda-store development team. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +apiVersion: v1 +kind: Service +metadata: + labels: + app: postgres + name: postgres +spec: + ports: + - name: "5432" + port: 5432 + targetPort: 5432 + selector: + app: postgres +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-claim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + labels: + app: postgres +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + strategy: + type: Recreate + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:13 + args: + - postgres + env: + - name: POSTGRES_DB + value: conda-store + - name: POSTGRES_PASSWORD + value: password + - name: POSTGRES_USER + value: admin + ports: + - containerPort: 5432 + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: postgres-claim + restartPolicy: Always + volumes: + - name: postgres-claim + persistentVolumeClaim: + claimName: postgres-claim diff --git a/examples/helm/dependencies/redis.yaml b/examples/helm/dependencies/redis.yaml new file mode 100644 index 000000000..759a74613 --- /dev/null +++ b/examples/helm/dependencies/redis.yaml @@ -0,0 +1,63 @@ +# Copyright (c) conda-store development team. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +apiVersion: v1 +kind: Service +metadata: + labels: + app: redis + name: redis +spec: + ports: + - name: "6379" + port: 6379 + targetPort: 6379 + selector: + app: redis +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: redis-claim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + labels: + app: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + strategy: + type: Recreate + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: bitnami/redis:6.0.16 + env: + - name: REDIS_PASSWORD + value: password + ports: + - containerPort: 6379 + volumeMounts: + - mountPath: /bitnami/redis/data + name: redis-claim + restartPolicy: Always + volumes: + - name: redis-claim + persistentVolumeClaim: + claimName: redis-claim diff --git a/examples/helm/dependencies/secrets.yaml b/examples/helm/dependencies/secrets.yaml new file mode 100644 index 000000000..6bece5c44 --- /dev/null +++ b/examples/helm/dependencies/secrets.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: v1 +data: + username: YWRtaW4= + password: cGFzc3dvcmQ= +kind: Secret +metadata: + name: conda-store-postgres-secret +type: Opaque + +--- +apiVersion: v1 +data: + username: YWRtaW4= + password: cGFzc3dvcmQ= +kind: Secret +metadata: + name: conda-store-minio-secret +type: Opaque + +--- +apiVersion: v1 +data: + password: cGFzc3dvcmQ= +kind: Secret +metadata: + name: conda-store-redis-secret +type: Opaque diff --git a/examples/helm/dependencies/volume-local.yaml b/examples/helm/dependencies/volume-local.yaml new file mode 100644 index 000000000..5b69a2922 --- /dev/null +++ b/examples/helm/dependencies/volume-local.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: conda-store-environments +spec: + capacity: + storage: 2Gi + accessModes: + - ReadWriteMany + hostPath: + path: /tmp/hostpath-provisioner/conda-store/conda-store-worker-environments + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + claimRef: + namespace: conda-store + name: conda-store-worker-claim diff --git a/examples/helm/tmpl/api-server-config.py b/examples/helm/tmpl/api-server-config.py new file mode 100644 index 000000000..9ba1e27e8 --- /dev/null +++ b/examples/helm/tmpl/api-server-config.py @@ -0,0 +1,64 @@ +import logging +import os + +from conda_store_server.server.auth import DummyAuthentication +from conda_store_server.storage import S3Storage + +# ================================== +# conda-store settings +# ================================== +c.CondaStore.storage_threshold = 1024 +c.CondaStore.storage_class = S3Storage +c.CondaStore.store_directory = "/opt/conda-store/conda-store/" +c.CondaStore.database_url = f"postgresql+psycopg2://{os.environ.get('POSTGRES_USERNAME')}:{os.environ.get('POSTGRES_PASSWORD')}@postgres/conda-store" +c.CondaStore.redis_url = ( + f"redis://:{os.environ.get('REDIS_PASSWORD')}@redis:6379/0" +) +c.CondaStore.default_uid = 1000 +c.CondaStore.default_gid = 100 +c.CondaStore.default_permissions = "555" +c.CondaStore.conda_included_packages = ["ipykernel"] +c.CondaStore.default_namespace = "global" +c.CondaStore.filesystem_namespace = "conda-store" +c.CondaStore.conda_allowed_channels = [] # allow all channels +c.CondaStore.conda_indexed_channels = [ + "main", + "conda-forge", + "https://repo.anaconda.com/pkgs/main", +] + +# ================================== +# s3 settings +# ================================== +c.S3Storage.internal_endpoint = "minio:9000" +c.S3Storage.internal_secure = False +c.S3Storage.external_endpoint = "localhost:9000" +c.S3Storage.external_secure = False +c.S3Storage.access_key = os.environ.get('MINIO_USERNAME') +c.S3Storage.secret_key = os.environ.get('MINIO_PASSWORD') +c.S3Storage.region = "us-east-1" # minio region default +c.S3Storage.bucket_name = "conda-store" + +c.RBACAuthorizationBackend.role_mappings_version = 2 + +# ================================== +# server settings +# ================================== +c.CondaStoreServer.log_level = logging.INFO +c.CondaStoreServer.log_format = ( + "%(asctime)s %(levelname)9s %(name)s:%(lineno)4s: %(message)s" +) +c.CondaStoreServer.enable_ui = True +c.CondaStoreServer.enable_api = True +c.CondaStoreServer.enable_registry = False +c.CondaStoreServer.enable_metrics = True +c.CondaStoreServer.address = "0.0.0.0" +c.CondaStoreServer.port = 8080 +c.CondaStoreServer.behind_proxy = True +# This MUST start with `/` +c.CondaStoreServer.url_prefix = "/" + +# ================================== +# auth settings +# ================================== +c.CondaStoreServer.authentication_class = DummyAuthentication diff --git a/examples/helm/tmpl/defaults.yaml b/examples/helm/tmpl/defaults.yaml new file mode 100644 index 000000000..983ef651d --- /dev/null +++ b/examples/helm/tmpl/defaults.yaml @@ -0,0 +1,4 @@ +image: + repository: quansight/conda-store-server + pullPolicy: IfNotPresent + tag: 2024.11.1 diff --git a/examples/helm/tmpl/values.local.yaml.tmpl b/examples/helm/tmpl/values.local.yaml.tmpl new file mode 100644 index 000000000..6744bd00d --- /dev/null +++ b/examples/helm/tmpl/values.local.yaml.tmpl @@ -0,0 +1,38 @@ +global: + namespace: conda-store + +apiServer: + nodePort: 30501 + port: 8080 + replicas: 1 + image: + repository: {{ (datasource "defaults").image.repository }} + pullPolicy: {{ (datasource "defaults").image.pullPolicy }} + tag: {{ (datasource "defaults").image.tag }} + condaStoreConfig: | +{{ include "api-server-config" | indent 4 }} + +uiServer: + nodePort: 30500 + port: 8080 + replicas: 1 + image: + repository: conda-store-server + pullPolicy: Never + tag: v1 + appEnv: + apiUrl: http://localhost:8081 + loginPageUrl: http://localhost:8081/login?next= + logoutPageUrl: http://localhost:8081/logout?next=/ + +worker: + port: 8080 + replicas: 1 + image: + repository: {{ (datasource "defaults").image.repository }} + pullPolicy: {{ (datasource "defaults").image.pullPolicy }} + tag: {{ (datasource "defaults").image.tag }} + environmentsVolume: conda-store-environments + environmentsVolumeSize: 2Gi + condaStoreConfig: | +{{ include "worker-config" | indent 4 }} diff --git a/examples/helm/tmpl/worker-config.py b/examples/helm/tmpl/worker-config.py new file mode 100644 index 000000000..a6d891c10 --- /dev/null +++ b/examples/helm/tmpl/worker-config.py @@ -0,0 +1,49 @@ +import logging +import os + +from conda_store_server.storage import S3Storage + +# ================================== +# conda-store settings +# ================================== +c.CondaStore.storage_threshold = 1024 +c.CondaStore.storage_class = S3Storage +c.CondaStore.store_directory = "/opt/conda-store/conda-store/" +c.CondaStore.database_url = f"postgresql+psycopg2://{os.environ.get('POSTGRES_USERNAME')}:{os.environ.get('POSTGRES_PASSWORD')}@postgres/conda-store" +c.CondaStore.redis_url = ( + f"redis://:{os.environ.get('REDIS_PASSWORD')}@redis:6379/0" +) +c.CondaStore.default_uid = 1000 +c.CondaStore.default_gid = 100 +c.CondaStore.default_permissions = "555" +c.CondaStore.conda_included_packages = ["ipykernel"] +c.CondaStore.environment_directory = "{store_directory}/{namespace}/envs/{namespace}-{name}" +c.CondaStore.default_namespace = "global" +c.CondaStore.filesystem_namespace = "conda-store" +c.CondaStore.conda_allowed_channels = [] # allow all channels +c.CondaStore.conda_indexed_channels = [ + "main", + "conda-forge", + "https://repo.anaconda.com/pkgs/main", +] + +# ================================== +# s3 settings +# ================================== +c.S3Storage.internal_endpoint = "minio:9000" +c.S3Storage.internal_secure = False +c.S3Storage.external_endpoint = "localhost:9000" +c.S3Storage.external_secure = False +c.S3Storage.access_key = os.environ.get('MINIO_USERNAME') +c.S3Storage.secret_key = os.environ.get('MINIO_PASSWORD') +c.S3Storage.region = "us-east-1" # minio region default +c.S3Storage.bucket_name = "conda-store" + +c.RBACAuthorizationBackend.role_mappings_version = 2 + +# ================================== +# worker settings +# ================================== +c.CondaStoreWorker.log_level = logging.INFO +c.CondaStoreWorker.watch_paths = ["/opt/environments"] +c.CondaStoreWorker.concurrency = 4