diff --git a/applications/backoffice/src/pages/HomePage.tsx b/applications/backoffice/src/pages/HomePage.tsx index 9cd8ddd5..3b0995af 100644 --- a/applications/backoffice/src/pages/HomePage.tsx +++ b/applications/backoffice/src/pages/HomePage.tsx @@ -29,6 +29,13 @@ export default (props: any) => { const [ repositories, setRepositories ] = React.useState(null); const [ error, setError ] = React.useState(null); + let realm = "osb2" + if (window.location.hostname.includes("local")) { + realm = "osblocal" + } else if (window.location.hostname.includes("dev")) { + realm = "osb2dev" + } + const fetchInfo = () => { // Initialise APIs with token const token = Cookies.get('accessToken'); @@ -83,17 +90,10 @@ export default (props: any) => { } } - const getKeyCloakProfile = () => { - const hostname = window.location.hostname; - let realm = "osbv2" - if (hostname.includes("local")) { - realm = "osblocal" - } - else if (hostname.includes("dev")) { - realm = "osb2dev" - } + const keycloakBaseUrl = React.useMemo(() => { + return "/auth/admin/master/console/#/realms/" + realm + "/users/"; - } + }, [realm]); // for links to profiles const osbProfile = "/user/"; @@ -118,7 +118,12 @@ export default (props: any) => { const dataColumns: GridColDef[] = [ { - field: 'id', headerName: 'Profile', renderCell: (param) => {return <> OSB  |  KeyCloak }, + field: 'id', headerName: 'Profile', renderCell: (param: any) => + <> + OSB +  |  + KeyCloak + , minWidth: 50, flex: 2, }, { diff --git a/applications/jupyterhub/deploy/values-prod.yaml b/applications/jupyterhub/deploy/values-prod.yaml index 88302029..63318d25 100644 --- a/applications/jupyterhub/deploy/values-prod.yaml +++ b/applications/jupyterhub/deploy/values-prod.yaml @@ -1,3 +1,23 @@ +harness: + quotas: + # default for User Quotas + # user quotas can be set as Keycloak attributes on Groups and Users + # quotas set on User level will always overrule Group quotas + # the following keys are valid for Jupyterhub pods (workspaces) + + # sets the maximum number of (included named) servers open concurrently (int) + quota-ws-open: 3 + # sets the cpu guaranteed on a single workspace in CPU units (float) + quota-ws-guaranteecpu: 0.2 + # sets the cpu limit on a single workspace in CPU units (float) + quota-ws-maxcpu: 1 + # sets the memory guaranteed on a single workspace in Gb units (float) + quota-ws-guaranteemem: 0.5 + # sets the memory limit on a single workspace in Gb units (float) + quota-ws-maxmem: 1.5 + # sets the storage dedicated to the user data in Gb units (float) + quota-storage-max: 2 + singleuser: storage: capacity: 2Gi @@ -6,4 +26,6 @@ singleuser: guarantee: 0.2 memory: limit: 1.5G - guarantee: 0.5G \ No newline at end of file + guarantee: 0.5G +legacyusermax: 445 +legacyworkspacemax: 465 \ No newline at end of file diff --git a/applications/jupyterhub/deploy/values.yaml b/applications/jupyterhub/deploy/values.yaml index 84dd460e..3c3812b9 100755 --- a/applications/jupyterhub/deploy/values.yaml +++ b/applications/jupyterhub/deploy/values.yaml @@ -10,6 +10,25 @@ harness: dependencies: build: - cloudharness-base + quotas: + # default for User Quotas + # user quotas can be set as Keycloak attributes on Groups and Users + # quotas set on User level will always overrule Group quotas + # the following keys are valid for Jupyterhub pods (workspaces) + + # sets the maximum number of (included named) servers open concurrently (int) + quota-ws-open: 3 + # sets the cpu guaranteed on a single workspace in CPU units (float) + quota-ws-guaranteecpu: 0.2 + # sets the cpu limit on a single workspace in CPU units (float) + quota-ws-maxcpu: 1 + # sets the memory guaranteed on a single workspace in Gb units (float) + quota-ws-guaranteemem: 0.5 + # sets the memory limit on a single workspace in Gb units (float) + quota-ws-maxmem: 1 + # sets the storage dedicated to the user data in Gb units (float) + quota-storage-max : 1.25 + hub: config: JupyterHub: @@ -20,16 +39,20 @@ hub: c.Spawner.port = 8000 c.Spawner.http_timeout = 300 c.Spawner.start_timeout = 300 - c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' localhost localhost:3000 *.osb.local *.opensourcebrain.org *.v2.opensourcebrain.org"}} + c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' localhost localhost:3000 *.osb.local *.metacell.us *.opensourcebrain.org *.v2.opensourcebrain.org"}} spawner: >- c.Spawner.args = [] singleuser: storage: type: dynamic capacity: 2Mi + dynamic: pvcNameTemplate: osb-user-{userid} volumeNameTemplate: osb-user-{userid} + storageClass: "{{namespace}}-nfs-client" + storageAccessModes: + - "ReadWriteMany" homeMountPath: /opt/user extraLabels: {} cpu: @@ -37,4 +60,12 @@ singleuser: guarantee: 0.2 memory: limit: 1G - guarantee: 0.5G \ No newline at end of file + guarantee: 0.5G +prePuller: + + # hook relates to the hook-image-awaiter Job and hook-image-puller DaemonSet + hook: + enabled: false + continuous: + enabled: false + pullProfileListImages: false diff --git a/applications/jupyterhub/hub/jupyter_notebook_config.py b/applications/jupyterhub/hub/jupyter_notebook_config.py index 5340a9d7..748f6bca 100755 --- a/applications/jupyterhub/hub/jupyter_notebook_config.py +++ b/applications/jupyterhub/hub/jupyter_notebook_config.py @@ -20,7 +20,7 @@ print(c.NotebookApp.tornado_settings) c.NotebookApp.tornado_settings = { 'headers': { - 'Content-Security-Policy': "frame-ancestors 'self' localhost:3000 localhost:* localhost *.osb.local *.opensourcebrain.org", + 'Content-Security-Policy': "frame-ancestors 'self' localhost:3000 localhost:* localhost *.osb.local *.opensourcebrain.org *.metacell.us", } } print(c.NotebookApp.tornado_settings) diff --git a/applications/jupyterhub/kubespawner b/applications/jupyterhub/kubespawner new file mode 160000 index 00000000..8942af43 --- /dev/null +++ b/applications/jupyterhub/kubespawner @@ -0,0 +1 @@ +Subproject commit 8942af43f3a5eb951b41c3012c52b301d9ed6655 diff --git a/applications/jupyterhub/src/osb_jupyter/osb_jupyter/jupyterhub.py b/applications/jupyterhub/src/osb_jupyter/osb_jupyter/jupyterhub.py index 91b074a8..ceefbb64 100755 --- a/applications/jupyterhub/src/osb_jupyter/osb_jupyter/jupyterhub.py +++ b/applications/jupyterhub/src/osb_jupyter/osb_jupyter/jupyterhub.py @@ -36,7 +36,7 @@ def change_pod_manifest(self: KubeSpawner): Returns: - """ - + print("OSB change pod manifest") # get the workspace cookie to determine the workspace id def get_from_cookie(cookie_name): @@ -46,6 +46,13 @@ def get_from_cookie(cookie_name): "Required cookie not found. Check that the cookie named '%s' is set." % cookie_name) return cookie.value + def user_volume_is_legacy(user_id): + print("User id", user_id, "max", self.config['apps']['jupyterhub'].get('legacyusermax', 0)) + return int(user_id) < self.config['apps']['jupyterhub'].get('legacyusermax', 0) + + def workspace_volume_is_legacy(workspace_id): + return int(workspace_id) < self.config['apps']['jupyterhub'].get('legacyworkspacemax', 0) + try: workspace_id = get_from_cookie('workspaceId') volume_name = f'workspace-{workspace_id}' @@ -58,9 +65,6 @@ def get_from_cookie(cookie_name): 'name': volume_name, 'persistentVolumeClaim': { 'claimName': volume_name, - 'spec': { - 'accessModes': ['ReadWriteOnce', 'ReadOnlyMany'] - } } } @@ -78,13 +82,18 @@ def get_from_cookie(cookie_name): self.common_labels = labels self.extra_labels = labels - - self.pod_affinity_required.append(affinity_spec('user', self.user.name)) + self.storage_class = f'{self.config["namespace"]}-nfs-client' + if not user_volume_is_legacy(self.user.id): + # User pod affinity is by default added by cloudharness + self.pod_affinity_required = [] + write_access = has_user_write_access( workspace_id, self.user, workspace_owner) - if write_access: + if workspace_volume_is_legacy(workspace_id): # Pods with write access must be on the same node self.pod_affinity_required.append(affinity_spec('workspace', workspace_id)) + from pprint import pprint + pprint(self.volumes) if not [v for v in self.volume_mounts if v['name'] == volume_name]: self.volume_mounts.append({ 'name': volume_name, @@ -95,6 +104,7 @@ def get_from_cookie(cookie_name): log.error('Change pod manifest failed due to an error.', exc_info=True) + def has_user_write_access(workspace_id, user: User, workspace_owner: str): print('name:', user.name, workspace_owner) if workspace_owner == user.name: diff --git a/applications/jupyterlab-minimal/Dockerfile b/applications/jupyterlab-minimal/Dockerfile new file mode 100644 index 00000000..56dedad7 --- /dev/null +++ b/applications/jupyterlab-minimal/Dockerfile @@ -0,0 +1,3 @@ +FROM jupyter/base-notebook:hub-1.4.2 + +COPY hub/jupyter_notebook_config.py /etc/jupyter/jupyter_notebook_config.py \ No newline at end of file diff --git a/applications/jupyterlab-minimal/deploy/values.yaml b/applications/jupyterlab-minimal/deploy/values.yaml new file mode 100644 index 00000000..d5264ee5 --- /dev/null +++ b/applications/jupyterlab-minimal/deploy/values.yaml @@ -0,0 +1,24 @@ +harness: + subdomain: notebooks + service: + auto: false + port: 80 + name: proxy-public + deployment: + auto: false + dependencies: + build: + - cloudharness-base + hard: + - jupyterhub + jupyterhub: + args: ["--debug", "--NotebookApp.default_url=/lab", "--NotebookApp.notebook_dir=/opt/workspace"] + applicationHook: "osb_jupyter.change_pod_manifest" + extraConfig: + timing: | + c.Spawner.port = 8000 + c.Spawner.http_timeout = 300 + c.Spawner.start_timeout = 300 + + c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' localhost:3000 *.osb.local osb.local localhost *.metacell.us *.opensourcebrain.org"}} + diff --git a/applications/jupyterlab-minimal/hub/jupyter_notebook_config.py b/applications/jupyterlab-minimal/hub/jupyter_notebook_config.py new file mode 100644 index 00000000..c3cbd116 --- /dev/null +++ b/applications/jupyterlab-minimal/hub/jupyter_notebook_config.py @@ -0,0 +1,66 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +from jupyter_core.paths import jupyter_data_dir +import subprocess +import os +import errno +import stat + +c = get_config() +c.NotebookApp.ip = '0.0.0.0' +c.NotebookApp.port = 8888 +c.NotebookApp.open_browser = False + +# https://github.com/jupyter/notebook/issues/3130 +c.FileContentsManager.delete_to_trash = False + +print('*'*80) +import notebook +print(c.NotebookApp.tornado_settings) +c.NotebookApp.tornado_settings = { + 'headers': { + 'Content-Security-Policy': "frame-ancestors 'self' localhost:3000 localhost *.osb.local *.opensourcebrain.org", + } +} +print(c.NotebookApp.tornado_settings) +print('*'*80) + +# Generate a self-signed certificate +if 'GEN_CERT' in os.environ: + dir_name = jupyter_data_dir() + pem_file = os.path.join(dir_name, 'notebook.pem') + try: + os.makedirs(dir_name) + except OSError as exc: # Python >2.5 + if exc.errno == errno.EEXIST and os.path.isdir(dir_name): + pass + else: + raise + + # Generate an openssl.cnf file to set the distinguished name + cnf_file = os.path.join(os.getenv('CONDA_DIR', '/usr/lib'), 'ssl', 'openssl.cnf') + if not os.path.isfile(cnf_file): + with open(cnf_file, 'w') as fh: + fh.write('''\ +[req] +distinguished_name = req_distinguished_name +[req_distinguished_name] +''') + + # Generate a certificate if one doesn't exist on disk + subprocess.check_call(['openssl', 'req', '-new', + '-newkey', 'rsa:2048', + '-days', '365', + '-nodes', '-x509', + '-subj', '/C=XX/ST=XX/L=XX/O=generated/CN=generated', + '-keyout', pem_file, + '-out', pem_file]) + # Restrict access to the file + os.chmod(pem_file, stat.S_IRUSR | stat.S_IWUSR) + c.NotebookApp.certfile = pem_file + +# Change default umask for all subprocesses of the notebook server if set in +# the environment +if 'NB_UMASK' in os.environ: + os.umask(int(os.environ['NB_UMASK'], 8)) diff --git a/applications/jupyterlab/Dockerfile b/applications/jupyterlab/Dockerfile index ff7dde55..8f329283 100644 --- a/applications/jupyterlab/Dockerfile +++ b/applications/jupyterlab/Dockerfile @@ -1,4 +1,4 @@ -FROM jupyter/base-notebook:hub-1.4.2 +FROM jupyter/base-notebook:hub-1.5.0 USER root @@ -41,7 +41,7 @@ RUN jupyter labextension install plotlywidget # this will cause the libs to get updated and use the cached dependencies # Invalidate the cache -ARG NOCACHE +# ARG NOCACHE USER root # LFPy diff --git a/applications/jupyterlab/deploy/values.yaml b/applications/jupyterlab/deploy/values.yaml index 7a261fab..d5264ee5 100755 --- a/applications/jupyterlab/deploy/values.yaml +++ b/applications/jupyterlab/deploy/values.yaml @@ -20,5 +20,5 @@ harness: c.Spawner.http_timeout = 300 c.Spawner.start_timeout = 300 - c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' localhost:3000 *.osb.local osb.local localhost *.opensourcebrain.org"}} + c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' localhost:3000 *.osb.local osb.local localhost *.metacell.us *.opensourcebrain.org"}} diff --git a/applications/jupyterlab/hub/jupyter_notebook_config.py b/applications/jupyterlab/hub/jupyter_notebook_config.py index c3cbd116..4e25c2ae 100755 --- a/applications/jupyterlab/hub/jupyter_notebook_config.py +++ b/applications/jupyterlab/hub/jupyter_notebook_config.py @@ -20,7 +20,7 @@ print(c.NotebookApp.tornado_settings) c.NotebookApp.tornado_settings = { 'headers': { - 'Content-Security-Policy': "frame-ancestors 'self' localhost:3000 localhost *.osb.local *.opensourcebrain.org", + 'Content-Security-Policy': "frame-ancestors 'self' localhost:3000 localhost *.osb.local *.metacell.us *.opensourcebrain.org", } } print(c.NotebookApp.tornado_settings) diff --git a/applications/jupyterlab/requirements.txt b/applications/jupyterlab/requirements.txt index 27153324..5fd4862f 100644 --- a/applications/jupyterlab/requirements.txt +++ b/applications/jupyterlab/requirements.txt @@ -49,7 +49,7 @@ protobuf==3.17.0 git+https://github.com/SheffieldML/GPy.git@devel #modeci_mdf==0.3.3 # big jump in size of image... -sklearn # Required for some Neuromatch Academy material +scikit-learn # Required for some Neuromatch Academy material fasttext # Required for some Neuromatch Academy material diff --git a/applications/netpyne/Dockerfile b/applications/netpyne/Dockerfile index 870046f0..a92ce648 100644 --- a/applications/netpyne/Dockerfile +++ b/applications/netpyne/Dockerfile @@ -2,7 +2,7 @@ FROM node:13.14 as jsbuild ENV REPO=https://github.com/MetaCell/NetPyNE-UI.git ENV BRANCH_TAG=osb2-dev ENV FOLDER=netpyne -RUN echo "no-cache 2022-08-26" +RUN echo "no-cache 2023-1-9" RUN git clone $REPO -b $BRANCH_TAG $FOLDER RUN rm -Rf .git diff --git a/applications/netpyne/deploy/values.yaml b/applications/netpyne/deploy/values.yaml index 81d48ab9..768e9c4a 100644 --- a/applications/netpyne/deploy/values.yaml +++ b/applications/netpyne/deploy/values.yaml @@ -12,7 +12,7 @@ harness: c.Spawner.port = 8000 c.Spawner.http_timeout = 300 c.Spawner.start_timeout = 300 - c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' localhost:3000 *.osb.local localhost *.opensourcebrain.org"}} + c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' localhost:3000 *.osb.local localhost *.metacell.us *.opensourcebrain.org"}} dependencies: hard: - jupyterhub \ No newline at end of file diff --git a/applications/netpyne/overrides/hub/jupyter_notebook_config.py b/applications/netpyne/overrides/hub/jupyter_notebook_config.py index 83d04b3a..e88686ad 100755 --- a/applications/netpyne/overrides/hub/jupyter_notebook_config.py +++ b/applications/netpyne/overrides/hub/jupyter_notebook_config.py @@ -19,7 +19,7 @@ print(c.NotebookApp.tornado_settings) c.NotebookApp.tornado_settings = { 'headers': { - 'Content-Security-Policy': "frame-ancestors 'self' localhost:3000 localhost:* localhost *.osb.local *.opensourcebrain.org", + 'Content-Security-Policy': "frame-ancestors 'self' localhost:3000 localhost:* localhost *.osb.local *.opensourcebrain.org *.metacell.us", } } print(c.NotebookApp.tornado_settings) diff --git a/applications/nwb-explorer/Dockerfile b/applications/nwb-explorer/Dockerfile index f2ef1078..bc533ff1 100644 --- a/applications/nwb-explorer/Dockerfile +++ b/applications/nwb-explorer/Dockerfile @@ -36,32 +36,34 @@ RUN rm -rf /var/lib/apt/lists RUN apt-get update -qq &&\ apt-get install python3-tk vim nano unzip git g++ libjpeg-dev zlib1g-dev -qq RUN pip install cython --no-cache-dir -# Temporary fix for deprecated api usage on some requirement -# RUN pip install setuptools==45 - -COPY --from=clone $FOLDER/requirements.txt requirements.txt -RUN pip install -r requirements.txt --no-cache-dir -COPY --from=clone $FOLDER . - - RUN chown $NB_UID /opt RUN chown $NB_UID . -RUN chown -R $NB_UID /opt/conda/etc/jupyter/nbconfig + + USER $NB_UID +COPY --from=clone $FOLDER/requirements.txt requirements.txt +RUN pip install -r requirements.txt --no-cache-dir +COPY --from=clone $FOLDER . + + RUN mkdir -p /opt/workspace RUN ln -s /opt/workspace workspace RUN mkdir -p /opt/home RUN python utilities/install.py --npm-skip --no-test -COPY hub/jupyter_notebook_config.py /etc/jupyter/jupyter_notebook_config.py +COPY --from=clone $FOLDER/utilities/custom.css /home/jovyan/.jupyter/custom/custom.css # this removes the frame ancestor default cors settings RUN rm -f ~/.jupyter/*.json USER root # sym link workspace pvc to $FOLDER +RUN chown -R $NB_UID /opt/conda/etc/jupyter/nbconfig +COPY hub/jupyter_notebook_config.py /etc/jupyter/jupyter_notebook_config.py +COPY hub/jupyter_notebook_config.py /opt/conda/etc/jupyter/nbconfig/jupyter_notebook_config.py + RUN chown $NB_UID . COPY --from=jsbuild --chown=$NB_UID:$NB_UID $FOLDER/webapp/build webapp/build COPY --from=jsbuild --chown=$NB_UID:$NB_UID $FOLDER/webapp/node_modules/@geppettoengine webapp/node_modules/@geppettoengine diff --git a/applications/nwb-explorer/deploy/values.yaml b/applications/nwb-explorer/deploy/values.yaml index 71953115..8eaad139 100644 --- a/applications/nwb-explorer/deploy/values.yaml +++ b/applications/nwb-explorer/deploy/values.yaml @@ -12,4 +12,4 @@ harness: c.Spawner.port = 8000 c.Spawner.http_timeout = 300 c.Spawner.start_timeout = 300 - c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' *.osb.local localhost"}} + c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' *.osb.local localhost *.opensourcebrain.org *.metacell.us"}} diff --git a/applications/nwb-explorer/hub/jupyter_notebook_config.py b/applications/nwb-explorer/hub/jupyter_notebook_config.py index 9e48083a..f66ad7d6 100755 --- a/applications/nwb-explorer/hub/jupyter_notebook_config.py +++ b/applications/nwb-explorer/hub/jupyter_notebook_config.py @@ -19,7 +19,7 @@ print(c.NotebookApp.tornado_settings) c.NotebookApp.tornado_settings = { 'headers': { - 'Content-Security-Policy': "frame-ancestors 'self' localhost:3000 localhost *.osb.local *.opensourcebrain.org", + 'Content-Security-Policy': "frame-ancestors 'self' localhost:3000 localhost *.osb.local *.opensourcebrain.org *.metacell.us", } } print(c.NotebookApp.tornado_settings) diff --git a/applications/osb-portal/deploy/values-minimal.yaml b/applications/osb-portal/deploy/values-minimal.yaml index 90d0fcfc..1b6a8b04 100644 --- a/applications/osb-portal/deploy/values-minimal.yaml +++ b/applications/osb-portal/deploy/values-minimal.yaml @@ -6,4 +6,5 @@ harness: - workspaces - jupyterhub - notifications + - jupyterlab-minimal diff --git a/applications/osb-portal/package.json b/applications/osb-portal/package.json index 0145f7c3..a0fd43ac 100644 --- a/applications/osb-portal/package.json +++ b/applications/osb-portal/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "start:dev": "webpack serve --progress --config webpack.dev.js --env DOMAIN=https://v2dev.opensourcebrain.org --env NAMESPACE=osb2dev --env NODE_OPTIONS='--max-old-space-size=12048'", - "start:test": "webpack serve --progress --config webpack.dev.js --env DOMAIN=https://v2.opensourcebrain.org --env NAMESPACE=osb2 --env NODE_OPTIONS='--max-old-space-size=12048'", + "start:prod": "webpack serve --progress --config webpack.dev.js --env DOMAIN=https://v2.opensourcebrain.org --env NAMESPACE=osb2 --env NODE_OPTIONS='--max-old-space-size=12048'", "start:local": "webpack serve --progress --config webpack.dev.js --env DOMAIN=http://osb.local --env NAMESPACE=osblocal --env NODE_OPTIONS='--max-old-space-size=12048'", "start:dev-be": "webpack serve --progress --config webpack.dev.js --env DOMAIN=https://v2dev.opensourcebrain.org --env ACCOUNTS_API_DOMAIN=localhost:5001 --env WORKSPACES_DOMAIN=http://localhost:5000 --env NAMESPACE=osblocal --env NODE_OPTIONS='--max-old-space-size=12048'", "start:dev-app": "webpack serve --progress --config webpack.dev.js --env APP_DOMAIN=https://localhost:8888 --env DOMAIN=https://v2.opensourcebrain.org --env NAMESPACE=osblocal --env NODE_OPTIONS='--max-old-space-size=12048'", diff --git a/applications/osb-portal/src/components/index.ts b/applications/osb-portal/src/components/index.ts index 0358c809..94329613 100644 --- a/applications/osb-portal/src/components/index.ts +++ b/applications/osb-portal/src/components/index.ts @@ -32,6 +32,7 @@ import { RepositoriesPage as repositoriesPage } from "../pages/RepositoriesPage" import repositories from "../components/repository/Repositories"; import { retrieveAllTags, loadTags } from "../store/actions/tags"; import { WorkspaceCard as workspaceCard } from "./workspace/WorkspaceCard"; +import WorkspaceActionsMenuUnbound from "./workspace/WorkspaceActionsMenu"; const mapWorkspacesStateToProps = (state: RootState) => ({ user: state.user, @@ -51,6 +52,7 @@ const dispatchWorkspaceProps = { const mapUserStateToProps = (state: RootState) => ({ user: state.user, + workspacesCounter: state.workspaces.counter }); const dispatchUserProps = { @@ -87,6 +89,7 @@ const mapTagsToProps = (state: RootState) => ({ const mapUserAndTagsToProps = (state: RootState) => ({ user: state.user, tags: state.tags, + }); const mapAboutDialogToProps = (state: RootState) => ({ @@ -170,3 +173,8 @@ export const ProtectedRoute = connect( mapUserStateToProps, dispatchUserProps )(protectedRoute); + +export const WorkspaceActionsMenu = connect( + null, + dispatchWorkspaceProps +)(WorkspaceActionsMenuUnbound); diff --git a/applications/osb-portal/src/components/repository/EditRepoDialog.tsx b/applications/osb-portal/src/components/repository/EditRepoDialog.tsx index 2b2533f9..8213539c 100644 --- a/applications/osb-portal/src/components/repository/EditRepoDialog.tsx +++ b/applications/osb-portal/src/components/repository/EditRepoDialog.tsx @@ -43,7 +43,8 @@ import { Tag, } from "../../apiclient/workspaces"; import { UserInfo } from "../../types/user"; -import { TagFaces } from "@material-ui/icons"; + +const DEFAULT_CONTEXTS = ["main", "master"] const useStyles = makeStyles((theme) => ({ root: { @@ -217,7 +218,7 @@ export const EditRepoDialog = ({ setFormValues({ ...formValues, tags: info.tags.map((tag) => ({tag})), - defaultContext: info.contexts[0], + defaultContext: info.contexts.find((c => DEFAULT_CONTEXTS.includes(c))) || info.contexts[0], summary: info.summary, name: info.name @@ -254,7 +255,7 @@ export const EditRepoDialog = ({ const handleInputContext = (event: any) => { const value = event?.target?.value || event.text; - + setFormValues({...formValues, defaultContext: value}) }; const addOrUpdateRepository = () => { diff --git a/applications/osb-portal/src/components/repository/Repositories.tsx b/applications/osb-portal/src/components/repository/Repositories.tsx index 51e98793..bd059808 100644 --- a/applications/osb-portal/src/components/repository/Repositories.tsx +++ b/applications/osb-portal/src/components/repository/Repositories.tsx @@ -37,7 +37,7 @@ import Resources from "./resources"; interface RepositoriesProps { repositories: OSBRepository[]; showSimpleVersion?: boolean; - handleRepositoryClick: (repositoryId: number) => void; + handleRepositoryClick: (repository: Repository) => void; handleTagClick: (tagObject: Tag) => void; handleTagUnclick: (tagObject: Tag) => void; handleTypeClick: (type: string) => void; @@ -263,7 +263,7 @@ export default (props: RepositoriesProps) => { onClick={(e: any) => { console.log(e); if (e.target.tagName.toLowerCase() !== "a") - props.handleRepositoryClick(repository.id); + props.handleRepositoryClick(repository); }} > {repository.name} diff --git a/applications/osb-portal/src/components/repository/RepositoryResourceBrowser.tsx b/applications/osb-portal/src/components/repository/RepositoryResourceBrowser.tsx index c8b0eecb..c7e49768 100644 --- a/applications/osb-portal/src/components/repository/RepositoryResourceBrowser.tsx +++ b/applications/osb-portal/src/components/repository/RepositoryResourceBrowser.tsx @@ -18,8 +18,8 @@ import Table from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; +import Alert from '@material-ui/lab/Alert'; import prettyBytes from "pretty-bytes"; import { @@ -232,6 +232,7 @@ export default ({ /> + {!repository.contextResources || !repository.contextResources.children.length && The content list for this repository is empty. This is likely due to a temporary issue with the {repository.repositoryType} APIs. Please try again later. } diff --git a/applications/osb-portal/src/components/workspace/AddResourceForm.tsx b/applications/osb-portal/src/components/workspace/AddResourceForm.tsx index 87e07245..9d2ba86a 100644 --- a/applications/osb-portal/src/components/workspace/AddResourceForm.tsx +++ b/applications/osb-portal/src/components/workspace/AddResourceForm.tsx @@ -291,9 +291,9 @@ export default (props: WorkspaceEditProps) => { setChecked([]); }; - const loadRepository = (repositoryId: number) => { + const loadRepository = (repository: OSBRepository) => { setRepositoryLoading(true); - RepositoryService.getRepository(repositoryId).then((repo) => { + RepositoryService.getRepository(repository.id).then((repo) => { setRepository(repo); }); }; @@ -491,12 +491,10 @@ export default (props: WorkspaceEditProps) => { - loadRepository(repositoryId) - } + handleRepositoryClick={loadRepository} showSimpleVersion={true} searchRepositories={true} - filterChanged={(newFilter) => setFilter(newFilter)} + filterChanged={(newFilter) => setFilter({...filter, text: newFilter})} /> {totalPages > 1 ? ( diff --git a/applications/osb-portal/src/components/workspace/WorkspaceActionsMenu.tsx b/applications/osb-portal/src/components/workspace/WorkspaceActionsMenu.tsx index e22e5c96..b1b7edf4 100644 --- a/applications/osb-portal/src/components/workspace/WorkspaceActionsMenu.tsx +++ b/applications/osb-portal/src/components/workspace/WorkspaceActionsMenu.tsx @@ -22,12 +22,13 @@ import { bgDarkest, textColor } from "../../theme"; // TODO: refactor to use redux instead of passing props interface WorkspaceActionsMenuProps { - workspace: Workspace; - updateWorkspace?: (ws: Workspace) => null; + workspace?: Workspace; + updateWorkspace?: (ws: Workspace) => void; deleteWorkspace?: (wsId: number) => void; - refreshWorkspaces: () => void; + refreshWorkspaces?: () => void; user?: UserInfo; - isWorkspaceOpen: boolean; + isWorkspaceOpen?: boolean; + [other: string]: any; } const useStyles = makeStyles((theme) => ({ diff --git a/applications/osb-portal/src/components/workspace/WorkspaceCard.tsx b/applications/osb-portal/src/components/workspace/WorkspaceCard.tsx index 1cd2a5ac..4d0586a6 100644 --- a/applications/osb-portal/src/components/workspace/WorkspaceCard.tsx +++ b/applications/osb-portal/src/components/workspace/WorkspaceCard.tsx @@ -15,9 +15,8 @@ import LocalOfferIcon from "@material-ui/icons/LocalOffer"; import { Workspace } from "../../types/workspace"; import { formatDate } from "../../utils"; import { UserInfo } from "../../types/user"; -import WorkspaceActionsMenu from "./WorkspaceActionsMenu"; +import { WorkspaceActionsMenu } from ".."; import { bgDarkest, paragraph, textColor } from "../../theme"; -import AccountCircleIcon from "@material-ui/icons/AccountCircle"; interface Props { workspace: Workspace; @@ -122,9 +121,6 @@ export const WorkspaceCard = (props: Props) => { diff --git a/applications/osb-portal/src/components/workspace/WorkspaceEditor.tsx b/applications/osb-portal/src/components/workspace/WorkspaceEditor.tsx index af16cc9f..3caf1cb8 100644 --- a/applications/osb-portal/src/components/workspace/WorkspaceEditor.tsx +++ b/applications/osb-portal/src/components/workspace/WorkspaceEditor.tsx @@ -176,6 +176,9 @@ export default (props: WorkspaceEditProps) => { }, (e) => { setLoading(false); + if (e.status === 405) { + throw new Error("Maximum number of workspaces exceeded."); + } throw new Error("Error submitting the workspace"); // console.error('Error submitting the workspace', e); } diff --git a/applications/osb-portal/src/components/workspace/WorkspaceFrame.tsx b/applications/osb-portal/src/components/workspace/WorkspaceFrame.tsx index 42ea17e3..7c00b13f 100644 --- a/applications/osb-portal/src/components/workspace/WorkspaceFrame.tsx +++ b/applications/osb-portal/src/components/workspace/WorkspaceFrame.tsx @@ -94,31 +94,23 @@ export const WorkspaceFrame = (props: { openResource(); }, [currentResource]); - const getResourceToOpen = () => { - if (currentResource != null) { - if (!app || currentResource.type.application === OSBApplications[app]) { - return currentResource; - } - } else { - return workspace.resources[workspace.resources.length - 1]; - } - }; + const openResource = async () => { - const resource: WorkspaceResource = getResourceToOpen(); + const resource: WorkspaceResource = currentResource; const iFrame = document.getElementById( "workspace-frame" ) as HTMLIFrameElement; - if (resource.status === ResourceStatus.available) { + + if (resource && resource.status === ResourceStatus.available) { const fileName: string = WORKSPACE_BASE_DIRECTORY + WorkspaceResourceService.getResourcePath(resource); - - iFrame.contentWindow.postMessage( - { type: "LOAD_RESOURCE", payload: fileName }, - "*" - ); - + + iFrame.contentWindow.postMessage( + { type: "LOAD_RESOURCE", payload: fileName }, + "*" + ); } }; diff --git a/applications/osb-portal/src/components/workspace/WorkspaceFromRepository.tsx b/applications/osb-portal/src/components/workspace/WorkspaceFromRepository.tsx index 75b39705..650635d3 100644 --- a/applications/osb-portal/src/components/workspace/WorkspaceFromRepository.tsx +++ b/applications/osb-portal/src/components/workspace/WorkspaceFromRepository.tsx @@ -140,7 +140,7 @@ export const WorkspaceFromRepository = ({ const user = useSelector((state: RootState) => state.user); const classes = useStyles(); - const [selectedRepository, setSelectedRepository] = React.useState(null); + const [selectedRepository, setSelectedRepository] = React.useState(null); enum Stage { SELECT_REPO, @@ -287,13 +287,14 @@ export const WorkspaceFromRepository = ({ { - setSelectedRepository(repositoryId); + handleRepositoryClick={(repository) => { + setSelectedRepository(repository); setStage(Stage.SELECT_FILES); }} showSimpleVersion={true} searchRepositories={true} filterChanged={setFilter} + /> {totalPages > 1 ? ( ); case Stage.SELECT_FILES: return returnDialoged( - + ); case Stage.EDIT_WORKSPACE: return ( @@ -359,7 +360,7 @@ export const WorkspaceFromRepository = ({ = ({ // Keep drawer closed for jupyter by default const [open, setOpen] = React.useState(app === "jupyter" ? false : true); + const getActiveResource = () => { + if (workspace.lastOpen != null) { + if (!app || workspace.lastOpen.type.application === OSBApplications[app]) { + return workspace.lastOpen; + } + } + if (app) { + return workspace.resources.find( + (resource) => + resource.type.application === OSBApplications[app] && + resource.status === ResourceStatus.available + ); + } else if (workspace.resources?.length) { + return workspace.resources.find(resource => resource.status === ResourceStatus.available); + } + }; const [currentResource, setCurrentResource] = - React.useState(!app ? workspace.lastOpen : null); + React.useState(getActiveResource()); const handleToggleDrawer = () => setOpen(!open); @@ -135,6 +151,7 @@ export const WorkspaceDrawer: React.FunctionComponent = ({ diff --git a/applications/osb-portal/src/components/workspace/drawer/WorkspaceInteractions.tsx b/applications/osb-portal/src/components/workspace/drawer/WorkspaceInteractions.tsx index 236194ee..74f79619 100644 --- a/applications/osb-portal/src/components/workspace/drawer/WorkspaceInteractions.tsx +++ b/applications/osb-portal/src/components/workspace/drawer/WorkspaceInteractions.tsx @@ -34,7 +34,7 @@ import OSBDialog from "../../common/OSBDialog"; import AddResourceForm from "../AddResourceForm"; import { canEditWorkspace } from "../../../service/UserService"; import { primaryColor } from "../../../theme"; -import WorkspaceActionsMenu from "../WorkspaceActionsMenu"; +import { WorkspaceActionsMenu } from "../.."; import { UserInfo } from "../../../types/user"; const useStyles = makeStyles((theme) => ({ @@ -118,6 +118,7 @@ interface WorkspaceProps { [propName: string]: any; openResource: (r: WorkspaceResource) => any; refreshWorkspacePage?: () => void; + currentResource: WorkspaceResource; } export default (props: WorkspaceProps | any) => { @@ -247,9 +248,6 @@ export default (props: WorkspaceProps | any) => { @@ -290,6 +288,7 @@ export default (props: WorkspaceProps | any) => { @@ -326,9 +325,6 @@ export default (props: WorkspaceProps | any) => { diff --git a/applications/osb-portal/src/components/workspace/drawer/WorkspaceResourceBrowser.tsx b/applications/osb-portal/src/components/workspace/drawer/WorkspaceResourceBrowser.tsx index 97a21ca9..28cef332 100644 --- a/applications/osb-portal/src/components/workspace/drawer/WorkspaceResourceBrowser.tsx +++ b/applications/osb-portal/src/components/workspace/drawer/WorkspaceResourceBrowser.tsx @@ -136,13 +136,13 @@ interface WorkspaceProps { workspace: Workspace; refreshWorkspace: () => void; openResource: (r: WorkspaceResource) => any; + currentResource: WorkspaceResource } const WorkspaceResourceBrowser = (props: WorkspaceProps) => { - const { workspace, refreshWorkspace, openResource } = props; + const { workspace, refreshWorkspace, openResource, currentResource } = props; - const lastOpenResourceId = - workspace.lastOpen !== null ? workspace.lastOpen.id : -1; + const lastOpenResourceId = currentResource?.id ?? -1; const resources = workspace.resources.filter( (resource) => resource.id !== undefined ); diff --git a/applications/osb-portal/src/pages/RepositoriesPage/RepositoriesPage.tsx b/applications/osb-portal/src/pages/RepositoriesPage/RepositoriesPage.tsx index ac5cf4c6..dfb8e735 100644 --- a/applications/osb-portal/src/pages/RepositoriesPage/RepositoriesPage.tsx +++ b/applications/osb-portal/src/pages/RepositoriesPage/RepositoriesPage.tsx @@ -417,8 +417,8 @@ export const RepositoriesPage = ({ user }: { user: UserInfo }) => { repositories={repositories} refreshRepositories={() => updateList(tabValue)} searchFilterValues={searchFilterValues} - handleRepositoryClick={(repositoryId: number) => - openRepoUrl(repositoryId) + handleRepositoryClick={(repository: OSBRepository) => + openRepoUrl(repository.id) } handleTagClick={(tag: Tag) => searchFilterValues.tags.includes(tag.tag) diff --git a/applications/osb-portal/src/pages/RepositoryPage.tsx b/applications/osb-portal/src/pages/RepositoryPage.tsx index 227076b6..e5a9cdf6 100644 --- a/applications/osb-portal/src/pages/RepositoryPage.tsx +++ b/applications/osb-portal/src/pages/RepositoryPage.tsx @@ -675,7 +675,7 @@ export const RepositoryPage = (props: any) => { 0} diff --git a/applications/osb-portal/src/pages/UserPage.tsx b/applications/osb-portal/src/pages/UserPage.tsx index c0dd8667..30bc497f 100644 --- a/applications/osb-portal/src/pages/UserPage.tsx +++ b/applications/osb-portal/src/pages/UserPage.tsx @@ -257,7 +257,7 @@ export const UserPage = (props: any) => { setError(e); } ); - }, [userId]); + }, [userId, tabValue, props.workspacesCounter]); if (error) { throw error; @@ -631,7 +631,10 @@ export const UserPage = (props: any) => { lg={4} xl={3} > - + ); })} diff --git a/applications/osb-portal/test/e2e/main_flows.spec.ts b/applications/osb-portal/test/e2e/main_flows.spec.ts index b6a0fa18..51757221 100644 --- a/applications/osb-portal/test/e2e/main_flows.spec.ts +++ b/applications/osb-portal/test/e2e/main_flows.spec.ts @@ -1,17 +1,17 @@ -import * as puppeteer from "puppeteer"; -import * as selectors from "./selectors"; -import { +const puppeteer = require("puppeteer"); +const selectors = require("./selectors"); +const { ONE_SECOND, ONE_MINUTE, TWO_MINUTES, TEN_MINUTES, -} from "./time_constants"; +} = require("./time_constants"); let page: any; let browser: any; -jest.setTimeout(TEN_MINUTES); +jest.setTimeout(TEN_MINUTES* 4); -const WORKSPACE_LOAD_TIMEOUT = TWO_MINUTES * 1.5; +const WORKSPACE_LOAD_TIMEOUT = TEN_MINUTES; const getCurrentWorkpaces: () => Promise> = async () => { const pageFrame = page.mainFrame(); @@ -25,6 +25,7 @@ const testApplication = console.log("Opening workspace with", appName); await page.waitForSelector(selectors.OSB_LOGO); await page.click(selectors.OSB_LOGO); + await page.waitForSelector(selectors.SMOKE_TEST_WORKSPACE); @@ -80,6 +81,13 @@ const testApplication = } ); const frame = await elementHandle.contentFrame(); + + await frame.waitForSelector(selectors.SPAWN, { + timeout: ONE_MINUTE, + }); + + + for(const appSelector of appSelectors) { if(!frame.isDetached()) { await frame.waitForSelector(appSelector, { @@ -100,29 +108,37 @@ describe("OSB v2 Smoke Tests", () => { `--window-size=1600,1000`, "--ignore-certificate-errors" ], - headless: true, + headless: !process.env.PUPPETEER_DISPLAY, defaultViewport: { width: 1600, height: 1000, }, }); - page = await browser.newPage(); console.log( "Checking page", process.env.APP_URL || "https://v2dev.opensourcebrain.org/" ); + + + }); + + beforeEach(async () => { + page = await browser.newPage(); + await page .goto(process.env.APP_URL || "https://v2dev.opensourcebrain.org/", { waitUntil: "networkidle0", }) .catch(() => {}); - console.log("Env", process.env); - await page.waitForSelector(selectors.WORKSPACES); }); + afterEach(async () => { + await page.close(); + }); + afterAll(() => { browser.close(); }); @@ -168,10 +184,8 @@ describe("OSB v2 Smoke Tests", () => { await page.click(selectors.CREATE_NEW_WORKSPACE); await page.waitForSelector(selectors.SMOKE_TEST_WORKSPACE); await page.waitForSelector(selectors.YOUR_WORKSPACES); - const privateWorkspacesAfter = await getCurrentWorkpaces(); - expect(privateWorkspacesAfter.length).toBe( - privateWorkspacesBefore.length + 1 - ); + await page.waitForSelector(".workspace-card"); + await page.waitForSelector(selectors.SMOKE_TEST_WORKSPACE); }); test( @@ -179,17 +193,17 @@ describe("OSB v2 Smoke Tests", () => { testApplication("NWB Explorer", [selectors.NWB_APP], "/nwbexplorer") ); + test("Open workspace with Jupyter Lab", testApplication("JupyterLab", [ + selectors.JUPYTER_CONTENT], "/jupyter")); + test("Open workspace with NetPyNE", testApplication("NetPyNE", [ selectors.NETPYNE_CELL_BUTTON, selectors.NETPYNE_MAIN_CONTAINER], "/netpyne")); - test("Open workspace with Jupyter Lab", testApplication("JupyterLab", [ - selectors.JUPYTER_CONTENT], "/jupyter")); + test("Delete created workspace", async () => { console.log("Deleting created workspace"); - - await page.click(selectors.OSB_LOGO); await page.waitForSelector(selectors.SMOKE_TEST_WORKSPACE); let menuBtn; @@ -197,16 +211,17 @@ describe("OSB v2 Smoke Tests", () => { await menuBtn.click(); - await page.waitForSelector(".delete-workspace", {timeout: ONE_SECOND}); + await page.waitForSelector(".delete-workspace", {timeout: ONE_SECOND * 5}); await page.evaluate(() => document.querySelector(".delete-workspace")?.click()); // page.click does not work on the popover await page.waitForSelector(".delete-workspace", {hidden: true}); + await page.waitForTimeout(ONE_SECOND); } - }); + }); test("Logout", async () => { console.log("Logging out"); diff --git a/applications/osb-portal/test/e2e/selectors.ts b/applications/osb-portal/test/e2e/selectors.ts index eb246bdd..5a071772 100644 --- a/applications/osb-portal/test/e2e/selectors.ts +++ b/applications/osb-portal/test/e2e/selectors.ts @@ -29,3 +29,4 @@ export const NETPYNE_CELL_BUTTON = "#selectCellButton"; export const APPLICATION_FRAME = '#workspace-frame' export const NWB_APP = "#main-container-inner"; export const JUPYTER_CONTENT = '#jp-main-dock-panel'; +export const SPAWN = ".spawn-container" diff --git a/applications/workspaces/api/__open_alchemy_d910ba2ef878f7db0223a966b81c8b3f3b65027bb39e4431bb05140171eece39_cache__ b/applications/workspaces/api/__open_alchemy_d910ba2ef878f7db0223a966b81c8b3f3b65027bb39e4431bb05140171eece39_cache__ deleted file mode 100644 index 6061aa02..00000000 --- a/applications/workspaces/api/__open_alchemy_d910ba2ef878f7db0223a966b81c8b3f3b65027bb39e4431bb05140171eece39_cache__ +++ /dev/null @@ -1 +0,0 @@ -{"hash": "b18fdf603b24cd4678693ba834b60afd0cc7f031ee0e59d72f05319d70b1c100", "data": {"schemas": {"valid": true}}} \ No newline at end of file diff --git a/applications/workspaces/api/openapi.yaml b/applications/workspaces/api/openapi.yaml index 02e0ac12..b7b51265 100644 --- a/applications/workspaces/api/openapi.yaml +++ b/applications/workspaces/api/openapi.yaml @@ -121,6 +121,8 @@ paths: description: Save successful. '400': description: The Workspace already exists. + '405': + description: Not allowed to create a new workspace security: - bearerAuth: [] diff --git a/applications/workspaces/deploy/values-minimal.yaml b/applications/workspaces/deploy/values-minimal.yaml index f42ce05f..712c1494 100644 --- a/applications/workspaces/deploy/values-minimal.yaml +++ b/applications/workspaces/deploy/values-minimal.yaml @@ -1,34 +1,3 @@ harness: sentry: false - deployment: - auto: true - port: 8080 - volume: - name: workspaces-images - size: 100M - mountpath: /usr/src/app/workspaces/static/workspaces - secrets: - github-user: - github-token: - dependencies: - build: - - cloudharness-base - - cloudharness-flask - soft: - - common - - accounts - - events - - workflows - - volumemanager - database: - auto: false - name: workspaces-postgres-host - type: postgres - port: 5432 - image: postgres:latest - initialdb: workspaces - user: workspace - password: secret - datavolume: /opt/data/ - pgdata: /opt/data/pgdata workspace_size: 10Mi \ No newline at end of file diff --git a/applications/workspaces/deploy/values.yaml b/applications/workspaces/deploy/values.yaml index 5a7446ef..10bed4c3 100644 --- a/applications/workspaces/deploy/values.yaml +++ b/applications/workspaces/deploy/values.yaml @@ -29,6 +29,11 @@ harness: limits: memory: 512Mi cpu: 1500m + quotas: + # sets the max number of workspaces per user + quota-ws-max: 5 + # sets the storage dedicated to a single workspace in Gb units (float) + quota-ws-storage-max: 1 secrets: github-user: github-token: @@ -41,7 +46,7 @@ harness: - common - workflows - notifications - + - nfsserver hard: - argo - accounts @@ -63,4 +68,10 @@ harness: limits: memory: "256Mi" cpu: "500m" + test: + unit: + enabled: true + commands: + + - pytest /usr/src/app/ workspace_size: 2Gi \ No newline at end of file diff --git a/applications/workspaces/server/.dockerignore b/applications/workspaces/server/.dockerignore index a05d73b5..a60e0444 100644 --- a/applications/workspaces/server/.dockerignore +++ b/applications/workspaces/server/.dockerignore @@ -6,9 +6,10 @@ git_push.sh test-requirements.txt # Byte-compiled / optimized / DLL files -__pycache__/ +__pycache__/* *.py[cod] *$py.class +*.pyc # C extensions *.so diff --git a/applications/workspaces/server/Dockerfile b/applications/workspaces/server/Dockerfile index 31cf91c3..1324f21f 100644 --- a/applications/workspaces/server/Dockerfile +++ b/applications/workspaces/server/Dockerfile @@ -14,7 +14,6 @@ RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY requirements.txt /usr/src/app/ -COPY requirements /usr/src/app/requirements RUN pip3 install --no-cache-dir -r requirements.txt @@ -31,4 +30,4 @@ ENV FLASK_APP=workspaces.__main__ ENV FLASK_LOG_LEVEL=info RUN pip3 install -e . -ENTRYPOINT gunicorn --workers=$WORKERS --bind=0.0.0.0:$PORT $FLASK_APP:app \ No newline at end of file +CMD gunicorn --workers=$WORKERS --bind=0.0.0.0:$PORT $FLASK_APP:app \ No newline at end of file diff --git a/applications/workspaces/server/requirements.txt b/applications/workspaces/server/requirements.txt index d32f6531..0703ccaa 100644 --- a/applications/workspaces/server/requirements.txt +++ b/applications/workspaces/server/requirements.txt @@ -1,2 +1,98 @@ --r requirements/requirements-base.txt --r requirements/requirements-testing.txt +argo-workflows==5.0.0 +asn1crypto==0.24.0 +atomicwrites==1.4.0 +attrs==21.4.0 +autopep8==1.6.0 +black==22.1.0 +blinker==1.4 +cachetools==5.0.0 +certifi==2019.3.9 +cffi==1.15.0 +chardet==3.0.4 +charset-normalizer==2.0.12 +click==8.0.4 +clickclick==20.10.2 +connexion==2.6.0 +coverage==6.3.2 +cryptography==3.3.2 +distlib==0.3.4 +ecdsa==0.17.0 +filelock==3.6.0 +flake8==4.0.1 +Flask==1.1.2 +Flask-Cors==3.0.8 +flask-oidc==1.4.0 +Flask-SQLAlchemy==2.4.1 +Flask-Testing==0.6.1 +google-auth==2.6.2 +greenlet==1.1.2 +gunicorn==20.1.0 +httplib2==0.20.4 +idna==2.8 +importlib-metadata==4.2.0 +importlib-resources==5.4.0 +inflection==0.5.1 +isort==5.10.1 +itsdangerous==2.0.1 +Jinja2==3.0.3 +jsonschema==4.4.0 +kafka-python==2.0.2 +kazoo==2.5.0 +kubernetes==11.0.0 +MarkupSafe==2.1.1 +mccabe==0.6.1 +more-itertools==8.12.0 +mypy-extensions==0.4.3 +oauth2client==4.1.3 +oauthlib==3.2.0 +OpenAlchemy==1.1.0 +openapi-schema-validator==0.2.3 +openapi-spec-validator==0.4.0 +packaging==21.3 +pathspec==0.9.0 +pep517==0.12.0 +pip-tools==6.5.1 +platformdirs==2.5.1 +pluggy==0.13.1 +psycopg2-binary +py==1.11.0 +pyaml==21.10.1 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycodestyle==2.8.0 +pycparser==2.21 +pyflakes==2.4.0 +PyJWT>=2.0.0 +pykafka==2.8.0 +pyOpenSSL==19.0.0 +pyparsing==3.0.7 +pyrsistent==0.18.1 +PySocks==1.6.8 +pytest==4.6.11 +pytest-cov==3.0.0 +pytest-randomly==1.2.3 +python-dateutil==2.8.2 +python-jose==3.3.0 +PyYAML==6.0 +requests==2.27.1 +requests-oauthlib==1.3.1 +rsa==4.8 +ruamel.yaml==0.16.13 +ruamel.yaml.clib==0.2.6 +sentry-sdk==0.14.4 +six==1.16.0 +SQLAlchemy==1.4.32 +swagger-ui-bundle==0.0.8 +tabulate==0.8.9 +toml==0.10.2 +tomli==2.0.1 +tox==3.24.5 +typed-ast==1.5.2 +typing_extensions==4.1.1 +urllib3==1.26.9 +virtualenv==20.13.4 +wcwidth==0.2.5 +websocket-client==1.3.1 +Werkzeug==2.0.3 +zipp==3.7.0 +responses diff --git a/applications/workspaces/server/requirements/requirements-base.txt b/applications/workspaces/server/requirements/requirements-base.txt deleted file mode 100644 index 35aa40f1..00000000 --- a/applications/workspaces/server/requirements/requirements-base.txt +++ /dev/null @@ -1,98 +0,0 @@ -argo-workflows==5.0.0 -asn1crypto==0.24.0 -atomicwrites==1.4.0 -attrs==21.4.0 -autopep8==1.6.0 -black==22.1.0 -blinker==1.4 -cachetools==5.0.0 -certifi==2019.3.9 -cffi==1.15.0 -chardet==3.0.4 -charset-normalizer==2.0.12 -click==8.0.4 -clickclick==20.10.2 -connexion==2.6.0 -coverage==6.3.2 -cryptography==3.3.2 -distlib==0.3.4 -ecdsa==0.17.0 -filelock==3.6.0 -flake8==4.0.1 -Flask==1.1.2 -Flask-Cors==3.0.8 -flask-oidc==1.4.0 -Flask-SQLAlchemy==2.4.1 -Flask-Testing==0.6.1 -google-auth==2.6.2 -greenlet==1.1.2 -gunicorn==20.1.0 -httplib2==0.20.4 -idna==2.8 -importlib-metadata==4.2.0 -importlib-resources==5.4.0 -inflection==0.5.1 -isort==5.10.1 -itsdangerous==2.0.1 -Jinja2==3.0.3 -jsonschema==4.4.0 -kafka-python==2.0.2 -kazoo==2.5.0 -kubernetes==11.0.0 -MarkupSafe==2.1.1 -mccabe==0.6.1 -more-itertools==8.12.0 -mypy-extensions==0.4.3 -oauth2client==4.1.3 -oauthlib==3.2.0 -OpenAlchemy==1.1.0 -openapi-schema-validator==0.2.3 -openapi-spec-validator==0.4.0 -packaging==21.3 -pathspec==0.9.0 -pep517==0.12.0 -pip-tools==6.5.1 -platformdirs==2.5.1 -pluggy==0.13.1 -psycopg2-binary -py==1.11.0 -pyaml==21.10.1 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pycodestyle==2.8.0 -pycparser==2.21 -pyflakes==2.4.0 -PyJWT==1.7.1 -pykafka==2.8.0 -pyOpenSSL==19.0.0 -pyparsing==3.0.7 -pyrsistent==0.18.1 -PySocks==1.6.8 -pytest==4.6.11 -pytest-cov==3.0.0 -pytest-randomly==1.2.3 -python-dateutil==2.8.2 -python-jose==3.3.0 -python-keycloak==0.24.0 -PyYAML==6.0 -requests==2.27.1 -requests-oauthlib==1.3.1 -rsa==4.8 -ruamel.yaml==0.16.13 -ruamel.yaml.clib==0.2.6 -sentry-sdk==0.14.4 -six==1.16.0 -SQLAlchemy==1.4.32 -swagger-ui-bundle==0.0.8 -tabulate==0.8.9 -toml==0.10.2 -tomli==2.0.1 -tox==3.24.5 -typed-ast==1.5.2 -typing_extensions==4.1.1 -urllib3==1.26.9 -virtualenv==20.13.4 -wcwidth==0.2.5 -websocket-client==1.3.1 -Werkzeug==2.0.3 -zipp==3.7.0 diff --git a/applications/workspaces/server/requirements/requirements-testing.txt b/applications/workspaces/server/requirements/requirements-testing.txt deleted file mode 100644 index ca031e0d..00000000 --- a/applications/workspaces/server/requirements/requirements-testing.txt +++ /dev/null @@ -1,9 +0,0 @@ -flake8 -isort -pip-tools -tox -autopep8 -pytest~=4.6.7 # needed for python 2.7+3.4 -pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 -flask_testing==0.6.1 \ No newline at end of file diff --git a/applications/workspaces/server/test-requirements.txt b/applications/workspaces/server/test-requirements.txt index b201821a..70381cb7 100644 --- a/applications/workspaces/server/test-requirements.txt +++ b/applications/workspaces/server/test-requirements.txt @@ -1,8 +1,12 @@ -pytest~=4.6.7 # needed for python 2.7+3.4 -pytest-cov>=2.8.1 -pytest-randomly==1.2.3 # needed for python 2.7+3.4 +pytest +pytest-cov +pytest-randomly flask_testing==0.6.1 black isort tox +responses +-e ../../../cloud-harness/libraries/models +-e ../../../cloud-harness/libraries/cloudharness-common +-e . \ No newline at end of file diff --git a/applications/workspaces/server/test/services/dandi.toml b/applications/workspaces/server/test/services/dandi.toml new file mode 100644 index 00000000..45ddfcff --- /dev/null +++ b/applications/workspaces/server/test/services/dandi.toml @@ -0,0 +1,54 @@ +[[responses]] + +[responses.response] +method = "GET" +url = "https://api.dandiarchive.org/api/dandisets/000029/versions/" +body = "{\"count\":8,\"next\":null,\"previous\":null,\"results\":[{\"version\":\"draft\",\"name\":\"Test dataset for development purposes\",\"asset_count\":4,\"size\":20716124,\"status\":\"Valid\",\"created\":\"2020-07-31T16:44:32.094000Z\",\"modified\":\"2022-11-07T23:45:30.267449Z\",\"dandiset\":{\"identifier\":\"000029\",\"created\":\"2020-07-31T16:44:32.094000Z\",\"modified\":\"2022-06-27T19:36:18.198345Z\",\"contact_person\":\"Last, First\",\"embargo_status\":\"OPEN\"}},{\"version\":\"0.210712.1903\",\"name\":\"Test dataset for development purposes\",\"asset_count\":5,\"size\":20716128,\"status\":\"Valid\",\"created\":\"2021-07-12T19:03:54.185140Z\",\"modified\":\"2021-07-12T19:03:54.410697Z\",\"dandiset\":{\"identifier\":\"000029\",\"created\":\"2020-07-31T16:44:32.094000Z\",\"modified\":\"2022-06-27T19:36:18.198345Z\",\"contact_person\":\"Last, First\",\"embargo_status\":\"OPEN\"}},{\"version\":\"0.210730.1538\",\"name\":\"Test dataset for development purposes\",\"asset_count\":5,\"size\":20716130,\"status\":\"Valid\",\"created\":\"2021-07-30T15:38:58.725535Z\",\"modified\":\"2021-07-30T15:38:58.951710Z\",\"dandiset\":{\"identifier\":\"000029\",\"created\":\"2020-07-31T16:44:32.094000Z\",\"modified\":\"2022-06-27T19:36:18.198345Z\",\"contact_person\":\"Last, First\",\"embargo_status\":\"OPEN\"}},{\"version\":\"0.210805.2047\",\"name\":\"Test dataset for development purposes\",\"asset_count\":5,\"size\":20716130,\"status\":\"Valid\",\"created\":\"2021-08-05T20:47:35.155215Z\",\"modified\":\"2021-08-05T20:47:35.354089Z\",\"dandiset\":{\"identifier\":\"000029\",\"created\":\"2020-07-31T16:44:32.094000Z\",\"modified\":\"2022-06-27T19:36:18.198345Z\",\"contact_person\":\"Last, First\",\"embargo_status\":\"OPEN\"}},{\"version\":\"0.210806.1511\",\"name\":\"Test dataset for development purposes\",\"asset_count\":5,\"size\":20716130,\"status\":\"Valid\",\"created\":\"2021-08-06T15:11:13.175097Z\",\"modified\":\"2021-08-06T15:11:13.471183Z\",\"dandiset\":{\"identifier\":\"000029\",\"created\":\"2020-07-31T16:44:32.094000Z\",\"modified\":\"2022-06-27T19:36:18.198345Z\",\"contact_person\":\"Last, First\",\"embargo_status\":\"OPEN\"}},{\"version\":\"0.210806.2112\",\"name\":\"Test dataset for development purposes\",\"asset_count\":5,\"size\":20716130,\"status\":\"Valid\",\"created\":\"2021-08-06T21:12:59.814044Z\",\"modified\":\"2021-08-06T21:12:59.936073Z\",\"dandiset\":{\"identifier\":\"000029\",\"created\":\"2020-07-31T16:44:32.094000Z\",\"modified\":\"2022-06-27T19:36:18.198345Z\",\"contact_person\":\"Last, First\",\"embargo_status\":\"OPEN\"}},{\"version\":\"0.210915.1646\",\"name\":\"Test dataset for development purposes\",\"asset_count\":6,\"size\":20716176,\"status\":\"Valid\",\"created\":\"2021-09-15T16:46:57.398908Z\",\"modified\":\"2021-09-15T16:46:57.697348Z\",\"dandiset\":{\"identifier\":\"000029\",\"created\":\"2020-07-31T16:44:32.094000Z\",\"modified\":\"2022-06-27T19:36:18.198345Z\",\"contact_person\":\"Last, First\",\"embargo_status\":\"OPEN\"}},{\"version\":\"0.221107.2344\",\"name\":\"Test dataset for development purposes\",\"asset_count\":4,\"size\":20716124,\"status\":\"Valid\",\"created\":\"2022-11-07T23:44:44.010846Z\",\"modified\":\"2022-11-07T23:44:45.180397Z\",\"dandiset\":{\"identifier\":\"000029\",\"created\":\"2020-07-31T16:44:32.094000Z\",\"modified\":\"2022-06-27T19:36:18.198345Z\",\"contact_person\":\"Last, First\",\"embargo_status\":\"OPEN\"}}]}" +status = 200 +content_type = "text/plain" +auto_calculate_content_length = false +[[responses]] + +[responses.response] +method = "GET" +url = "https://api.dandiarchive.org/api/dandisets/000029/versions/draft/assets/paths/?path_prefix=" +body = "{\"count\":4,\"next\":null,\"previous\":null,\"results\":[{\"path\":\"sub-anm369962\",\"version\":419,\"aggregate_files\":1,\"aggregate_size\":6644036,\"asset\":null},{\"path\":\"sub-anm369963\",\"version\":419,\"aggregate_files\":1,\"aggregate_size\":6393196,\"asset\":null},{\"path\":\"sub-anm369964\",\"version\":419,\"aggregate_files\":1,\"aggregate_size\":7660100,\"asset\":null},{\"path\":\"sub-RAT123\",\"version\":419,\"aggregate_files\":1,\"aggregate_size\":18792,\"asset\":null}]}" +status = 200 +content_type = "text/plain" +auto_calculate_content_length = false +[[responses]] + +[responses.response] +method = "GET" +url = "https://api.dandiarchive.org/api/dandisets/000029/versions/draft/assets/paths/?path_prefix=sub-anm369962" +body = "{\"count\":1,\"next\":null,\"previous\":null,\"results\":[{\"path\":\"sub-anm369962/sub-anm369962_behavior+ecephys.nwb\",\"version\":419,\"aggregate_files\":1,\"aggregate_size\":6644036,\"asset\":{\"asset_id\":\"4f6a8f1d-ca04-491f-a530-9ed00a909c21\",\"url\":\"https://dandiarchive.s3.amazonaws.com/blobs/180/9f5/1809f541-1cb1-48b8-b916-9a696bab488d\"}}]}" +status = 200 +content_type = "text/plain" +auto_calculate_content_length = false +[[responses]] + +[responses.response] +method = "GET" +url = "https://api.dandiarchive.org/api/dandisets/000029/versions/draft/assets/paths/?path_prefix=sub-anm369963" +body = "{\"count\":1,\"next\":null,\"previous\":null,\"results\":[{\"path\":\"sub-anm369963/sub-anm369963_behavior+ecephys.nwb\",\"version\":419,\"aggregate_files\":1,\"aggregate_size\":6393196,\"asset\":{\"asset_id\":\"38f2024d-62a9-4c79-8a22-7a0ff34b331d\",\"url\":\"https://dandiarchive.s3.amazonaws.com/blobs/e72/88a/e7288a2c-444c-42c5-b7bb-9a58c728992b\"}}]}" +status = 200 +content_type = "text/plain" +auto_calculate_content_length = false +[[responses]] + +[responses.response] +method = "GET" +url = "https://api.dandiarchive.org/api/dandisets/000029/versions/draft/assets/paths/?path_prefix=sub-anm369964" +body = "{\"count\":1,\"next\":null,\"previous\":null,\"results\":[{\"path\":\"sub-anm369964/sub-anm369964_behavior+ecephys.nwb\",\"version\":419,\"aggregate_files\":1,\"aggregate_size\":7660100,\"asset\":{\"asset_id\":\"689d7c0c-d980-462e-9a56-3df87efc9658\",\"url\":\"https://dandiarchive.s3.amazonaws.com/blobs/c40/57c/c4057c5e-7af5-4370-878f-ccfc971aeba4\"}}]}" +status = 200 +content_type = "text/plain" +auto_calculate_content_length = false +[[responses]] + +[responses.response] +method = "GET" +url = "https://api.dandiarchive.org/api/dandisets/000029/versions/draft/assets/paths/?path_prefix=sub-RAT123" +body = "{\"count\":1,\"next\":null,\"previous\":null,\"results\":[{\"path\":\"sub-RAT123/sub-RAT123.nwb\",\"version\":419,\"aggregate_files\":1,\"aggregate_size\":18792,\"asset\":{\"asset_id\":\"86e09d7e-4355-4887-9c5a-7a137c046953\",\"url\":\"https://dandiarchive.s3.amazonaws.com/blobs/2db/af0/2dbaf0fd-5003-4a0a-b4c0-bc8cdbdb3826\"}}]}" +status = 200 +content_type = "text/plain" +auto_calculate_content_length = false diff --git a/applications/workspaces/server/test/services/test_dandi_adapter.py b/applications/workspaces/server/test/services/test_dandi_adapter.py index 53e85106..dfc3d26c 100644 --- a/applications/workspaces/server/test/services/test_dandi_adapter.py +++ b/applications/workspaces/server/test/services/test_dandi_adapter.py @@ -1,10 +1,32 @@ +import pytest +import os from workspaces.service.osbrepository.adapters.dandiadapter import DandiAdapter from workspaces.models import OSBRepository +import responses +from responses import _recorder +HERE = os.path.dirname(os.path.realpath(__file__)) +os.environ["CH_VALUES_PATH"] = os.path.join(os.path.dirname(HERE), "values.yaml") +resp_file = os.path.join(HERE, "dandi.toml") + + +@_recorder.record(file_path=resp_file) +def record_resources(): + repo = OSBRepository(id=1, name="Allen", + uri="https://gui.dandiarchive.org/#/dandiset/000029") + adapter = DandiAdapter(repo) + + contexts = adapter.get_contexts() + assert contexts + + resources = adapter.get_resources(contexts[0]) + +@responses.activate def test_resources(): + responses._add_from_file(file_path=resp_file) repo = OSBRepository(id=1, name="Allen", - uri="https://gui.dandiarchive.org/#/dandiset/000107") + uri="https://gui.dandiarchive.org/dandiset/000029") adapter = DandiAdapter(repo) contexts = adapter.get_contexts() @@ -13,3 +35,13 @@ def test_resources(): resources = adapter.get_resources(contexts[0]) assert resources + assert len(resources.children) == 4 + assert resources.children[0].resource + assert resources.children[0].resource.name == "sub-anm369962" + assert len(resources.children[0].children) == 1 + assert resources.children[0].children[0].resource.name == "sub-anm369962_behavior+ecephys.nwb" + assert resources.children[0].children[0].resource.path + + +if __name__ == "__main__": + record_resources() \ No newline at end of file diff --git a/applications/workspaces/server/test/values.yaml b/applications/workspaces/server/test/values.yaml new file mode 100644 index 00000000..bbb8e150 --- /dev/null +++ b/applications/workspaces/server/test/values.yaml @@ -0,0 +1,1146 @@ +apps: + accounts: + admin: {pass: metacell, role: administrator, user: admin} + client: {id: rest-client, secret: 5678eb6e-9e2c-4ee5-bd54-34e7411339e8} + enabled: true + harness: + aliases: [] + database: + auto: true + image_ref: null + name: keycloak-postgres + pass: password + postgres: + image: postgres:10.4 + initialdb: auth_db + ports: + - {name: http, port: 5432} + resources: + limits: {cpu: 1000m, memory: 2Gi} + requests: {cpu: 100m, memory: 512Mi} + size: 2Gi + type: postgres + user: user + dependencies: + build: [] + hard: [] + soft: [] + deployment: + auto: true + image: osb/accounts:latest + name: accounts + port: 8080 + replicas: 1 + resources: &id001 + limits: {cpu: 500m, memory: 1024Mi} + requests: {cpu: 10m, memory: 512Mi} + volume: null + domain: null + env: + - {name: KEYCLOAK_IMPORT, value: /tmp/realm.json} + - {name: KEYCLOAK_USER, value: admin} + - {name: KEYCLOAK_PASSWORD, value: metacell} + - {name: PROXY_ADDRESS_FORWARDING, value: 'true'} + - {name: DB_VENDOR, value: POSTGRES} + - {name: DB_ADDR, value: keycloak-postgres} + - {name: DB_DATABASE, value: auth_db} + - {name: DB_USER, value: user} + - {name: DB_PASSWORD, value: password} + - {name: JAVA_OPTS, value: '-server -Xms64m -Xmx896m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m + -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman + -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED + --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED'} + name: accounts + readinessProbe: {path: /auth/realms/master} + resources: + - {dst: /tmp/realm.json, name: realm-config, src: realm.json} + secrets: {api_user_password: password} + secured: false + service: {auto: true, name: accounts, port: 8080} + subdomain: accounts + test: + api: + autotest: true + checks: [all] + enabled: false + runParams: [] + e2e: {enabled: false, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + harvest: true + image: osb/accounts:latest + name: accounts + port: 8080 + resources: *id001 + task-images: {} + webclient: {id: web-client, secret: 452952ae-922c-4766-b912-7b106271e34b} + accounts_api: + harness: + aliases: [] + dependencies: + build: [cloudharness-base, cloudharness-flask] + hard: [accounts] + soft: [] + deployment: + auto: true + image: osb/accounts-api:latest + name: accounts-api + port: 8080 + replicas: 1 + resources: &id002 + limits: {cpu: 500m, memory: 500Mi} + requests: {cpu: 10m, memory: 32Mi} + volume: null + domain: null + livenessProbe: {path: /api/live} + name: accounts-api + readinessProbe: {path: /api/ready} + secrets: null + secured: false + service: {auto: true, name: accounts-api, port: 8080} + subdomain: api.accounts + test: + api: + autotest: true + checks: [all] + enabled: false + runParams: [] + e2e: {enabled: false, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + image: osb/accounts-api:latest + name: accounts-api + port: 8080 + resources: *id002 + task-images: {cloudharness-base: 'osb/cloudharness-base:latest', cloudharness-flask: 'osb/cloudharness-flask:latest'} + argo: + harness: + aliases: [] + dependencies: + build: [] + hard: [] + soft: [] + deployment: + auto: false + image: null + name: argo + port: 8080 + replicas: 1 + resources: &id003 + limits: {cpu: 500m, memory: 500Mi} + requests: {cpu: 10m, memory: 32Mi} + volume: null + domain: null + name: argo + secrets: null + secured: true + service: {auto: false, name: argo-server, port: 2746} + subdomain: argo + test: + api: + autotest: true + checks: [all] + enabled: false + runParams: [] + e2e: {enabled: false, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + image: null + name: argo + port: 8080 + resources: *id003 + serviceaccount: argo-workflows + task-images: {} + common: + harness: + aliases: [] + dependencies: + build: [cloudharness-flask] + hard: [] + soft: [] + deployment: + auto: true + image: osb/common:latest + name: common + port: 8080 + replicas: 1 + resources: &id004 + limits: {cpu: 200m, memory: 256Mi} + requests: {cpu: 50m, memory: 128Mi} + volume: null + domain: null + env: + - {name: SENTRY_DSN, value: 'https://fc2326dc50e34ac2b7188130e173f002@sentry.metacell.us/5'} + name: common + secrets: null + secured: false + service: {auto: true, name: common, port: 8080} + subdomain: common + test: + api: + autotest: true + checks: [all] + enabled: true + runParams: [] + e2e: {enabled: false, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + image: osb/common:latest + name: common + port: 8080 + resources: *id004 + task-images: {cloudharness-flask: 'osb/cloudharness-flask:latest'} + events: + harness: + aliases: [] + dependencies: + build: [] + hard: [] + soft: [] + deployment: + auto: false + image: null + name: events + port: 8080 + replicas: 1 + resources: &id005 + limits: {cpu: 500m, memory: 500Mi} + requests: {cpu: 10m, memory: 32Mi} + volume: null + domain: null + env: + - {name: ZK_HOSTS, value: 'zookeeper:2181'} + name: events + secrets: null + secured: false + service: {auto: false, name: events-ui, port: 80} + subdomain: events + test: + api: + autotest: true + checks: [all] + enabled: false + runParams: [] + e2e: {enabled: true, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + image: null + kafka: + name: bootstrap + port: 9092 + resources: + limits: {cpu: 500m, memory: 600Mi} + requests: {cpu: 50m, memory: 100Mi} + storage: 100Mi + name: events + port: 8080 + pzoo: + resources: + limits: {memory: 500Mi} + requests: {cpu: 10m, memory: 100Mi} + storage: 1Gi + resources: *id005 + task-images: {} + zoo: + resources: + limits: {memory: 500Mi} + requests: {cpu: 10m, memory: 100Mi} + storage: 1Gi + jupyterhub: + cull: {concurrency: 10, enabled: true, every: 60, maxAge: 0, removeNamedServers: true, + timeout: 360, users: false} + custom: {} + debug: {enabled: false} + fullnameOverride: '' + global: {safeToShowValues: false} + harness: + aliases: [] + dependencies: + build: [cloudharness-base] + hard: [] + soft: [accounts] + deployment: + auto: false + image: osb/jupyterhub:latest + name: jupyterhub + port: 8081 + replicas: 1 + resources: &id006 + limits: {cpu: 500m, memory: 500Mi} + requests: {cpu: 10m, memory: 32Mi} + volume: null + domain: null + name: jupyterhub + quotas: {quota-storage-max: 1.25, quota-ws-guaranteecpu: 0.2, quota-ws-guaranteemem: 0.5, + quota-ws-maxcpu: 1, quota-ws-maxmem: 1, quota-ws-open: 3} + secrets: null + secured: true + service: {auto: false, name: proxy-public, port: 80} + subdomain: hub + test: + api: + autotest: true + checks: [all] + enabled: false + runParams: [] + e2e: {enabled: false, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + hub: + activeServerLimit: null + allowNamedServers: true + annotations: {} + args: [] + authenticatePrometheus: null + baseUrl: / + command: [] + concurrentSpawnLimit: 64 + config: + JupyterHub: {admin_access: true, authenticator_class: ch} + consecutiveFailureLimit: 5 + containerSecurityContext: {allowPrivilegeEscalation: false, runAsGroup: 1000, + runAsUser: 1000} + cookieSecret: null + db: + password: null + pvc: + accessModes: [ReadWriteOnce] + annotations: {} + selector: {} + storage: 1Gi + storageClassName: null + subPath: null + type: sqlite-pvc + upgrade: null + url: null + deploymentStrategy: {type: Recreate} + existingSecret: null + extraConfig: {spawner: 'c.Spawner.args = []', timing: 'c.Spawner.port = 8000 + + c.Spawner.http_timeout = 300 + + c.Spawner.start_timeout = 300 + + c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": + "frame-ancestors ''self'' localhost localhost:3000 localhost:* *.osb.local + *.metacell.us *.opensourcebrain.org *.v2.opensourcebrain.org"}} + + '} + extraContainers: [] + extraEnv: {} + extraFiles: {} + extraPodSpec: {} + extraVolumeMounts: [] + extraVolumes: [] + fsGid: 1000 + image: + name: jupyterhub/k8s-hub + pullPolicy: null + pullSecrets: [] + tag: 1.1.3 + initContainers: [] + labels: {} + lifecycle: {} + livenessProbe: {enabled: true, failureThreshold: 30, initialDelaySeconds: 300, + periodSeconds: 10, timeoutSeconds: 3} + namedServerLimitPerUser: null + networkPolicy: + allowedIngressPorts: [] + egress: + - to: + - ipBlock: {cidr: 0.0.0.0/0} + enabled: false + ingress: [] + interNamespaceAccessLabels: ignore + nodeSelector: {} + pdb: {enabled: false, maxUnavailable: null, minAvailable: 1} + readinessProbe: {enabled: true, failureThreshold: 1000, initialDelaySeconds: 0, + periodSeconds: 2, timeoutSeconds: 1} + redirectToServer: null + resources: {} + service: + annotations: {} + extraPorts: [] + loadBalancerIP: null + ports: {nodePort: null} + type: ClusterIP + serviceAccount: + annotations: {} + services: {} + shutdownOnLogout: null + templatePaths: [] + templateVars: {} + tolerations: [] + image: osb/jupyterhub:latest + imagePullSecret: {automaticReferenceInjection: true, create: false, email: null, + password: null, registry: null, username: null} + imagePullSecrets: [] + ingress: + annotations: {} + enabled: false + hosts: [] + pathSuffix: null + pathType: Prefix + tls: [] + name: jupyterhub + nameOverride: null + port: 8081 + prePuller: + annotations: {} + containerSecurityContext: {allowPrivilegeEscalation: false, runAsGroup: 65534, + runAsUser: 65534} + continuous: {enabled: false} + extraImages: {} + extraTolerations: [] + hook: + containerSecurityContext: {allowPrivilegeEscalation: false, runAsGroup: 65534, + runAsUser: 65534} + enabled: false + image: + name: jupyterhub/k8s-image-awaiter + pullPolicy: null + pullSecrets: [] + tag: 1.1.3 + nodeSelector: {} + podSchedulingWaitDuration: 10 + pullOnlyOnChanges: true + resources: {} + serviceAccount: + annotations: {} + tolerations: [] + pause: + containerSecurityContext: {allowPrivilegeEscalation: false, runAsGroup: 65534, + runAsUser: 65534} + image: + name: k8s.gcr.io/pause + pullPolicy: null + pullSecrets: [] + tag: '3.5' + pullProfileListImages: false + resources: {} + proxy: + annotations: {} + chp: + containerSecurityContext: {allowPrivilegeEscalation: false, runAsGroup: 65534, + runAsUser: 65534} + defaultTarget: null + errorTarget: null + extraCommandLineFlags: [] + extraEnv: {} + extraPodSpec: {} + image: + name: jupyterhub/configurable-http-proxy + pullPolicy: null + pullSecrets: [] + tag: 4.5.0 + livenessProbe: {enabled: true, initialDelaySeconds: 60, periodSeconds: 10} + networkPolicy: + allowedIngressPorts: [http, https] + egress: + - to: + - ipBlock: {cidr: 0.0.0.0/0} + enabled: false + ingress: [] + interNamespaceAccessLabels: ignore + nodeSelector: {} + pdb: {enabled: false, maxUnavailable: null, minAvailable: 1} + readinessProbe: {enabled: true, failureThreshold: 1000, initialDelaySeconds: 0, + periodSeconds: 2} + resources: {} + tolerations: [] + deploymentStrategy: {rollingUpdate: null, type: Recreate} + https: + enabled: false + hosts: [] + letsencrypt: {acmeServer: 'https://acme-v02.api.letsencrypt.org/directory', + contactEmail: null} + manual: {cert: null, key: null} + secret: {crt: tls.crt, key: tls.key, name: null} + type: letsencrypt + labels: {} + secretSync: + containerSecurityContext: {allowPrivilegeEscalation: false, runAsGroup: 65534, + runAsUser: 65534} + image: + name: jupyterhub/k8s-secret-sync + pullPolicy: null + pullSecrets: [] + tag: 1.1.3 + resources: {} + secretToken: null + service: + annotations: {} + disableHttpPort: false + extraPorts: [] + labels: {} + loadBalancerIP: null + loadBalancerSourceRanges: [] + nodePorts: {http: null, https: null} + type: NodePort + traefik: + containerSecurityContext: {allowPrivilegeEscalation: false, runAsGroup: 65534, + runAsUser: 65534} + extraDynamicConfig: {} + extraEnv: {} + extraPodSpec: {} + extraPorts: [] + extraStaticConfig: {} + extraVolumeMounts: [] + extraVolumes: [] + hsts: {includeSubdomains: false, maxAge: 15724800, preload: false} + image: + name: traefik + pullPolicy: null + pullSecrets: [] + tag: v2.4.11 + labels: {} + networkPolicy: + allowedIngressPorts: [http, https] + egress: + - to: + - ipBlock: {cidr: 0.0.0.0/0} + enabled: true + ingress: [] + interNamespaceAccessLabels: ignore + nodeSelector: {} + pdb: {enabled: false, maxUnavailable: null, minAvailable: 1} + resources: {} + serviceAccount: + annotations: {} + tolerations: [] + rbac: {enabled: true} + resources: *id006 + scheduling: + corePods: + nodeAffinity: {matchNodePurpose: prefer} + tolerations: + - {effect: NoSchedule, key: hub.jupyter.org/dedicated, operator: Equal, value: core} + - {effect: NoSchedule, key: hub.jupyter.org_dedicated, operator: Equal, value: core} + podPriority: {defaultPriority: 0, enabled: false, globalDefault: false, userPlaceholderPriority: -10} + userPlaceholder: + containerSecurityContext: {allowPrivilegeEscalation: false, runAsGroup: 65534, + runAsUser: 65534} + enabled: true + image: + name: k8s.gcr.io/pause + pullPolicy: null + pullSecrets: [] + tag: '3.5' + replicas: 0 + resources: {} + userPods: + nodeAffinity: {matchNodePurpose: prefer} + tolerations: + - {effect: NoSchedule, key: hub.jupyter.org/dedicated, operator: Equal, value: user} + - {effect: NoSchedule, key: hub.jupyter.org_dedicated, operator: Equal, value: user} + userScheduler: + containerSecurityContext: {allowPrivilegeEscalation: false, runAsGroup: 65534, + runAsUser: 65534} + enabled: false + extraPodSpec: {} + image: + name: k8s.gcr.io/kube-scheduler + pullPolicy: null + pullSecrets: [] + tag: v1.19.13 + logLevel: 4 + nodeSelector: {} + pdb: {enabled: true, maxUnavailable: 1, minAvailable: null} + plugins: + score: + disabled: + - {name: SelectorSpread} + - {name: TaintToleration} + - {name: PodTopologySpread} + - {name: NodeResourcesBalancedAllocation} + - {name: NodeResourcesLeastAllocated} + - {name: NodePreferAvoidPods} + - {name: NodeAffinity} + - {name: InterPodAffinity} + - {name: ImageLocality} + enabled: + - {name: NodePreferAvoidPods, weight: 161051} + - {name: NodeAffinity, weight: 14631} + - {name: InterPodAffinity, weight: 1331} + - {name: NodeResourcesMostAllocated, weight: 121} + - {name: ImageLocality, weight: 11} + replicas: 2 + resources: {} + serviceAccount: + annotations: {} + tolerations: [] + singleuser: + cloudMetadata: {blockWithIptables: false} + cmd: /usr/local/bin/start-singleuser.sh + cpu: {guarantee: 0.2, limit: 1} + defaultUrl: null + events: true + extraAnnotations: {} + extraContainers: [] + extraEnv: {} + extraFiles: {} + extraLabels: {hub.jupyter.org/network-access-hub: 'true'} + extraNodeAffinity: + preferred: [] + required: [] + extraPodAffinity: + preferred: [] + required: [] + extraPodAntiAffinity: + preferred: [] + required: [] + extraPodConfig: {} + extraResource: + guarantees: {} + limits: {} + extraTolerations: [] + fsGid: 100 + image: + name: jupyter/base-notebook + pullPolicy: null + pullSecrets: [] + tag: hub-1.4.2 + initContainers: [] + lifecycleHooks: {} + memory: {guarantee: 0.5G, limit: 1G} + networkPolicy: + allowedIngressPorts: [] + egress: + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: [169.254.169.254/32] + enabled: false + ingress: [] + interNamespaceAccessLabels: ignore + networkTools: + image: + name: jupyterhub/k8s-network-tools + pullPolicy: null + pullSecrets: [] + tag: 1.1.3 + nodeSelector: {} + podNameTemplate: null + profileList: [] + serviceAccountName: null + startTimeout: 300 + storage: + capacity: 2Mi + dynamic: + pvcNameTemplate: osb-user-{userid} + storageAccessModes: [ReadWriteOnce] + storageClass: '{{namespace}}-nfs-client' + volumeNameTemplate: osb-user-{userid} + extraLabels: {} + extraVolumeMounts: [] + extraVolumes: [] + homeMountPath: /opt/user + static: {pvcName: null, subPath: '{username}'} + type: dynamic + uid: 1000 + task-images: {cloudharness-base: 'osb/cloudharness-base:latest'} + jupyterlab_minimal: + harness: + aliases: [] + dependencies: + build: [cloudharness-base] + hard: [jupyterhub] + soft: [] + deployment: + auto: false + image: osb/jupyterlab-minimal:latest + name: jupyterlab-minimal + port: 8080 + replicas: 1 + resources: &id007 + limits: {cpu: 500m, memory: 500Mi} + requests: {cpu: 10m, memory: 32Mi} + volume: null + domain: null + jupyterhub: + applicationHook: osb_jupyter.change_pod_manifest + args: [--debug, --NotebookApp.default_url=/lab, --NotebookApp.notebook_dir=/opt/workspace] + extraConfig: {timing: 'c.Spawner.port = 8000 + + c.Spawner.http_timeout = 300 + + c.Spawner.start_timeout = 300 + + + c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": + "frame-ancestors ''self'' localhost:3000 *.osb.local osb.local localhost + *.metacell.us *.opensourcebrain.org"}} + + '} + name: jupyterlab-minimal + secrets: null + secured: false + service: {auto: false, name: proxy-public, port: 80} + subdomain: notebooks + test: + api: + autotest: true + checks: [all] + enabled: false + runParams: [] + e2e: {enabled: false, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + image: osb/jupyterlab-minimal:latest + name: jupyterlab-minimal + port: 8080 + resources: *id007 + task-images: {cloudharness-base: 'osb/cloudharness-base:latest'} + nfsserver: + affinity: + podAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app + operator: In + values: [nfs-server] + topologyKey: kubernetes.io/hostname + harness: + aliases: [] + dependencies: + build: [] + hard: [] + soft: [] + deployment: + auto: false + image: osb/nfsserver:latest + name: nfsserver + port: 8080 + replicas: 1 + resources: &id008 + limits: {cpu: 100m, memory: 128Mi} + requests: {cpu: 10m, memory: 128Mi} + volume: null + domain: null + name: nfsserver + secrets: null + secured: false + service: {auto: false, name: nfsserver, port: 80} + subdomain: nfsserver + test: + api: + autotest: true + checks: [all] + enabled: false + runParams: [] + e2e: {enabled: false, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + image: osb/nfsserver:latest + labels: {} + leaderElection: {enabled: true} + name: nfsserver + nameOverride: nfs-provisioner + nfs: + mountOptions: [nolock, nfsvers=3, local_lock=all] + path: /exports + reclaimPolicy: Retain + server: null + useDNS: false + volumeName: nfs-subdir-external-provisioner-root + nodeSelector: {} + podAnnotations: {} + podSecurityContext: {} + podSecurityPolicy: {enabled: false} + port: 8080 + rbac: {create: true} + replicaCount: 1 + resources: *id008 + securityContext: {} + server: {diskSize: 10Gi} + serviceAccount: + annotations: {} + create: true + name: null + storageClass: + accessModes: ReadWriteOnce + allowVolumeExpansion: true + annotations: {} + archiveOnDelete: true + create: true + defaultClass: false + name: nfs-client + onDelete: null + pathPattern: null + reclaimPolicy: Delete + volumeBindingMode: Immediate + strategyType: Recreate + task-images: {} + tolerations: [] + notifications: + harness: + aliases: [] + dependencies: + build: [cloudharness-base] + hard: [] + soft: [] + deployment: + auto: true + image: osb/notifications:latest + name: notifications + port: 8080 + replicas: 1 + resources: &id009 + limits: {cpu: 100m, memory: 256Mi} + requests: {cpu: 25m, memory: 64Mi} + volume: null + domain: null + events: + cdc: + - app: workspaces + types: + - events: [create] + name: workspace + - events: [create] + name: osbrepository + name: notifications + secrets: {email-password: null, email-user: null} + secured: false + service: {auto: false, name: notifications, port: 80} + subdomain: notifications + test: + api: + autotest: true + checks: [all] + enabled: false + runParams: [] + e2e: {enabled: false, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + image: osb/notifications:latest + name: notifications + notification: + channels: + admins: null + log: + adapter: email + backends: [console] + from: info@example.com + templateFolder: text + to: [info@example.com] + operations: + create: + channels: [admins] + subject: New {{ message_type }} - {{ domain }} + template: model-instance-create + delete: + channels: [admins] + subject: Delete {{ message_type }} - {{ domain }} + template: model-instance-delete + update: + channels: [admins] + subject: Update {{ message_type }} - {{ domain }} + template: model-instance-update + port: 8080 + resources: *id009 + task-images: {cloudharness-base: 'osb/cloudharness-base:latest'} + osb_portal: + harness: + accounts: + users: + - clientRoles: [] + email: testuser@opensourcebrain.org + realmRoles: [] + username: testuser + aliases: [] + dependencies: + build: [cloudharness-base, cloudharness-frontend-build] + hard: [] + soft: [accounts-api, workspaces, jupyterhub, notifications, jupyterlab-minimal] + deployment: + auto: true + image: osb/osb-portal:latest + name: osb-portal + port: 80 + replicas: 1 + resources: &id010 + limits: {cpu: 500m, memory: 500Mi} + requests: {cpu: 10m, memory: 32Mi} + volume: null + domain: null + name: osb-portal + resources: + - {dst: /usr/share/nginx/html/keycloak.json, name: keycloak, src: keycloak.json} + secrets: null + secured: false + sentry: false + service: {auto: true, name: osb-portal, port: 80} + subdomain: www + test: + api: + autotest: true + checks: [all] + enabled: false + runParams: [] + e2e: {enabled: true, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: + - {name: workspaces} + - {name: common} + - {name: accounts-api} + image: osb/osb-portal:latest + name: osb-portal + port: 80 + resources: *id010 + task-images: {cloudharness-base: 'osb/cloudharness-base:latest', cloudharness-frontend-build: 'osb/cloudharness-frontend-build:latest'} + workflows: + harness: + aliases: [] + dependencies: + build: [cloudharness-flask] + hard: [argo] + soft: [] + deployment: + auto: true + image: osb/workflows:latest + name: workflows + port: 8080 + replicas: 1 + resources: &id011 + limits: {cpu: 500m, memory: 500Mi} + requests: {cpu: 10m, memory: 32Mi} + volume: null + domain: null + name: workflows + secrets: null + secured: false + service: {auto: true, name: workflows, port: 8080} + subdomain: workflows + test: + api: + autotest: true + checks: [all] + enabled: true + runParams: [] + e2e: {enabled: false, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + image: osb/workflows:latest + name: workflows + port: 8080 + resources: *id011 + task-images: {cloudharness-flask: 'osb/cloudharness-flask:latest', workflows-extract-download: 'osb/workflows-extract-download:latest', + workflows-notify-queue: 'osb/workflows-notify-queue:latest', workflows-send-result-event: 'osb/workflows-send-result-event:latest'} + workspaces: + harness: + aliases: [] + database: + auto: false + datavolume: /opt/data/ + image: postgres:13 + image_ref: null + initialdb: workspaces + name: workspaces-postgres-host + pass: metacell + password: secret + pgdata: /opt/data/pgdata + port: 5432 + postgres: + image: postgres:13 + initialdb: cloudharness + ports: + - {name: http, port: 5432} + resources: + limits: {cpu: 500m, memory: 256Mi} + requests: {cpu: 200m, memory: 128Mi} + size: 1Gi + type: postgres + user: workspace + dependencies: + build: [cloudharness-base, cloudharness-flask] + hard: [argo, accounts] + soft: [events, common, workflows, notifications, nfsserver] + deployment: + auto: true + image: osb/workspaces:latest + name: workspaces + port: 8080 + replicas: 1 + resources: &id012 + limits: {cpu: 1500m, memory: 512Mi} + requests: {cpu: 200m, memory: 128Mi} + volume: {mountpath: /usr/src/app/workspaces/static/workspaces, name: workspaces-images, + size: 4G} + domain: null + livenessProbe: {initialDelaySeconds: 5, path: /api/live, periodSeconds: 15} + name: workspaces + quotas: {quota-ws-max: 5, quota-ws-storage-max: 1} + readinessProbe: {initialDelaySeconds: 10, path: /api/ready, periodSeconds: 15} + secrets: {github-token: null, github-user: null} + secured: false + sentry: false + service: {auto: true, name: workspaces, port: 8080} + subdomain: workspaces + test: + api: + autotest: true + checks: [all] + enabled: false + runParams: [] + e2e: {enabled: false, ignoreConsoleErrors: false, ignoreRequestErrors: false, + smoketest: true} + unit: + commands: [pytest -c /usr/src/app/] + enabled: true + uri_role_mapping: + - roles: [administrator] + uri: /* + - {uri: /api/openapi.json, white-listed: true} + use_services: [] + image: osb/workspaces:latest + name: workspaces + port: 8080 + resources: *id012 + task-images: {cloudharness-base: 'osb/cloudharness-base:latest', cloudharness-flask: 'osb/cloudharness-flask:latest', + workspaces-dandi-copy: 'osb/workspaces-dandi-copy:latest', workspaces-figshare-copy: 'osb/workspaces-figshare-copy:latest', + workspaces-github-copy: 'osb/workspaces-github-copy:latest', workspaces-scan-workspace: 'osb/workspaces-scan-workspace:latest'} + workspace_size: 10Mi +backup: + active: false + dir: /backups + keep_days: '7' + keep_months: '6' + keep_weeks: '4' + resources: + limits: {cpu: 50m, memory: 64Mi} + requests: {cpu: 25m, memory: 32Mi} + schedule: '*/5 * * * *' + suffix: .gz + volumesize: 2Gi +domain: osb.local +env: +- {name: CH_VERSION, value: 0.0.1} +- {name: CH_CHART_VERSION, value: 0.0.1} +- {name: CH_NFSSERVER_SUBDOMAIN, value: nfsserver} +- {name: CH_NFSSERVER_NAME, value: nfsserver} +- {name: CH_NOTIFICATIONS_SUBDOMAIN, value: notifications} +- {name: CH_NOTIFICATIONS_NAME, value: notifications} +- {name: CH_ACCOUNTS_SUBDOMAIN, value: accounts} +- {name: CH_ACCOUNTS_NAME, value: accounts} +- {name: CH_JUPYTERHUB_SUBDOMAIN, value: hub} +- {name: CH_JUPYTERHUB_NAME, value: jupyterhub} +- {name: CH_ARGO_SUBDOMAIN, value: argo} +- {name: CH_ARGO_NAME, value: argo} +- {name: CH_COMMON_SUBDOMAIN, value: common} +- {name: CH_COMMON_NAME, value: common} +- {name: CH_EVENTS_SUBDOMAIN, value: events} +- {name: CH_EVENTS_NAME, value: events} +- {name: CH_WORKFLOWS_SUBDOMAIN, value: workflows} +- {name: CH_WORKFLOWS_NAME, value: workflows} +- {name: CH_WORKSPACES_SUBDOMAIN, value: workspaces} +- {name: CH_WORKSPACES_NAME, value: workspaces} +- {name: CH_ACCOUNTS_API_SUBDOMAIN, value: api.accounts} +- {name: CH_ACCOUNTS_API_NAME, value: accounts-api} +- {name: CH_OSB_PORTAL_SUBDOMAIN, value: www} +- {name: CH_OSB_PORTAL_NAME, value: osb-portal} +- {name: CH_JUPYTERLAB_MINIMAL_SUBDOMAIN, value: notebooks} +- {name: CH_JUPYTERLAB_MINIMAL_NAME, value: jupyterlab-minimal} +- {name: CH_DOMAIN, value: osb.local} +- {name: CH_IMAGE_REGISTRY, value: ''} +- {name: CH_IMAGE_TAG, value: latest} +ingress: + enabled: true + letsencrypt: {email: filippo@metacell.us} + name: cloudharness-ingress + ssl_redirect: false +local: true +localIp: 172.21.58.243 +mainapp: osb-portal +name: osb +namespace: osblocal +privenv: +- {name: CH_SECRET, value: In God we trust; all others must bring data. ― W. Edwards + Deming} +registry: {name: '', secret: ''} +secured_gatekeepers: false +smtp: {host: smtp.ionos.co.uk, port: 587} +tag: latest +task-images: {cloudharness-base: 'osb/cloudharness-base:latest', cloudharness-flask: 'osb/cloudharness-flask:latest', + cloudharness-frontend-build: 'osb/cloudharness-frontend-build:latest', workflows-extract-download: 'osb/workflows-extract-download:latest', + workflows-notify-queue: 'osb/workflows-notify-queue:latest', workflows-send-result-event: 'osb/workflows-send-result-event:latest', + workspaces-dandi-copy: 'osb/workspaces-dandi-copy:latest', workspaces-figshare-copy: 'osb/workspaces-figshare-copy:latest', + workspaces-github-copy: 'osb/workspaces-github-copy:latest', workspaces-scan-workspace: 'osb/workspaces-scan-workspace:latest'} +tls: false diff --git a/applications/workspaces/server/tox.ini b/applications/workspaces/server/tox.ini index 540564e9..bfb421f4 100644 --- a/applications/workspaces/server/tox.ini +++ b/applications/workspaces/server/tox.ini @@ -2,7 +2,7 @@ addopts=--tb=short [tox] -envlist = py37,isort-check,isort-fix,black-check,black-fix +envlist = py39 skipsdist = true @@ -13,17 +13,16 @@ setenv = commands = pytest --cov=workspaces deps = - -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt -[testenv:black-check] -commands = black --line-length=120 --check workspaces +; [testenv:black-check] +; commands = black --line-length=120 --check workspaces -[testenv:black-fix] -commands = black --line-length=120 workspaces +; [testenv:black-fix] +; commands = black --line-length=120 workspaces -[testenv:isort-check] -commands = isort -m 3 -tc -l 120 -rc -c workspaces +; [testenv:isort-check] +; commands = isort -m 3 -tc -l 120 -rc -c workspaces -[testenv:isort-fix] -commands = isort -m 3 -tc -l 120 -rc workspaces +; [testenv:isort-fix] +; commands = isort -m 3 -tc -l 120 -rc workspaces diff --git a/applications/workspaces/server/workspaces/config.py b/applications/workspaces/server/workspaces/config.py index e575f3a2..0b3accf1 100644 --- a/applications/workspaces/server/workspaces/config.py +++ b/applications/workspaces/server/workspaces/config.py @@ -28,6 +28,8 @@ class Config(object): APP_NAME = "workspaces" WSMGR_HOSTNAME = socket.gethostname() WSMGR_IPADDRESS = socket.gethostbyname(WSMGR_HOSTNAME) + # set the max number of workspaces per user + MAX_NUMBER_WORKSPACES_PER_USER = 3 try: CH_NAMESPACE = conf.get_configuration()["namespace"] diff --git a/applications/workspaces/server/workspaces/controllers/workspace_controller.py b/applications/workspaces/server/workspaces/controllers/workspace_controller.py index 4c9cc90a..50e6d674 100644 --- a/applications/workspaces/server/workspaces/controllers/workspace_controller.py +++ b/applications/workspaces/server/workspaces/controllers/workspace_controller.py @@ -8,7 +8,7 @@ from workspaces.repository.model_repository import WorkspaceImageRepository, WorkspaceRepository, db from workspaces.repository.models import WorkspaceEntity, WorkspaceImage from workspaces.helpers.etl_helpers import copy_origins -from workspaces.service.model_service import NotAuthorized, WorkspaceService +from workspaces.service.model_service import NotAuthorized, NotAllowed, WorkspaceService def _save_image(id_=None, image=None, filename_base=None): ext = mimetypes.guess_extension(image.mimetype) @@ -100,3 +100,5 @@ def workspace_clone(id_, body=None): return ws.to_dict() except NotAuthorized: return "Not authorized", 401 + except NotAllowed: + return "Not allowed", 405 diff --git a/applications/workspaces/server/workspaces/openapi/openapi.yaml b/applications/workspaces/server/workspaces/openapi/openapi.yaml index 02e0ac12..b7b51265 100644 --- a/applications/workspaces/server/workspaces/openapi/openapi.yaml +++ b/applications/workspaces/server/workspaces/openapi/openapi.yaml @@ -121,6 +121,8 @@ paths: description: Save successful. '400': description: The Workspace already exists. + '405': + description: Not allowed to create a new workspace security: - bearerAuth: [] diff --git a/applications/workspaces/server/workspaces/service/kubernetes.py b/applications/workspaces/server/workspaces/service/kubernetes.py index 0519d9ae..77603155 100644 --- a/applications/workspaces/server/workspaces/service/kubernetes.py +++ b/applications/workspaces/server/workspaces/service/kubernetes.py @@ -11,4 +11,4 @@ def create_volume(name, size="2G"): from cloudharness.service import pvc - pvc.create_persistent_volume_claim(name, size, log) + pvc.create_persistent_volume_claim(name=name, size=size, logger=log, useNFS=True) diff --git a/applications/workspaces/server/workspaces/service/model_service.py b/applications/workspaces/server/workspaces/service/model_service.py index 5ebe6c0f..0b1fbd33 100644 --- a/applications/workspaces/server/workspaces/service/model_service.py +++ b/applications/workspaces/server/workspaces/service/model_service.py @@ -31,6 +31,7 @@ from workspaces.service import osbrepository as osbrepository_helper from workspaces.service.kubernetes import create_volume from workspaces.service.auth import get_auth_client, keycloak_user_id +from workspaces.service.user_quota_service import get_pvc_size, get_max_workspaces_for_user from workspaces.utils import dao_entity2dict @@ -47,6 +48,10 @@ class NotAuthorized(Exception): pass +class NotAllowed(Exception): + pass + + class UserService(): def get(self, user_id): @@ -131,9 +136,6 @@ def is_authorized(self, object): raise NotImplementedError( f"Authorization not implemented for {self.__class__.__name__}") - - - class WorkspaceService(BaseModelService): repository = WorkspaceRepository() @@ -144,11 +146,25 @@ class WorkspaceService(BaseModelService): @staticmethod def get_pvc_name(workspace_id): return f"workspace-{workspace_id}" + + def check_max_num_workspaces_per_user(self, user_id=None): + if not user_id: + user_id = keycloak_user_id() + if not get_auth_client().user_has_realm_role(user_id=user_id, role="administrator"): + # for non admin users check if max number of ws per user limit is reached + num_ws_current_user = self.repository.search(user_id=user_id).total + max_num_ws_current_user = get_max_workspaces_for_user(user_id) + if num_ws_current_user >= max_num_ws_current_user: + raise NotAllowed( + f"Max number of {max_num_ws_current_user} workspaces " \ + "limit exceeded" + ) @send_event(message_type="workspace", operation="create") def post(self, body): if 'user_id' not in body: body['user_id'] = keycloak_user_id() + self.check_max_num_workspaces_per_user(body['user_id']) for r in body.get("resources", []): r.update({"origin": json.dumps(r.get("origin"))}) workspace = Workspace.from_dict(body) # Validate @@ -164,10 +180,13 @@ def put(self, body, id_): def get_workspace_volume_size(self, ws: Workspace): # Place to change whenever we implement user or workspace based sizing - return get_configuration('workspaces').conf['workspace_size'] + user_id = keycloak_user_id() + return get_pvc_size(user_id) @send_event(message_type="workspace", operation="create") def clone(self, workspace_id): + user_id = keycloak_user_id() + self.check_max_num_workspaces_per_user(user_id) from workspaces.service.workflow import clone_workspaces_content workspace = self.get(workspace_id) if workspace is None: @@ -177,7 +196,7 @@ def clone(self, workspace_id): cloned = dict( name=f"Clone of {workspace['name']}", tags=workspace['tags'], - user_id=keycloak_user_id(), + user_id=user_id, description=workspace['description'], publicable=False, diff --git a/applications/workspaces/server/workspaces/service/osbrepository/adapters/dandiadapter.py b/applications/workspaces/server/workspaces/service/osbrepository/adapters/dandiadapter.py index 7a11ea08..6bcaf688 100644 --- a/applications/workspaces/server/workspaces/service/osbrepository/adapters/dandiadapter.py +++ b/applications/workspaces/server/workspaces/service/osbrepository/adapters/dandiadapter.py @@ -1,9 +1,14 @@ +import concurrent.futures +import os import re -import requests import sys -import concurrent.futures +from typing import List, TypedDict + +import requests from cloudharness import log as logger -from workspaces.models import DandiRepositoryResource, RepositoryResource, RepositoryResourceNode, RepositoryInfo + +from workspaces.models import DandiRepositoryResource, RepositoryInfo, RepositoryResource, RepositoryResourceNode + from .utils import add_to_tree MAX_WORKERS = 20 @@ -13,16 +18,28 @@ class DandiException(Exception): pass +class DandiAsset(TypedDict): + asset_id: str + url: str + + +class DandiResource(TypedDict): + path: str + version: int + aggregate_files: int + aggregate_size: int + asset: DandiAsset + + class DandiAdapter: def __init__(self, osbrepository, uri=None): self.osbrepository = osbrepository self.uri = uri if uri else osbrepository.uri self.api_url = "https://api.dandiarchive.org/api" - self.futures = [] + try: self.dandiset_id = re.search( - ".*/dandiset/(.*?)[/.*$|$]", - self.uri + "/").group(1) + ".*/dandiset/(.*?)[/.*$|$]", self.uri + "/").group(1) except AttributeError: raise DandiException(f"{self.uri} is not a Dandi set url.") @@ -40,60 +57,78 @@ def get_json(self, uri): except Exception as e: raise DandiException("Unexpected error:", sys.exc_info()[0]) - def getFolderContents(self, context, path_prefix): + def __retrieve_folder_contents(self, context, path_prefix) -> List[DandiResource]: uri = f"{self.api_url}/dandisets/{self.dandiset_id}/versions/{context}/assets/paths/?path_prefix={path_prefix}" - return uri, self.get_json(uri)['results'] + return self.get_json(uri)["results"] - def getFiles(self, tree, context, path_prefix=""): + def __retrieve_files(self, tree_node, context, path_prefix=""): logger.debug(f"getFiles for {path_prefix}") - path, contents = self.getFolderContents(context, path_prefix) - if "files" in contents: - for key2, dandi_file in contents["files"].items(): - # we save the version in the url query param for later usage in the download task - download_url = f"{self.api_url}/assets/{dandi_file['asset_id']}?folder={context}/{path_prefix}" - add_to_tree( - tree=tree, - tree_path=dandi_file["path"].split("/"), - path=download_url, - size=dandi_file["size"], - timestamp_modified=dandi_file["modified"], - osbrepository_id=self.osbrepository.id, - ) - if "folders" in contents: - with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: - for key, dandi_folder in contents["folders"].items(): - new_path_prefix = f"{path_prefix}/{key}".strip("/") + contents = self.__retrieve_folder_contents(context, path_prefix) + futures = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: + for resource in contents: + if resource["asset"]: # downloadable resource + dandi_file = resource["asset"] + # we save the version in the url query param for later usage in the download task + asset_path = f"{self.api_url}/assets/{dandi_file['asset_id']}?folder={context}/{path_prefix}" + tree_node.children.append( + RepositoryResourceNode( + resource=RepositoryResource( + path=asset_path, + name=os.path.basename(resource["path"]), + size=resource["aggregate_size"], + # timestamp_modified=dandi_file["modified"], + osbrepository_id=self.osbrepository.id, + ), + children=[], + ) + ) + else: # a folder + new_path_prefix = resource['path'] folder_url = f"{self.api_url}/dandisets/{self.dandiset_id}/versions/{context}/assets/paths/?path_prefix={new_path_prefix}&folder={context}/{new_path_prefix}" - add_to_tree( - tree=tree, - tree_path=f"{new_path_prefix}".split("/"), - path=folder_url, - osbrepository_id=self.osbrepository.id, + + folder_node = RepositoryResourceNode( + resource=RepositoryResource( + path=folder_url, + name=os.path.basename(resource["path"]), + osbrepository_id=self.osbrepository.id, + ), + children=[], ) - self.futures.append( + + tree_node.children.append(folder_node) + # self.__retrieve_files( + # context=context, tree_node=folder_node, path_prefix=new_path_prefix) + futures.append( executor.submit( - self.getFiles, + self.__retrieve_files, context=context, - tree=tree, + tree_node=folder_node, path_prefix=new_path_prefix, ) ) + return futures def get_info(self) -> RepositoryInfo: resp = self.get_json(f"{self.api_url}/dandisets/{self.dandiset_id}") base_info = resp["most_recent_published_version"] or resp["draft_version"] - detailed_info= self._get_dandi_info(base_info["version"]) - return RepositoryInfo(name=base_info["name"], contexts=self.get_contexts(), tags=detailed_info["metadata"].get("keywords", []), summary=detailed_info["metadata"].get("description", "")) + detailed_info = self._get_dandi_info(base_info["version"]) + return RepositoryInfo( + name=base_info["name"], + contexts=self.get_contexts(), + tags=detailed_info["metadata"].get("keywords", []), + summary=detailed_info["metadata"].get("description", ""), + ) def get_base_uri(self): return f"https://dandiarchive.org/dandiset/{self.dandiset_id}" def get_contexts(self): versions = self.get_json( - f"{self.api_url}/dandisets/{self.dandiset_id}/versions")["results"] + f"{self.api_url}/dandisets/{self.dandiset_id}/versions/")["results"] return [context["version"] for context in versions] - def createTreeRoot(self): + def create_tree_node(self): return RepositoryResourceNode( resource=DandiRepositoryResource( name="/", @@ -103,10 +138,10 @@ def createTreeRoot(self): children=[], ) - def get_resources(self, context): - tree = self.createTreeRoot() - self.getFiles(tree, context) - for future in concurrent.futures.as_completed(self.futures): + def get_resources(self, context) -> RepositoryResourceNode: + tree = self.create_tree_node() + futures = self.__retrieve_files(tree, context) + for future in concurrent.futures.as_completed(futures): pass return tree @@ -140,10 +175,10 @@ def create_copy_task(self, workspace_id, name, path): "(.*)folder=.*$", path).group(1).strip("&").strip("?") if "path_prefix" in downloadpath: context = folder.split("/")[0] - tree = self.createTreeRoot() - self.getFiles( + tree = self.create_tree_node() + futures = self.__retrieve_files( tree, context, path_prefix="/".join(folder.split("/")[1:])) - for future in concurrent.futures.as_completed(self.futures): + for future in concurrent.futures.as_completed(futures): pass tasks = [] self._create_copy_task_assets_of_folder(workspace_id, tree, tasks) @@ -160,9 +195,7 @@ def _create_copy_task_assets_of_folder(self, workspace_id, tree, tasks): else: resource = tree.resource task = self._create_copy_asset_task( - workspace_id=workspace_id, - name=resource.name, - path=resource.path) + workspace_id=workspace_id, name=resource.name, path=resource.path) tasks.append(task) def _create_copy_asset_task(self, workspace_id, name, path): @@ -172,6 +205,7 @@ def _create_copy_asset_task(self, workspace_id, name, path): downloadpath = re.search("(.*)\?folder=.*$", path).group(1) print(f"Copy task: {name} - {folder} - {downloadpath}") import workspaces.service.workflow as workflow + return workflow.create_copy_task( image_name="workspaces-dandi-copy", workspace_id=workspace_id, diff --git a/applications/workspaces/server/workspaces/service/user_quota_service.py b/applications/workspaces/server/workspaces/service/user_quota_service.py index 896a2103..f4e0fac6 100644 --- a/applications/workspaces/server/workspaces/service/user_quota_service.py +++ b/applications/workspaces/server/workspaces/service/user_quota_service.py @@ -1,3 +1,14 @@ +from cloudharness.applications import get_configuration +from cloudharness.auth.quota import get_user_quotas + def get_pvc_size(user_id): - return "2Gi" + application_config = get_configuration('workspaces') + user_quotas = get_user_quotas(application_config, user_id=user_id) + return f"{user_quotas.get('quota-ws-storage-max', '0.1')}Gi" + + +def get_max_workspaces_for_user(user_id) -> int: + application_config = get_configuration('workspaces') + user_quotas = get_user_quotas(application_config, user_id=user_id) + return int(user_quotas.get('quota-ws-max', '1')) diff --git a/applications/workspaces/server/workspaces/service/workflow.py b/applications/workspaces/server/workspaces/service/workflow.py index 0fed72f9..5a491028 100644 --- a/applications/workspaces/server/workspaces/service/workflow.py +++ b/applications/workspaces/server/workspaces/service/workflow.py @@ -47,7 +47,7 @@ def delete_resource(workspace_resource, pvc_name, resource_path: str): def run_copy_tasks(workspace_id, tasks): pvc_name = WorkspaceService.get_pvc_name(workspace_id) - shared_directory = f"{pvc_name}:/project_download" + shared_directory = f"{pvc_name}:/project_download:rwx" op = operations.SimpleDagOperation( f"osb-copy-tasks-job", tasks, @@ -62,7 +62,7 @@ def run_copy_tasks(workspace_id, tasks): def create_task(image_name, workspace_id, **kwargs): pvc_name = WorkspaceService.get_pvc_name(workspace_id) - shared_directory = f"{pvc_name}:/project_download" + shared_directory = f"{pvc_name}:/project_download:rwx" return tasks.CustomTask( name=f"{image_name}-{str(uuid.uuid4())[:8]}", image_name=image_name, @@ -90,7 +90,7 @@ def clone_workspaces_content(source_ws_id, dest_ws_id): source_pvc_name = WorkspaceService.get_pvc_name(source_ws_id) dest_pvc_name = WorkspaceService.get_pvc_name(dest_ws_id) source_volume = f"{source_pvc_name}:/source" - dest_volume = f"{dest_pvc_name}:/project_download" + dest_volume = f"{dest_pvc_name}:/project_download:rwx" copy_task = tasks.BashTask( name=f"clone-workspace-data", diff --git a/applications/workspaces/server/workspaces/views/api/rest_api_views.py b/applications/workspaces/server/workspaces/views/api/rest_api_views.py index 5fd4febc..6cae72c4 100644 --- a/applications/workspaces/server/workspaces/views/api/rest_api_views.py +++ b/applications/workspaces/server/workspaces/views/api/rest_api_views.py @@ -2,6 +2,7 @@ from workspaces.service.model_service import ( NotAuthorized, + NotAllowed, OsbrepositoryService, VolumestorageService, WorkspaceService, @@ -15,6 +16,12 @@ class WorkspaceView(BaseModelView): service = WorkspaceService() + + def post(self, body): + try: + return super().post(body) + except NotAllowed: + return "Not allowed", 405 class OsbrepositoryView(BaseModelView): diff --git a/applications/workspaces/tasks/scan-workspace/main.py b/applications/workspaces/tasks/scan-workspace/main.py index 2f2f6284..53c7301b 100644 --- a/applications/workspaces/tasks/scan-workspace/main.py +++ b/applications/workspaces/tasks/scan-workspace/main.py @@ -9,7 +9,7 @@ -folder = sys.argv[1].split(":")[-1] +folder = sys.argv[1].split(":")[1] workspace_id = sys.argv[2] queue = sys.argv[3] diff --git a/deployment/codefresh-dev.yaml b/deployment/codefresh-dev.yaml index 66f96ca3..f8005579 100644 --- a/deployment/codefresh-dev.yaml +++ b/deployment/codefresh-dev.yaml @@ -104,6 +104,21 @@ steps: type: parallel stage: build steps: + nfsserver: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/nfsserver + title: Nfsserver + working_directory: ./cloud-harness/applications/nfsserver notifications: type: build stage: build @@ -183,21 +198,6 @@ steps: image_name: osb/common title: Common working_directory: ./.overrides/applications/common/server - events: - type: build - stage: build - tag: '${{CF_BUILD_ID}}' - dockerfile: Dockerfile - registry: '${{CODEFRESH_REGISTRY}}' - buildkit: true - build_arguments: - - TAG=${{CF_BUILD_ID}} - - DOMAIN=${{DOMAIN}} - - NOCACHE=${{CF_BUILD_ID}} - - REGISTRY=${{REGISTRY}}/osb/ - image_name: osb/events - title: Events - working_directory: ./cloud-harness/applications/events workflows-send-result-event: type: build stage: build @@ -431,6 +431,15 @@ steps: image_name: osb/jupyterlab title: Jupyterlab working_directory: ./applications/jupyterlab + tests_unit: + stage: unittest + type: parallel + steps: + workspaces_ut: + title: Unit tests for workspaces + commands: + - pytest /usr/src/app/ + image: '${{workspaces}}' deployment: stage: deploy type: helm @@ -446,7 +455,11 @@ steps: cmd_ps: --wait --timeout 600s custom_value_files: - ./deployment/helm/values.yaml - custom_values: [] + custom_values: + - apps_notifications_harness_secrets_email-user=${{EMAIL-USER}} + - apps_notifications_harness_secrets_email-password=${{EMAIL-PASSWORD}} + - apps_workspaces_harness_secrets_github-user=${{GITHUB-USER}} + - apps_workspaces_harness_secrets_github-token=${{GITHUB-TOKEN}} image: codefresh/cfstep-helm:3.6.2 environment: - CHART_REF=./deployment/helm @@ -458,6 +471,116 @@ steps: - HELM_REPO_CONTEXT_PATH= - TIMEOUT=600s - VALUESFILE_values=./deployment/helm/values.yaml + build_test_images: + title: Build test images + type: parallel + stage: qa + steps: + test-e2e: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/test-e2e + title: Test e2e + working_directory: ./cloud-harness/test/test-e2e + test-api: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: test/test-api/Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_BASE=${{REGISTRY}}/osb/cloudharness-base:${{CF_BUILD_ID}} + image_name: osb/test-api + title: Test api + working_directory: ./cloud-harness + when: + condition: + all: + whenVarExists: includes("${{SKIP_TESTS}}", "{{SKIP_TESTS}}") == true + wait_deployment: + stage: qa + title: Wait deployment to be ready + image: codefresh/kubectl + commands: + - kubectl config use-context ${{CLUSTER_NAME}} + - kubectl config set-context --current --namespace=${{NAMESPACE}} + - kubectl rollout status deployment/notifications + - kubectl rollout status deployment/accounts + - kubectl rollout status deployment/volumemanager + - kubectl rollout status deployment/common + - kubectl rollout status deployment/workflows + - kubectl rollout status deployment/workspaces + - kubectl rollout status deployment/accounts-api + - kubectl rollout status deployment/osb-portal + - kubectl rollout status deployment/backoffice + - sleep 60 + when: + condition: + all: + whenVarExists: includes("${{SKIP_TESTS}}", "{{SKIP_TESTS}}") == true + tests_api: + stage: qa + title: Api tests + working_directory: /home/test + image: '${{test-api}}' + fail_fast: false + commands: + - echo $APP_NAME + scale: + common_api_test: + volumes: + - '${{CF_REPO_NAME}}/.overrides/applications/common:/home/test' + - '${{CF_REPO_NAME}}/deployment/helm/values.yaml:/opt/cloudharness/resources/allvalues.yaml' + environment: + - APP_URL=https://common.${{DOMAIN}}/api + commands: + - st --pre-run cloudharness_test.apitest_init run api/openapi.yaml --base-url + https://common.${{DOMAIN}}/api -c all + workflows_api_test: + volumes: + - '${{CF_REPO_NAME}}/cloud-harness/applications/workflows:/home/test' + - '${{CF_REPO_NAME}}/deployment/helm/values.yaml:/opt/cloudharness/resources/allvalues.yaml' + environment: + - APP_URL=https://workflows.${{DOMAIN}}/api + commands: + - st --pre-run cloudharness_test.apitest_init run api/openapi.yaml --base-url + https://workflows.${{DOMAIN}}/api -c all + when: + condition: + all: + whenVarExists: includes("${{SKIP_TESTS}}", "{{SKIP_TESTS}}") == true + tests_e2e: + stage: qa + title: End to end tests + working_directory: /home/test + image: '${{test-e2e}}' + fail_fast: false + commands: + - yarn test + scale: + osb-portal_e2e_test: + volumes: + - '${{CF_REPO_NAME}}/applications/osb-portal/test/e2e:/home/test/__tests__/osb-portal' + environment: + - APP_URL=https://www.${{DOMAIN}} + when: + condition: + all: + whenVarExists: includes("${{SKIP_TESTS}}", "{{SKIP_TESTS}}") == true approval: type: pending-approval stage: publish @@ -494,6 +617,14 @@ steps: tags: - '${{DEPLOYMENT_PUBLISH_TAG}}' registry: '${{REGISTRY_PUBLISH_URL}}' + publish_nfsserver: + stage: publish + type: push + title: Nfsserver + candidate: '${{REGISTRY}}/osb/nfsserver:${{CF_BUILD_ID}}' + tags: + - '${{DEPLOYMENT_PUBLISH_TAG}}' + registry: '${{REGISTRY_PUBLISH_URL}}' publish_notifications: stage: publish type: push @@ -534,14 +665,6 @@ steps: tags: - '${{DEPLOYMENT_PUBLISH_TAG}}' registry: '${{REGISTRY_PUBLISH_URL}}' - publish_events: - stage: publish - type: push - title: Events - candidate: '${{REGISTRY}}/osb/events:${{CF_BUILD_ID}}' - tags: - - '${{DEPLOYMENT_PUBLISH_TAG}}' - registry: '${{REGISTRY_PUBLISH_URL}}' publish_workflows-send-result-event: stage: publish type: push diff --git a/deployment/codefresh-local-test-minimal.yaml b/deployment/codefresh-local-test-minimal.yaml new file mode 100644 index 00000000..e5735b78 --- /dev/null +++ b/deployment/codefresh-local-test-minimal.yaml @@ -0,0 +1,538 @@ +version: '1.0' +stages: +- prepare +- build +- unittest +- deploy +- qa +steps: + main_clone: + title: Clone main repository + type: git-clone + stage: prepare + repo: '${{CF_REPO_OWNER}}/${{CF_REPO_NAME}}' + revision: '${{CF_BRANCH}}' + git: github + post_main_clone: + title: Post main clone + type: parallel + stage: prepare + steps: + - title: Cloning cloud-harness repository... + type: git-clone + stage: prepare + repo: https://github.com/MetaCell/cloud-harness.git + revision: '${{CLOUDHARNESS_BRANCH}}' + working_directory: . + git: github + prepare_deployment: + title: Prepare helm chart + image: python:3.9.10 + stage: prepare + working_directory: . + commands: + - bash cloud-harness/install.sh + - harness-deployment cloud-harness . -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} + -t ${{CF_BUILD_ID}} -d ${{CF_SHORT_REVISION}}.${{DOMAIN}} -r ${{REGISTRY}} -rs + ${{REGISTRY_SECRET}} -e local-test-minimal -N -i argo -i osb-portal + prepare_deployment_view: + commands: + - helm template ./deployment/helm --debug -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} + environment: + - ACTION=auth + - KUBE_CONTEXT=test-${{CF_BUILD_ID}} + image: codefresh/cfstep-helm:3.6.2 + stage: prepare + title: View helm chart + build_base_images: + title: Build base images + type: parallel + stage: build + steps: + cloudharness-base: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: infrastructure/base-images/cloudharness-base/Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/cloudharness-base + title: Cloudharness base + working_directory: ./cloud-harness + cloudharness-frontend-build: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: infrastructure/base-images/cloudharness-frontend-build/Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/cloudharness-frontend-build + title: Cloudharness frontend build + working_directory: ./cloud-harness + build_static_images: + title: Build static images + type: parallel + stage: build + steps: + cloudharness-flask: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_BASE=${{REGISTRY}}/osb/cloudharness-base:${{CF_BUILD_ID}} + image_name: osb/cloudharness-flask + title: Cloudharness flask + working_directory: ./cloud-harness/infrastructure/common-images/cloudharness-flask + build_application_images: + type: parallel + stage: build + steps: + notifications: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_BASE=${{REGISTRY}}/osb/cloudharness-base:${{CF_BUILD_ID}} + image_name: osb/notifications + title: Notifications + working_directory: ./.overrides/applications/notifications/server + accounts: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/accounts + title: Accounts + working_directory: ./.overrides/applications/accounts + jupyterhub: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_BASE=${{REGISTRY}}/osb/cloudharness-base:${{CF_BUILD_ID}} + image_name: osb/jupyterhub + title: Jupyterhub + working_directory: ./.overrides/applications/jupyterhub + common: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_FLASK=${{REGISTRY}}/osb/cloudharness-flask:${{CF_BUILD_ID}} + image_name: osb/common + title: Common + working_directory: ./.overrides/applications/common/server + events: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/events + title: Events + working_directory: ./cloud-harness/applications/events + workflows-send-result-event: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_BASE=${{REGISTRY}}/osb/cloudharness-base:${{CF_BUILD_ID}} + image_name: osb/workflows-send-result-event + title: Workflows send result event + working_directory: ./cloud-harness/applications/workflows/tasks/send-result-event + workflows-extract-download: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/workflows-extract-download + title: Workflows extract download + working_directory: ./cloud-harness/applications/workflows/tasks/extract-download + workflows-notify-queue: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_BASE=${{REGISTRY}}/osb/cloudharness-base:${{CF_BUILD_ID}} + image_name: osb/workflows-notify-queue + title: Workflows notify queue + working_directory: ./cloud-harness/applications/workflows/tasks/notify-queue + workflows: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_FLASK=${{REGISTRY}}/osb/cloudharness-flask:${{CF_BUILD_ID}} + image_name: osb/workflows + title: Workflows + working_directory: ./cloud-harness/applications/workflows/server + workspaces-figshare-copy: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_BASE=${{REGISTRY}}/osb/cloudharness-base:${{CF_BUILD_ID}} + image_name: osb/workspaces-figshare-copy + title: Workspaces figshare copy + working_directory: ./applications/workspaces/tasks/figshare-copy + workspaces-scan-workspace: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_BASE=${{REGISTRY}}/osb/cloudharness-base:${{CF_BUILD_ID}} + image_name: osb/workspaces-scan-workspace + title: Workspaces scan workspace + working_directory: ./applications/workspaces/tasks/scan-workspace + workspaces-github-copy: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_BASE=${{REGISTRY}}/osb/cloudharness-base:${{CF_BUILD_ID}} + image_name: osb/workspaces-github-copy + title: Workspaces github copy + working_directory: ./applications/workspaces/tasks/github-copy + workspaces-dandi-copy: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/workspaces-dandi-copy + title: Workspaces dandi copy + working_directory: ./applications/workspaces/tasks/dandi-copy + workspaces: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_FLASK=${{REGISTRY}}/osb/cloudharness-flask:${{CF_BUILD_ID}} + image_name: osb/workspaces + title: Workspaces + working_directory: ./applications/workspaces/server + accounts-api: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_FLASK=${{REGISTRY}}/osb/cloudharness-flask:${{CF_BUILD_ID}} + image_name: osb/accounts-api + title: Accounts api + working_directory: ./applications/accounts-api + osb-portal: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/osb-portal + title: Osb portal + working_directory: ./applications/osb-portal + jupyterlab-minimal: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/jupyterlab-minimal + title: Jupyterlab minimal + working_directory: ./applications/jupyterlab-minimal + prepare_cluster: + stage: deploy + image: codefresh/kubectl + fail_fast: false + commands: + - kubectl config use-context ${{CLUSTER_NAME}} + - kubectl create ns test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} + deployment: + stage: deploy + type: helm + working_directory: ./${{CF_REPO_NAME}} + title: Installing chart + arguments: + helm_version: 3.6.2 + chart_name: deployment/helm + release_name: test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} + kube_context: '${{CLUSTER_NAME}}' + namespace: test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} + chart_version: '${{CF_BUILD_ID}}' + cmd_ps: --timeout 600s + custom_value_files: + - ./deployment/helm/values.yaml + custom_values: + - apps_notifications_harness_secrets_email-user=${{EMAIL-USER}} + - apps_notifications_harness_secrets_email-password=${{EMAIL-PASSWORD}} + - apps_accounts_harness_secrets_api__user__password=${{API__USER__PASSWORD}} + - apps_workspaces_harness_secrets_github-user=${{GITHUB-USER}} + - apps_workspaces_harness_secrets_github-token=${{GITHUB-TOKEN}} + build_test_images: + title: Build test images + type: parallel + stage: qa + steps: + test-e2e: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/test-e2e + title: Test e2e + working_directory: ./cloud-harness/test/test-e2e + test-api: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: test/test-api/Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + - CLOUDHARNESS_BASE=${{REGISTRY}}/osb/cloudharness-base:${{CF_BUILD_ID}} + image_name: osb/test-api + title: Test api + working_directory: ./cloud-harness + wait_deployment: + stage: qa + title: Wait deployment to be ready + image: codefresh/kubectl + commands: + - kubectl config use-context ${{CLUSTER_NAME}} + - kubectl config set-context --current --namespace=test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} + - kubectl rollout status deployment/notifications + - kubectl rollout status deployment/accounts + - kubectl rollout status deployment/common + - kubectl rollout status deployment/events + - kubectl rollout status deployment/workflows + - kubectl rollout status deployment/workspaces + - kubectl rollout status deployment/accounts-api + - kubectl rollout status deployment/osb-portal + - sleep 60 + tests_api: + stage: qa + title: Api tests + working_directory: /home/test + image: '${{test-api}}' + fail_fast: false + commands: + - echo $APP_NAME + scale: + common_api_test: + volumes: + - '${{CF_REPO_NAME}}/.overrides/applications/common:/home/test' + - '${{CF_REPO_NAME}}/deployment/helm/values.yaml:/opt/cloudharness/resources/allvalues.yaml' + environment: + - APP_URL=https://common.${{CF_SHORT_REVISION}}.${{DOMAIN}}/api + commands: + - st --pre-run cloudharness_test.apitest_init run api/openapi.yaml --base-url + https://common.${{CF_SHORT_REVISION}}.${{DOMAIN}}/api -c all + workflows_api_test: + volumes: + - '${{CF_REPO_NAME}}/cloud-harness/applications/workflows:/home/test' + - '${{CF_REPO_NAME}}/deployment/helm/values.yaml:/opt/cloudharness/resources/allvalues.yaml' + environment: + - APP_URL=https://workflows.${{CF_SHORT_REVISION}}.${{DOMAIN}}/api + commands: + - st --pre-run cloudharness_test.apitest_init run api/openapi.yaml --base-url + https://workflows.${{CF_SHORT_REVISION}}.${{DOMAIN}}/api -c all + hooks: + on_fail: + exec: + image: alpine + commands: + - cf_export FAILED=failed + tests_e2e: + stage: qa + title: End to end tests + working_directory: /home/test + image: '${{test-e2e}}' + fail_fast: false + commands: + - yarn test + scale: + osb-portal_e2e_test: + volumes: + - '${{CF_REPO_NAME}}/applications/osb-portal/test/e2e:/home/test/__tests__/osb-portal' + environment: + - APP_URL=https://www.${{CF_SHORT_REVISION}}.${{DOMAIN}} + - USERNAME=testuser + - PASSWORD=test + hooks: + on_fail: + exec: + image: alpine + commands: + - cf_export FAILED=failed + approval: + type: pending-approval + stage: qa + title: Approve with failed tests + description: The pipeline will fail after ${{WAIT_ON_FAIL}} minutes + timeout: + timeUnit: minutes + duration: '${{WAIT_ON_FAIL}}' + finalState: denied + when: + condition: + all: + error: '"${{FAILED}}" == "failed"' + wait_on_fail: '${{WAIT_ON_FAIL}}' + dummy_end: + title: Dummy step + description: Without this, the on_finish hook is executed before the approval + step + image: python:3.9.10 + stage: qa + when: + condition: + all: + error: '"${{FAILED}}" == "failed"' + wait_on_fail: '${{WAIT_ON_FAIL}}' +hooks: + on_finish: + image: codefresh/kubectl + commands: + - kubectl config use-context ${{CLUSTER_NAME}} + - kubectl delete ns test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} diff --git a/deployment/codefresh-prod.yaml b/deployment/codefresh-prod.yaml index a2e77f76..5f3d469a 100644 --- a/deployment/codefresh-prod.yaml +++ b/deployment/codefresh-prod.yaml @@ -55,7 +55,11 @@ steps: cmd_ps: --wait --timeout 600s custom_value_files: - ./deployment/helm/values.yaml - custom_values: [] + custom_values: + - apps_notifications_harness_secrets_email-user=${{EMAIL-USER}} + - apps_notifications_harness_secrets_email-password=${{EMAIL-PASSWORD}} + - apps_workspaces_harness_secrets_github-user=${{GITHUB-USER}} + - apps_workspaces_harness_secrets_github-token=${{GITHUB-TOKEN}} image: codefresh/cfstep-helm:3.6.2 environment: - CHART_REF=./deployment/helm diff --git a/deployment/codefresh-test.yaml b/deployment/codefresh-test.yaml index 0a20a4a7..f08e43a8 100644 --- a/deployment/codefresh-test.yaml +++ b/deployment/codefresh-test.yaml @@ -100,21 +100,6 @@ steps: image_name: osb/cloudharness-flask title: Cloudharness flask working_directory: ./cloud-harness/infrastructure/common-images/cloudharness-flask - test-e2e: - type: build - stage: build - tag: '${{CF_BUILD_ID}}' - dockerfile: Dockerfile - registry: '${{CODEFRESH_REGISTRY}}' - buildkit: true - build_arguments: - - TAG=${{CF_BUILD_ID}} - - DOMAIN=${{DOMAIN}} - - NOCACHE=${{CF_BUILD_ID}} - - REGISTRY=${{REGISTRY}}/osb/ - image_name: osb/test-e2e - title: Test e2e - working_directory: ./cloud-harness/test/test-e2e build_application_images: type: parallel stage: build @@ -465,10 +450,34 @@ steps: kube_context: '${{CLUSTER_NAME}}' namespace: test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} chart_version: '${{CF_BUILD_ID}}' - cmd_ps: --wait --timeout 600s + cmd_ps: --timeout 600s custom_value_files: - ./deployment/helm/values.yaml - custom_values: [] + custom_values: + - apps_notifications_harness_secrets_email-user=${{EMAIL-USER}} + - apps_notifications_harness_secrets_email-password=${{EMAIL-PASSWORD}} + - apps_workspaces_harness_secrets_github-user=${{GITHUB-USER}} + - apps_workspaces_harness_secrets_github-token=${{GITHUB-TOKEN}} + build_test_images: + title: Build test images + type: parallel + stage: qa + steps: + test-e2e: + type: build + stage: build + tag: '${{CF_BUILD_ID}}' + dockerfile: Dockerfile + registry: '${{CODEFRESH_REGISTRY}}' + buildkit: true + build_arguments: + - TAG=${{CF_BUILD_ID}} + - DOMAIN=${{DOMAIN}} + - NOCACHE=${{CF_BUILD_ID}} + - REGISTRY=${{REGISTRY}}/osb/ + image_name: osb/test-e2e + title: Test e2e + working_directory: ./cloud-harness/test/test-e2e wait_deployment: stage: qa title: Wait deployment to be ready @@ -476,68 +485,16 @@ steps: commands: - kubectl config use-context ${{CLUSTER_NAME}} - kubectl config set-context --current --namespace=test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/notifications - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/accounts - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/volumemanager - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/common - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/events - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/workflows - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/workspaces - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/accounts-api - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/osb-portal - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/backoffice - - sleep 60 - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/notifications - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/accounts - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/volumemanager - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/common - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/events - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/workflows - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/workspaces - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/accounts-api - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/osb-portal - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/backoffice - - sleep 60 - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/notifications - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/accounts - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/volumemanager - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/common - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/events - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/workflows - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/workspaces - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/accounts-api - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/osb-portal - - kubectl -n test-${{NAMESPACE_BASENAME}}-${{CF_SHORT_REVISION}} rollout status - deployment/backoffice + - kubectl rollout status deployment/notifications + - kubectl rollout status deployment/accounts + - kubectl rollout status deployment/volumemanager + - kubectl rollout status deployment/common + - kubectl rollout status deployment/events + - kubectl rollout status deployment/workflows + - kubectl rollout status deployment/workspaces + - kubectl rollout status deployment/accounts-api + - kubectl rollout status deployment/osb-portal + - kubectl rollout status deployment/backoffice - sleep 60 tests_e2e: stage: qa diff --git a/deployment/pvc.yaml b/deployment/pvc.yaml new file mode 100644 index 00000000..c0973bdd --- /dev/null +++ b/deployment/pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: testpvc + namespace: osblocal +spec: + storageClassName: nfs-client-osblocal + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10Mi \ No newline at end of file diff --git a/kubespawner b/kubespawner new file mode 160000 index 00000000..9715f958 --- /dev/null +++ b/kubespawner @@ -0,0 +1 @@ +Subproject commit 9715f9580b42b0d5924b41aac1eff931b5ca71b2