diff --git a/.gitignore b/.gitignore index 38eee2e6..34a5a41b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,10 @@ __pycache__ .idea .vscode - env* venv support +# openshift config check folder +app-config/openshift/test-cara +app-config/openshift/cara-prod diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 332f6fe2..f57bc054 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,8 @@ +stages: + - test + - docker-build + - oc-tag + - deploy # Use the acc-py-devtools templates found at # https://gitlab.cern.ch/-/ide/project/acc-co/devops/python/acc-py-devtools/blob/master/-/acc_py_devtools/templates/gitlab-ci/python.yml. @@ -10,6 +15,9 @@ variables: PY_VERSION: "3.9" +# ################################################################################################### +# Test code + # A full installation of CARA, tested with pytest. test_install: extends: .acc_py_full_test @@ -21,10 +29,20 @@ test_dev: # A development installation of CARA tested with pytest. +test_dev-39: + variables: + PY_VERSION: "3.9" + extends: .acc_py_dev_test + + +# ################################################################################################### +# Test OpenShift config + .test_openshift_config: + stage: test rules: - if: '$OC_TOKEN && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == $BRANCH' - allow_failure: false # The branch must represent what is deployed. + allow_failure: true # The branch must represent what is deployed. FIXME: change to true because of a diff between ConfigMaps - if: '$OC_TOKEN && $CI_MERGE_REQUEST_EVENT_TYPE != "detached"' allow_failure: true # Anything other than the branch may fail without blocking the pipeline. image: registry.cern.ch/docker.io/mambaorg/micromamba @@ -34,7 +52,6 @@ test_dev: - wget https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz - tar xzf ./openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz - mv openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit/oc $HOME/env/bin/ - script: - cd ./app-config/openshift - oc login ${OC_SERVER} --token="${OC_TOKEN}" @@ -43,57 +60,51 @@ test_dev: - python ./config-normalise.py ./${CARA_INSTANCE}/actual ./${CARA_INSTANCE}/actual-normed - python ./config-normalise.py ./${CARA_INSTANCE}/expected ./${CARA_INSTANCE}/expected-normed - diff -u ./${CARA_INSTANCE}/actual-normed/ ./${CARA_INSTANCE}/expected-normed/ - artifacts: paths: - ./app-config/openshift/${CARA_INSTANCE}/actual - ./app-config/openshift/${CARA_INSTANCE}/expected -check_openshift_config_test-cara: +check_openshift_config_test: extends: .test_openshift_config variables: CARA_INSTANCE: 'test-cara' BRANCH: 'live/test-cara' - OC_SERVER: openshift-dev.cern.ch - OC_TOKEN: "${OPENSHIFT_CONFIG_CHECKER_TOKEN_TEST_CARA}" + OC_SERVER: https://api.paas.okd.cern.ch + OC_TOKEN: "${OPENSHIFT_TEST_CONFIG_CHECKER_TOKEN}" check_openshift_config_prod: extends: .test_openshift_config variables: - CARA_INSTANCE: 'cara' + CARA_INSTANCE: 'cara-prod' BRANCH: 'master' - OC_SERVER: openshift.cern.ch - OC_TOKEN: "${OPENSHIFT_CONFIG_CHECKER_TOKEN_PROD}" + OC_SERVER: https://api.paas.okd.cern.ch + OC_TOKEN: "${OPENSHIFT_PROD_CONFIG_CHECKER_TOKEN}" -# A development installation of CARA tested with pytest. -test_dev-39: - variables: - PY_VERSION: "3.9" - extends: .acc_py_dev_test - +# ################################################################################################### +# Build docker images .image_builder: - # Build and push images to the openshift instance, which automatically triggers an application re-deployment. - stage: deploy - image: - # Based on guidance at https://gitlab.cern.ch/gitlabci-examples/build_docker_image. - name: gitlab-registry.cern.ch/ci-tools/docker-image-builder - entrypoint: [""] - rules: - - if: '$OPENSHIFT_DOCKER_TOKEN_TEST != "" && $CI_COMMIT_BRANCH == "live/test-cara"' - variables: - DOCKER_REGISTRY: "${OPENSHIFT_DOCKER_REGISTRY_TEST}" - DOCKER_TOKEN: "${OPENSHIFT_DOCKER_TOKEN_TEST}" - - if: '$OPENSHIFT_DOCKER_TOKEN_PROD != "" && $CI_COMMIT_BRANCH == "master"' - variables: - DOCKER_REGISTRY: "${OPENSHIFT_DOCKER_REGISTRY_PROD}" - DOCKER_TOKEN: "${OPENSHIFT_DOCKER_TOKEN_PROD}" - script: - - echo "{\"auths\":{\"$DOCKER_REGISTRY\":{\"auth\":\"$DOCKER_TOKEN\"}}}" > /kaniko/.docker/config.json - - /kaniko/executor --context $CI_PROJECT_DIR/$DOCKER_CONTEXT_DIRECTORY --dockerfile $CI_PROJECT_DIR/$DOCKERFILE_DIRECTORY/Dockerfile --destination $DOCKER_REGISTRY/$IMAGE_NAME:latest + # Build and push images to the openshift instance, which automatically triggers an application re-deployment. + stage: docker-build + rules: + - if: '$CI_COMMIT_BRANCH == "live/test-cara"' + variables: + IMAGE_TAG: test-cara-latest + - if: '$CI_COMMIT_BRANCH == "master"' + variables: + IMAGE_TAG: cara-prod-latest + image: + # Based on guidance at https://gitlab.cern.ch/gitlabci-examples/build_docker_image. + name: gitlab-registry.cern.ch/ci-tools/docker-image-builder + entrypoint: [""] + script: + - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json + - echo "Building ${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:latest Docker image..." + - /kaniko/executor --context ${CI_PROJECT_DIR}/${DOCKER_CONTEXT_DIRECTORY} --dockerfile ${CI_PROJECT_DIR}/${DOCKERFILE_DIRECTORY}/Dockerfile --destination ${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${IMAGE_TAG} auth-service-image_builder: @@ -114,32 +125,67 @@ cara-webservice-image_builder: DOCKER_CONTEXT_DIRECTORY: "" -trigger_build_on_openshift: - stage: deploy - rules: - - if: '$OPENSHIFT_BUILD_WEBHOOK_SECRET' - script: - - curl -X POST -k https://openshift.cern.ch:443/apis/build.openshift.io/v1/namespaces/cara/buildconfigs/cara-router/webhooks/${OPENSHIFT_BUILD_WEBHOOK_SECRET}/generic - - -deploy_to_test: - stage: deploy - rules: - - if: '$CI_COMMIT_BRANCH == "live/test-cara" && $OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET' - script: - - curl -X POST -k https://api.paas.okd.cern.ch/apis/build.openshift.io/v1/namespaces/test-cara/buildconfigs/cara-router/webhooks/${OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET}/generic - - oci_calculator: - # A convenient way for users to run the CARA calculator. + extends: + - .image_builder + variables: + IMAGE_NAME: calculator + DOCKERFILE_DIRECTORY: app-config/cara-public-docker-image + DOCKER_CONTEXT_DIRECTORY: "" + + +# ################################################################################################### +# Link build Docker images OpenShift <-> GitLab registry + +.link_docker_images_with_gitlab_registry: + stage: oc-tag + image: gitlab-registry.cern.ch/paas-tools/openshift-client:latest + rules: + - if: '$CI_COMMIT_BRANCH == "live/test-cara"' + variables: + OC_PROJECT: "test-cara" + OC_TOKEN: ${OPENSHIFT_TEST_DEPLOY_TOKEN} + IMAGE_TAG: test-cara-latest + - if: '$CI_COMMIT_BRANCH == "master"' + variables: + OC_PROJECT: "cara-prod" + OC_TOKEN: ${OPENSHIFT_PROD_DEPLOY_TOKEN} + IMAGE_TAG: cara-prod-latest + script: + - oc tag --source=docker ${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest --token ${OC_TOKEN} --server=https://api.paas.okd.cern.ch -n ${OC_PROJECT} + +link_auth-service_with_gitlab_registry: + extends: + - .link_docker_images_with_gitlab_registry + variables: + IMAGE_NAME: auth-service + +link_cara-webservice_with_gitlab_registry: + extends: + - .link_docker_images_with_gitlab_registry + variables: + IMAGE_NAME: cara-webservice + +link_calculator_with_gitlab_registry: + extends: + - .link_docker_images_with_gitlab_registry + variables: + IMAGE_NAME: calculator + + +# ################################################################################################### +# Trigger build of CARA router on OpenShift + +trigger_cara-router_build_on_openshift: stage: deploy rules: - # Only run if branch is master (the default branch). - - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH - image: - name: gitlab-registry.cern.ch/ci-tools/docker-image-builder - entrypoint: [""] + - if: '$CI_COMMIT_BRANCH == "live/test-cara"' + variables: + OC_PROJECT: "test-cara" + BUILD_WEBHOOK_SECRET: ${OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET} + - if: '$CI_COMMIT_BRANCH == "master"' + variables: + OC_PROJECT: "cara-prod" + BUILD_WEBHOOK_SECRET: ${OPENSHIFT_PROD_BUILD_WEBHOOK_SECRET} script: - - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/app-config/cara-public-docker-image/Dockerfile --destination $CI_REGISTRY_IMAGE/calculator:latest - + - curl -X POST -k https://api.paas.okd.cern.ch/apis/build.openshift.io/v1/namespaces/${OC_PROJECT}/buildconfigs/cara-router/webhooks/${BUILD_WEBHOOK_SECRET}/generic diff --git a/README.md b/README.md index d6d770f9..aa67fda1 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ python -m cara.apps.calculator To run with the CERN theme: ``` -python -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern +python -m cara.apps.calculator --theme=cara/apps/templates/cern ``` To run the calculator on a different URL path: @@ -175,31 +175,33 @@ but it may be origin if you haven't configured it differently): First, get the [oc](https://docs.okd.io/3.11/cli_reference/get_started_cli.html) client and then login: ```console -$ oc login https://openshift-dev.cern.ch +$ oc login https://api.paas.okd.cern.ch ``` Then, switch to the project that you want to update: ```console -$ oc project test-cara +$ oc project cara-test ``` -If you need to create the application in a new project, run: +Create a new service account in OpenShift to use GitLab container registry: ```console -$ cd app-config/openshift +$ oc create serviceaccount gitlabci-deployer +serviceaccount "gitlabci-deployer" created -$ oc process -f routes.yaml --param HOST='test-cara.web.cern.ch' | oc create -f - -$ oc process -f configmap.yaml | oc create -f - -$ oc process -f services.yaml | oc create -f - -$ oc process -f imagestreams.yaml | oc create -f - -$ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc create -f - -$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='test-cara' | oc create -f - +$ oc policy add-role-to-user registry-editor -z gitlabci-deployer + +# We will refer to the output of this command as `test-token` +$ oc serviceaccounts get-token gitlabci-deployer +<...test-token...> ``` +Add the token to GitLab to allow GitLab to access OpenShift and define/change image stream tags. Go to `Settings` -> `CI / CD` -> `Variables` -> click on `Expand` button and create the variable `OPENSHIFT_TEST_DEPLOY_TOKEN`: insert the token `<...test-token...>`. + Then, create the webhook secret to be able to trigger automatic builds from GitLab. -Create and store the secret. Copy the secret above and add it to the GitLab project under `CI /CD` -> `Variables` with the name `OPENSHIFT_CARA_TEST_WEBHOOK_SECRET`. +Create and store the secret. Copy the secret above and add it to the GitLab project under `CI /CD` -> `Variables` with the name `OPENSHIFT_TEST_WEBHOOK_SECRET`. ```console $ WEBHOOKSECRET=$(openssl rand -hex 50) @@ -214,10 +216,26 @@ For CI usage, we also suggest creating a service account: oc create sa gitlab-config-checker ``` -Under ``Resources`` -> ``Membership`` enable the ``View`` role for this new service account. +Under ``User Management`` -> ``RoleBindings`` create a new `RoleBinding` to grant `View` access to the `gitlab-config-checker` service account: -To get this new user's authentication token go to ``Resources`` -> ``Secrets`` and locate the token in the newly -created secret associated with the user (in this case ``gitlab-config-checker-token-XXXX``). +* name: `gitlab-config-checker-view-role` +* role name: `view` +* service account: `gitlab-config-checker` + +To get this new user's authentication token go to ``User Management`` -> ``Service Accounts`` -> `gitlab-config-checker` and locate the token in the newly created secret associated with the user (in this case ``gitlab-config-checker-token-XXXX``). Copy the `token` value from `Data`. + +Create the various configurations: + +```console +$ cd app-config/openshift + +$ oc process -f routes.yaml --param HOST='test-cara.web.cern.ch' | oc create -f - +$ oc process -f configmap.yaml | oc create -f - +$ oc process -f services.yaml | oc create -f - +$ oc process -f imagestreams.yaml | oc create -f - +$ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc create -f - +$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='cara-test' | oc create -f - +``` ### CERN SSO integration @@ -272,7 +290,7 @@ $ oc process -f services.yaml | oc replace -f - $ oc process -f routes.yaml --param HOST='test-cara.web.cern.ch' | oc replace -f - $ oc process -f imagestreams.yaml | oc replace -f - $ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc replace -f - -$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='test-cara' | oc replace -f - +$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='cara-test' | oc replace -f - ``` Be aware that if you change/replace the **route** of the PROD instance, diff --git a/app-config/auth-service/Dockerfile b/app-config/auth-service/Dockerfile index 3c726961..75ef9310 100644 --- a/app-config/auth-service/Dockerfile +++ b/app-config/auth-service/Dockerfile @@ -1,4 +1,4 @@ -FROM condaforge/mambaforge as conda +FROM registry.cern.ch/docker.io/condaforge/mambaforge as conda RUN mamba create --yes -p /opt/app python=3.9 COPY . /opt/app-source @@ -17,7 +17,7 @@ RUN cd /opt/app \ && find /opt/app/lib -name '*.pyx' -delete \ ; -FROM debian +FROM registry.cern.ch/docker.io/library/debian COPY --from=conda /opt/app /opt/app CMD [ \ diff --git a/app-config/cara-public-docker-image/Dockerfile b/app-config/cara-public-docker-image/Dockerfile index d61f49d7..b4a01695 100644 --- a/app-config/cara-public-docker-image/Dockerfile +++ b/app-config/cara-public-docker-image/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9 +FROM registry.cern.ch/docker.io/library/python:3.9 # Copy just the requirements.txt initially, allowing Docker effectively to cache the build (good for dev). COPY ./requirements.txt /tmp/requirements.txt diff --git a/app-config/cara-webservice/Dockerfile b/app-config/cara-webservice/Dockerfile index 517b30c8..a1a0ba1d 100644 --- a/app-config/cara-webservice/Dockerfile +++ b/app-config/cara-webservice/Dockerfile @@ -1,4 +1,4 @@ -FROM condaforge/mambaforge as conda +FROM registry.cern.ch/docker.io/condaforge/mambaforge as conda RUN mamba create --yes -p /opt/app python=3.9 COPY . /opt/app-source @@ -18,7 +18,7 @@ RUN cd /opt/app \ && find /opt/app/lib -name '*.pyx' -delete \ ; -FROM debian +FROM registry.cern.ch/docker.io/library/debian COPY --from=conda /opt/app /opt/app ENV PATH=/opt/app/bin/:$PATH diff --git a/app-config/docker-compose.yml b/app-config/docker-compose.yml index 18d9d0a0..e96da5fd 100644 --- a/app-config/docker-compose.yml +++ b/app-config/docker-compose.yml @@ -12,7 +12,7 @@ services: - COOKIE_SECRET - APP_NAME=cara-webservice - CARA_CALCULATOR_PREFIX=/calculator-cern - - CARA_THEME=cara/apps/calculator/themes/cern + - CARA_THEME=cara/apps/templates/cern user: ${CURRENT_UID} cara-calculator-open: diff --git a/app-config/openshift/buildconfig.yaml b/app-config/openshift/buildconfig.yaml index 822050c0..9bcbc165 100644 --- a/app-config/openshift/buildconfig.yaml +++ b/app-config/openshift/buildconfig.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-application" creationTimestamp: null @@ -12,7 +12,7 @@ objects: - kind: BuildConfig - apiVersion: v1 + apiVersion: build.openshift.io/v1 metadata: name: cara-router labels: diff --git a/app-config/openshift/config-fetch.py b/app-config/openshift/config-fetch.py index 55a27230..973d0c11 100644 --- a/app-config/openshift/config-fetch.py +++ b/app-config/openshift/config-fetch.py @@ -9,7 +9,7 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: parser.description = "Fetch the openshift config for CARA" parser.set_defaults(handler=handler) parser.add_argument( - "instance", choices=['cara', 'test-cara'], + "instance", choices=['cara-prod', 'test-cara'], help="Pick the instance for which you want to fetch the config", ) parser.add_argument( @@ -35,25 +35,33 @@ def get_oc_server() -> typing.Optional[str]: def fetch_config(output_directory: pathlib.Path): output_directory.mkdir(exist_ok=True, parents=True) - for component in ['routes', 'configmap', 'services', 'imagestreams', 'buildconfig', 'deploymentconfig']: + for component, name in [ + ('routes', None), + ('configmap', 'auth-service'), + ('services', None), + ('imagestreams', None), + ('buildconfig', None), + ('deploymentconfig', None)]: + with (output_directory / f'{component}.yaml').open('wt') as fh: - cmd = ['oc', 'get', '--export', '-o', 'yaml', component] + cmd = ['oc', 'get', '-o', 'yaml', component] + if name: + cmd += [name] print(f'Running: {" ".join(cmd)}') subprocess.run(cmd, stdout=fh, check=True) print(f'Config in: {output_directory.absolute()}') def handler(args: argparse.ArgumentParser) -> None: - if args.instance == 'cara': - login_server = 'https://openshift.cern.ch:443' - project_name = 'cara' + login_server = 'https://api.paas.okd.cern.ch:443' + if args.instance == 'cara-prod': + project_name = 'cara-prod' elif args.instance == 'test-cara': - login_server = 'https://openshift-dev.cern.ch:443' project_name = 'test-cara' actual_login_server = get_oc_server() if actual_login_server != login_server: - print(f'\nPlease login to the correct openshift server with: \n\n oc login {login_server}\n', file=sys.stderr) + print(f'\nPlease login to the correct OpenShift server with: \n\n oc login {login_server}\n', file=sys.stderr) sys.exit(1) subprocess.run(['oc', 'project', project_name], stdout=subprocess.DEVNULL, check=True) diff --git a/app-config/openshift/config-generate.py b/app-config/openshift/config-generate.py index c17c411a..aedb2267 100644 --- a/app-config/openshift/config-generate.py +++ b/app-config/openshift/config-generate.py @@ -8,7 +8,7 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: parser.description = "Generate the config files which can be later submitted to openshift" parser.set_defaults(handler=handler) parser.add_argument( - "instance", choices=['cara', 'test-cara'], + "instance", choices=['cara-prod', 'test-cara'], help="Pick the instance for which you want to generate the config", ) parser.add_argument( @@ -39,13 +39,13 @@ def generate_config(output_directory: pathlib.Path, project_name: str, hostname: def handler(args: argparse.ArgumentParser) -> None: - if args.instance == 'cara': - project_name = 'cara' + if args.instance == 'cara-prod': + project_name = 'cara-prod' branch = 'master' hostname = 'cara.web.cern.ch' elif args.instance == 'test-cara': - branch = 'live/test-cara' project_name = 'test-cara' + branch = 'live/test-cara' hostname = 'test-cara.web.cern.ch' generate_config(pathlib.Path(args.output_directory), project_name, hostname, branch) diff --git a/app-config/openshift/config-normalise.py b/app-config/openshift/config-normalise.py index 4672a128..e9ea6c70 100644 --- a/app-config/openshift/config-normalise.py +++ b/app-config/openshift/config-normalise.py @@ -22,8 +22,9 @@ def clean_ephemeral_config(config: dict): config.get('metadata', []).clear() METADATA_TO_PRESERVE = ['labels', 'name'] + CERN_OKD4_METADATA_LABELS = ['migration.openshift.io', 'velero.io'] - for item in config['items']: + for item in config.get('items', {}): item.pop('status', None) for key in list(item['metadata'].keys()): @@ -31,10 +32,14 @@ def clean_ephemeral_config(config: dict): del item['metadata'][key] item.get('spec', {}).pop('clusterIP', None) + item.get('spec', {}).pop('clusterIPs', None) + item.get('spec', {}).pop('revisionHistoryLimit', None) if item['kind'] == 'BuildConfig': for trigger in item.get('spec', {}).get('triggers', []): trigger.get('imageChange', {}).pop('lastTriggeredImageID', None) + item.get('spec', {}).pop('failedBuildsHistoryLimit', None) + item.get('spec', {}).pop('successfulBuildsHistoryLimit', None) if item['kind'] == 'DeploymentConfig': item['spec'].get('template', {}).get('metadata', {}).pop('creationTimestamp', None) @@ -46,6 +51,11 @@ def clean_ephemeral_config(config: dict): for trigger in item['spec'].get('triggers', []): trigger.get('imageChangeParams', {}).pop('lastTriggeredImage', None) + for label in list(item['metadata'].get('labels', {}).keys()): + for prefix in CERN_OKD4_METADATA_LABELS: + if label.startswith(prefix): + item['metadata']['labels'].pop(label) + # Drop the template part of the config for now. # TODO: Remove this constraint to ensure our deployments reflect the fact that they are templated. r = item['metadata'].get('labels', {}).pop('template', None) diff --git a/app-config/openshift/configmap.yaml b/app-config/openshift/configmap.yaml index be119b92..9b6e2cbf 100644 --- a/app-config/openshift/configmap.yaml +++ b/app-config/openshift/configmap.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-configuration" annotations: diff --git a/app-config/openshift/deploymentconfig.yaml b/app-config/openshift/deploymentconfig.yaml index fcdd97e1..e4260f94 100644 --- a/app-config/openshift/deploymentconfig.yaml +++ b/app-config/openshift/deploymentconfig.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-application" annotations: @@ -10,7 +10,7 @@ template: "cara-application" objects: - - apiVersion: v1 + apiVersion: apps.openshift.io/v1 kind: DeploymentConfig metadata: name: auth-service @@ -69,7 +69,7 @@ name: 'auth-service:latest' namespace: ${PROJECT_NAME} - - apiVersion: v1 + apiVersion: apps.openshift.io/v1 kind: DeploymentConfig metadata: name: cara-app @@ -126,7 +126,7 @@ name: 'cara-webservice:latest' namespace: ${PROJECT_NAME} - - apiVersion: v1 + apiVersion: apps.openshift.io/v1 kind: DeploymentConfig metadata: name: cara-router @@ -179,7 +179,7 @@ namespace: ${PROJECT_NAME} - type: ConfigChange - - apiVersion: v1 + apiVersion: apps.openshift.io/v1 kind: DeploymentConfig metadata: name: cara-webservice @@ -208,7 +208,7 @@ - name: CARA_CALCULATOR_PREFIX value: /calculator-cern - name: CARA_THEME - value: cara/apps/calculator/themes/cern + value: cara/apps/templates/cern image: '${PROJECT_NAME}/cara-webservice' ports: - containerPort: 8080 @@ -263,7 +263,7 @@ namespace: ${PROJECT_NAME} - type: ConfigChange - - apiVersion: v1 + apiVersion: apps.openshift.io/v1 kind: DeploymentConfig metadata: name: cara-calculator-open diff --git a/app-config/openshift/imagestreams.yaml b/app-config/openshift/imagestreams.yaml index 26754cd4..5a3205c5 100644 --- a/app-config/openshift/imagestreams.yaml +++ b/app-config/openshift/imagestreams.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-imagestreams" creationTimestamp: null @@ -12,7 +12,7 @@ objects: - kind: ImageStream - apiVersion: v1 + apiVersion: image.openshift.io/v1 metadata: name: auth-service spec: @@ -20,7 +20,7 @@ local: False - kind: ImageStream - apiVersion: v1 + apiVersion: image.openshift.io/v1 metadata: name: cara-router spec: @@ -28,7 +28,7 @@ local: False - kind: ImageStream - apiVersion: v1 + apiVersion: image.openshift.io/v1 metadata: name: cara-webservice spec: diff --git a/app-config/openshift/routes.yaml b/app-config/openshift/routes.yaml index 70874b9b..a7f0f0e1 100644 --- a/app-config/openshift/routes.yaml +++ b/app-config/openshift/routes.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-route" creationTimestamp: null @@ -11,7 +11,7 @@ template: "cara-route" objects: - - apiVersion: v1 + apiVersion: route.openshift.io/v1 kind: Route metadata: name: cara-route diff --git a/app-config/openshift/services.yaml b/app-config/openshift/services.yaml index 73b53a07..d5de132e 100644 --- a/app-config/openshift/services.yaml +++ b/app-config/openshift/services.yaml @@ -1,6 +1,6 @@ --- kind: "Template" - apiVersion: "v1" + apiVersion: template.openshift.io/v1 metadata: name: "cara-services" creationTimestamp: null diff --git a/cara/apps/calculator/__init__.py b/cara/apps/calculator/__init__.py index 407382e3..6bfcb9d7 100644 --- a/cara/apps/calculator/__init__.py +++ b/cara/apps/calculator/__init__.py @@ -33,7 +33,7 @@ from .user import AuthenticatedUser, AnonymousUser # calculator version. If the calculator needs to make breaking changes (e.g. change # form attributes) then it can also increase its MAJOR version without needing to # increase the overall CARA version (found at ``cara.__version__``). -__version__ = "3.2.0" +__version__ = "3.3.0" class BaseRequestHandler(RequestHandler): @@ -148,11 +148,13 @@ class StaticModel(BaseRequestHandler): class LandingPage(BaseRequestHandler): def get(self): + template_environment = self.settings["template_environment"] template = self.settings["template_environment"].get_template( "index.html.j2") report = template.render( user=self.current_user, calculator_prefix=self.settings["calculator_prefix"], + text_blocks=template_environment.globals['common_text'] ) self.finish(report) @@ -229,7 +231,7 @@ def make_app( calculator_templates = Path(__file__).parent / "templates" templates_directories = [cara_templates, calculator_templates] if theme_dir: - templates_directories.insert(0, theme_dir / 'templates') + templates_directories.insert(0, theme_dir) loader = jinja2.FileSystemLoader([str(path) for path in templates_directories]) template_environment = jinja2.Environment( loader=loader, diff --git a/cara/apps/calculator/__main__.py b/cara/apps/calculator/__main__.py index 7ec61206..7ec8b69a 100644 --- a/cara/apps/calculator/__main__.py +++ b/cara/apps/calculator/__main__.py @@ -36,7 +36,6 @@ def main(): if theme_dir is not None: theme_dir = Path(theme_dir).absolute() assert theme_dir.exists() - assert (theme_dir / 'templates').exists() app = make_app(debug=args.no_debug, calculator_prefix=args.prefix, theme_dir=theme_dir) app.listen(args.port) IOLoop.instance().start() diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index 9776e558..595094bc 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -700,7 +700,7 @@ MECHANICAL_VENTILATION_TYPES = {'mech_type_air_changes', 'mech_type_air_supply', MASK_TYPES = {'Type I', 'FFP2'} MASK_WEARING_OPTIONS = {'mask_on', 'mask_off'} VENTILATION_TYPES = {'natural_ventilation', 'mechanical_ventilation', 'no_ventilation'} -VIRUS_TYPES = {'SARS_CoV_2', 'SARS_CoV_2_ALPHA', 'SARS_CoV_2_GAMMA', 'SARS_CoV_2_DELTA', 'SARS_CoV_2_BETA'} +VIRUS_TYPES = {'SARS_CoV_2', 'SARS_CoV_2_ALPHA', 'SARS_CoV_2_BETA','SARS_CoV_2_GAMMA', 'SARS_CoV_2_DELTA', 'SARS_CoV_2_OMICRON'} VOLUME_TYPES = {'room_volume_explicit', 'room_volume_from_dimensions'} WINDOWS_OPENING_REGIMES = {'windows_open_permanently', 'windows_open_periodically', 'not-applicable'} WINDOWS_TYPES = {'window_sliding', 'window_hinged', 'not-applicable'} diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index 59ab1302..e703e225 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -308,10 +308,10 @@ class ReportGenerator: context['permalink'] = generate_permalink(base_url, self.calculator_prefix, form) context['calculator_prefix'] = self.calculator_prefix context['scale_warning'] = { - 'level': 'orange-3', - 'incidence_rate': 'somewhere in between 25 and 100 new cases per 100 000 inhabitants', - 'onsite_access': 'of about 5000', - 'threshold': '' + 'level': 'red-4', + 'incidence_rate': 'higher or equal to 100 new cases per 100 000 inhabitants', + 'onsite_access': 'lower than 4000', + 'threshold': '5%' } return context diff --git a/cara/apps/static/css/style.css b/cara/apps/static/css/style.css index 7e701ebc..022b4ca6 100644 --- a/cara/apps/static/css/style.css +++ b/cara/apps/static/css/style.css @@ -234,6 +234,10 @@ footer img { width: 25%; } + #mobile_calculator_option { + display: none; + } + #nat_vent_image { height: 15em; } @@ -246,7 +250,7 @@ footer img { height: 6em; margin: 1%; } - #mobile-app-buttons { + #calculator_app_button { display: none!important; } .feedback { @@ -286,6 +290,9 @@ footer img { .nav-link { padding: .5rem .5rem!important; } + #apps_dropdown { + display: none; + } #report_version { font-size: .5rem; } @@ -302,13 +309,6 @@ footer img { #mobile_link { display: inline!important; } - #desktop_logo { - display: none!important; - } - - #mobile_logo { - display: block!important; - } .feedback { float:right; font-size:.75rem; @@ -328,16 +328,16 @@ footer img { } */ -/* Large (lg) devices (desktops, 992px and up) */ -@media (max-width: 992px) { - #download-pdf { +/* Large (lg) devices (tablets) */ +@media (max-width: 64em) { + .expert_app_button { display: none; } - #link_reproduce_results { - display: none; + #desktop_logo { + display: none!important; } - #mobile_link { - display: inline!important; + #mobile_logo { + display: block!important; } } diff --git a/cara/apps/calculator/templates/calculator.form.html.j2 b/cara/apps/templates/base/calculator.form.html.j2 similarity index 96% rename from cara/apps/calculator/templates/calculator.form.html.j2 rename to cara/apps/templates/base/calculator.form.html.j2 index 3f5663f8..81cdae06 100644 --- a/cara/apps/calculator/templates/calculator.form.html.j2 +++ b/cara/apps/templates/base/calculator.form.html.j2 @@ -62,23 +62,28 @@ ?
-
- - +
+
+
+ +

Room data: -
+ {% block room_data %} +
?
+ {% endblock room_data %}
@@ -513,10 +518,11 @@ Virus data:
SARS-CoV-2 covers the original "wild type" strain of the virus and three variants of concern (VOC):
Modify the default as necessary, according to local area prevalence e.g. for Geneva or Ain (France).
diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/templates/base/calculator.report.html.j2 similarity index 97% rename from cara/apps/calculator/templates/base/calculator.report.html.j2 rename to cara/apps/templates/base/calculator.report.html.j2 index 123ae79c..b113710b 100644 --- a/cara/apps/calculator/templates/base/calculator.report.html.j2 +++ b/cara/apps/templates/base/calculator.report.html.j2 @@ -201,13 +201,15 @@ {% if form.virus_type == "SARS_CoV_2" %} SARS-CoV-2 (nominal strain) {% elif form.virus_type == "SARS_CoV_2_ALPHA" %} - SARS-CoV-2 (Alpha VOC) + SARS-CoV-2 (Alpha VOC) {% elif form.virus_type == "SARS_CoV_2_BETA" %} - SARS-CoV-2 (Beta VOC) + SARS-CoV-2 (Beta VOC) {% elif form.virus_type == "SARS_CoV_2_GAMMA" %} - SARS-CoV-2 (Gamma VOC) + SARS-CoV-2 (Gamma VOC) {% elif form.virus_type == "SARS_CoV_2_DELTA" %} - SARS-CoV-2 (Delta VOC) + SARS-CoV-2 (Delta VOC) + {% elif form.virus_type == "SARS_CoV_2_OMICRON" %} + SARS-CoV-2 (Omicron VOC) {% endif %}

  • Room Volume: {{ model.concentration_model.room.volume }} m³

  • diff --git a/cara/apps/templates/base/index.html.j2 b/cara/apps/templates/base/index.html.j2 new file mode 100644 index 00000000..3df8598f --- /dev/null +++ b/cara/apps/templates/base/index.html.j2 @@ -0,0 +1,59 @@ +{% extends "layout.html.j2" %} +{% set active_page="home/" %} + +{% block main %} + {#
    #} +
    +
    + + +
    +
    + +
    + +
    +
    +

    Introduction


    +
    +

    + CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions. + It does this by simulating the long-range airborne spread SARS-CoV-2 virus in a finite volume, assuming homogenous mixing, and it estimates the risk of COVID-19 airborne transmission therein. + Please see the About page for more details on the methodology, assumptions and limitations of CARA. +

    +

    + The full CARA source code can be accessed freely under an Apache 2.0 open source license from our code repository. + It includes detailed instructions on how to run your own version of this tool. +

    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +

    Apps:

    +
    + +
    + +
    +
    +
    + + {% block cara_at_cern %} + {% endblock cara_at_cern %} + +
    +

    Acknowledgements


    + {{ text_blocks['Acknowledgements'] }} + +
    +
    +{% endblock main %} diff --git a/cara/apps/templates/base/layout.html.j2 b/cara/apps/templates/base/layout.html.j2 new file mode 100644 index 00000000..cfa9bb09 --- /dev/null +++ b/cara/apps/templates/base/layout.html.j2 @@ -0,0 +1,120 @@ + + + + + + + + + + {% block title %} + CARA | COVID Airborne Risk Assessment + {% endblock title %} + + + + + + + + {% block extra_headers %} + {% endblock extra_headers %} + + + + + + +
    + {% block main %} + {% endblock main %} +
    + + + + + + + + + + + + + + + + {% block body_scripts %} + {% endblock body_scripts %} + + + diff --git a/cara/apps/calculator/templates/userguide.html.j2 b/cara/apps/templates/base/userguide.html.j2 similarity index 98% rename from cara/apps/calculator/templates/userguide.html.j2 rename to cara/apps/templates/base/userguide.html.j2 index c1c17a06..9de64992 100644 --- a/cara/apps/calculator/templates/userguide.html.j2 +++ b/cara/apps/templates/base/userguide.html.j2 @@ -70,8 +70,10 @@ The choices are:

    The user can modify the selected variant from the default, according to the prevalence of the different variants in the local area. Access to this information can be found here:

    -{% endblock main %} +{# The main index, this template is intended to be implemented by themes #} +{% extends "base/index.html.j2" %} \ No newline at end of file diff --git a/cara/apps/templates/layout.html.j2 b/cara/apps/templates/layout.html.j2 index 15020daa..1298f8eb 100644 --- a/cara/apps/templates/layout.html.j2 +++ b/cara/apps/templates/layout.html.j2 @@ -1,113 +1,2 @@ - - - - - - - - - - {% block title %} - CARA | COVID Airborne Risk Assessment - {% endblock title %} - - - - - - - - {% block extra_headers %} - {% endblock extra_headers %} - - - - - - -
    - {% block main %} - {% endblock main %} -
    - - - - - - - - - - - - - - - - {% block body_scripts %} - {% endblock body_scripts %} - - - +{# The main layout, this template is intended to be implemented by themes #} +{% extends "base/layout.html.j2" %} diff --git a/cara/apps/templates/userguide.html.j2 b/cara/apps/templates/userguide.html.j2 new file mode 100644 index 00000000..6b34bc4d --- /dev/null +++ b/cara/apps/templates/userguide.html.j2 @@ -0,0 +1,2 @@ +{# The main calculator report, this template is intended to be implemented by themes #} +{% extends "base/userguide.html.j2" %} \ No newline at end of file diff --git a/cara/models.py b/cara/models.py index ffd19ae9..3cedbf22 100644 --- a/cara/models.py +++ b/cara/models.py @@ -504,6 +504,12 @@ Virus.types = { viable_to_RNA_ratio = 0.5, transmissibility_factor=0.51, ), + 'SARS_CoV_2_OMICRON': SARSCoV2( + viral_load_in_sputum=1e9, + infectious_dose=20., + viable_to_RNA_ratio=0.5, + transmissibility_factor=0.2 + ), } diff --git a/cara/monte_carlo/data.py b/cara/monte_carlo/data.py index 47f0f901..3b9172d2 100644 --- a/cara/monte_carlo/data.py +++ b/cara/monte_carlo/data.py @@ -138,6 +138,12 @@ virus_distributions = { viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, transmissibility_factor=0.51, ), + 'SARS_CoV_2_OMICRON': mc.SARSCoV2( + viral_load_in_sputum=symptomatic_vl_frequencies, + infectious_dose=infectious_dose_distribution, + viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, + transmissibility_factor=0.2, + ), } diff --git a/cara/scripts/themes/cern/cara_script.command b/cara/scripts/themes/cern/cara_script.command index 0d957e2f..952d852a 100755 --- a/cara/scripts/themes/cern/cara_script.command +++ b/cara/scripts/themes/cern/cara_script.command @@ -12,4 +12,4 @@ pip3 install -e . echo "############################################" echo "CARA is now running at http://localhost:8080" echo "############################################" -python3 -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern \ No newline at end of file +python3 -m cara.apps.calculator --theme=cara/apps/templates/cern \ No newline at end of file diff --git a/cara/scripts/themes/cern/cara_script.sh b/cara/scripts/themes/cern/cara_script.sh index dcd21d3f..4a25af3e 100755 --- a/cara/scripts/themes/cern/cara_script.sh +++ b/cara/scripts/themes/cern/cara_script.sh @@ -6,4 +6,4 @@ pip install -e . echo "############################################" echo "CARA is now running at http://localhost:8080" echo "############################################" -python -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern \ No newline at end of file +python -m cara.apps.calculator --theme=cara/apps/templates/cern \ No newline at end of file diff --git a/cara/tests/apps/calculator/test_webapp.py b/cara/tests/apps/calculator/test_webapp.py index 09bc104c..65f3b216 100644 --- a/cara/tests/apps/calculator/test_webapp.py +++ b/cara/tests/apps/calculator/test_webapp.py @@ -72,14 +72,13 @@ class TestBasicApp(tornado.testing.AsyncHTTPTestCase): class TestCernApp(tornado.testing.AsyncHTTPTestCase): def get_app(self): - cern_theme = Path(cara.apps.calculator.__file__).parent / 'themes' / 'cern' + cern_theme = Path(cara.apps.calculator.__file__).parent.parent / 'themes' / 'cern' return cara.apps.calculator.make_app(theme_dir=cern_theme) @tornado.testing.gen_test(timeout=_TIMEOUT) def test_report(self): response = yield self.http_client.fetch(self.get_url('/calculator/baseline-model/result')) self.assertEqual(response.code, 200) - assert 'CERN HSE' in response.body.decode() assert 'expected number of new cases is' in response.body.decode() diff --git a/setup.py b/setup.py index e8136836..e057da5c 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ REQUIREMENTS: dict = { 'matplotlib', 'memoization', 'mistune', - 'numpy', + 'numpy != 1.22.0', 'psutil', 'python-dateutil', 'scipy',