From 2a4a9f9aa21a67510825e032b489b27f7e39950b Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 21 Jan 2026 11:44:52 -0500 Subject: [PATCH 01/13] WIP: add ORMS --- .env.jinja | 2 +- .envs/orms.env.jinja | 101 ++++++++++++++++++ compose.orms.yaml | 49 +++++++++ .../dynamic-config/services.yaml.jinja | 18 ++++ copier.yaml | 4 + scripts/init_orms.sh | 37 +++++++ scripts/refresh_data_orms.sh | 44 ++++++++ 7 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 .envs/orms.env.jinja create mode 100644 compose.orms.yaml create mode 100644 scripts/init_orms.sh create mode 100644 scripts/refresh_data_orms.sh diff --git a/.env.jinja b/.env.jinja index 99f25f4..0adcd05 100644 --- a/.env.jinja +++ b/.env.jinja @@ -54,5 +54,5 @@ APPLE_TOPIC={{ app_id }} # compose configuration - files and project name: COMPOSE_PATH_SEPARATOR=; -COMPOSE_FILE=compose.yaml{% if use_ofelia %};compose.ofelia.yaml{% endif %}{% if db_use_adminer %};compose.adminer.yaml{% endif %} +COMPOSE_FILE=compose.yaml{% if use_ofelia %};compose.ofelia.yaml{% endif %}{% if use_orms %};compose.orms.yaml{% endif %}{% if db_use_adminer %};compose.adminer.yaml{% endif %} # COMPOSE_PROJECT_NAME=opal_${ENVIRONMENT} diff --git a/.envs/orms.env.jinja b/.envs/orms.env.jinja new file mode 100644 index 0000000..60e1d87 --- /dev/null +++ b/.envs/orms.env.jinja @@ -0,0 +1,101 @@ +# WEB SERVER SETTINGS +BASE_PATH=/var/www/orms +BASE_URL=https://${APP_HOST}:${HTTPS_PORT}/orms +IMAGE_PATH=/var/www/orms/images +IMAGE_URL=https://${APP_HOST}/orms/images +LOG_PATH=/var/www/orms/logs + +# DATABASE CONNECTION SETTINGS +# ORMS database settings +ORMS_DATABASE_USER=${DB_USER} +ORMS_DATABASE_PASSWORD=${DB_PASSWORD} +ORMS_DATABASE_HOST=${DB_HOST} +ORMS_DATABASE_NAME=OrmsDatabase +ORMS_DATABASE_PORT=${DB_PORT} + +# Log database settings +LOG_DATABASE_USER=${DB_USER} +LOG_DATABASE_PASSWORD=${DB_PASSWORD} +LOG_DATABASE_HOST=${DB_HOST} +LOG_DATABASE_NAME=OrmsLog +LOG_DATABASE_PORT=${DB_PORT} + +# FIREBASE SETTINGS +# URL of the Firebase realtime database (NOTE! The slash is required after .com) +FIREBASE_CONFIG_PATH=/config/firebase-config.json +FIREBASE_BRANCH=/orms/${INSTITUTION_CODE} + +# OPAL INTERFACE ENGINE (OIE) SETTINGS +# Setting to enable/disable OIE communication +OIE_ENABLED=0 +#OIE_URL=https://${OIE_HOST}:${OIE_PORT} +OIE_URL= +OIE_USERNAME= +OIE_PASSWORD= + +# SMS SETTINGS +# Setting to enable/disable SMS messages +SMS_ENABLED=0 +SMS_PROVIDER=twilio +SMS_LICENCE_KEY= +SMS_TOKEN= +SMS_REMINDER_CRON_ENABLED=0 +SMS_INCOMING_SMS_CRON_ENABLED=0 + +# NEW OPAL ADMIN SETTINGS +NEW_OPAL_ADMIN_HOST_INTERNAL=http://admin:8000 +NEW_OPAL_ADMIN_HOST_EXTERNAL=https://${APP_HOST}:${HTTPS_PORT} +NEW_OPAL_ADMIN_TOKEN= + + +LEGACY_OPAL_ADMIN_HOST_INTERNAL=http://admin-legacy:8080 +# API settings for direct requests from orms to legacy OA endpoints +LEGACY_OPAL_ADMIN_API_USERNAME=orms +LEGACY_OPAL_ADMIN_API_PASSWORD= +LEGACY_OPAL_ADMIN_HOST_EXTERNAL=https://${APP_HOST}:${HTTPS_PORT}/opalAdmin + +# SETTINGS FOR THE WEIGHTS +# Setting to enable/disable sending weights PDF to the OIE +SEND_WEIGHTS=0 + +# SETTINGS FOR THE VIRTUAL WAITING ROOM +# Setting for enabling cron job +VWR_CRON_ENABLED=0 + +# MESSAGE TRANSLATIONS +FAILED_CHECK_IN_MESSAGE_EN = "There is a problem checking in for your appointment(s). If you have an appointment today, please go to the reception to complete the check-in process." +FAILED_CHECK_IN_MESSAGE_FR = "Une erreur s'est produite lors de l'enregistrement pour votre/vos rendez-vous. Si vous avez un rendez-vous aujourd'hui, veuillez vous rendre à la réception pour terminer le processus d'enregistrement." +UNKNOWN_COMMAND_MESSAGE_EN = "You have not been checked-in. To check-in for an appointment, please reply with the word \"arrive\". No other messages are accepted." +UNKNOWN_COMMAND_MESSAGE_FR = "Vous n\'avez pas été enregistré(e). Pour vous enregister pour votre rendez-vous, svp repondez \"arrive\". Aucun autre message ne sera accepté." + +# List of the email addresses (a.k.a., recipients) who should get an email when new appointment type detected +# The list should be separated by comma (e.g., ",") with no space +EMAIL= + +# List of long codes +# TODO: investigate what list values we need to provide +# The list should be separated by comma (e.g., ",") with no space +# REGISTERED_LONG_CODES= + +# SMTP MAILER SETTINGS +# ORMS sends emails in 2 cases: +# - An error occurs in a cron +# - A new appointment code / clinic code combination is detected. +# This was supposed to be used to notify the staff that there's +# a new appointment type that should be enabled for SMS +RECIPIENT_EMAILS= +EMAIL_HOST= +EMAIL_USER= +EMAIL_PASSWORD= +EMAIL_SENT_FROM_ADDRESS= +EMAIL_PORT=587 + +DATABASE_USE_SSL=${DB_USE_TLS} +{% if db_use_tls -%} +# Path to your CA public key file; used for DB connections if DATABASE_USE_SSL is enabled +SSL_CA=${DB_CERTS} +{% endif -%} + +# Source System host for appointment location updates from orms (kiosk/vwr/sms patient checkins) +SOURCE_SYSTEM_SUPPORTS_CHECKIN=0 +SOURCE_SYSTEM_HOST_EXTERNAL= diff --git a/compose.orms.yaml b/compose.orms.yaml new file mode 100644 index 0000000..1b6dc20 --- /dev/null +++ b/compose.orms.yaml @@ -0,0 +1,49 @@ +services: + memcached: + image: memcached:1.6.40-alpine3.23 + environment: + - TZ=${TIMEZONE} + orms: + image: ghcr.io/opalmedapps/opal-rms:main + restart: unless-stopped + depends_on: + - memcached + - db + env_file: + - $PWD/.envs/orms.env + environment: + - PHP_MEMORY_LIMIT=512M + - TZ=${TIMEZONE} + volumes: + - type: bind + source: $PWD/config/firebase/web-config.json + target: /config/firebase-config.json + read_only: true + labels: + - "ofelia.enabled=true" + - "ofelia.job-exec.vwr-appointments.schedule=@every 3s" + - "ofelia.job-exec.vwr-appointments.command=php php/cron/generateVwrAppointments.php" + - "ofelia.job-exec.vwr-appointments.no-overlap=true" + - "ofelia.job-exec.vwr-appointments.user=www-data" + # - "ofelia.job-exec.incoming-sms.schedule=@every 5s" + # - "ofelia.job-exec.incoming-sms.command=php php/cron/processIncomingSmsMessages.php" + # - "ofelia.job-exec.incoming-sms.no-overlap=true" + - "ofelia.job-exec.appointment-reminder.schedule=0 0 18 * * *" + - "ofelia.job-exec.appointment-reminder.command=php php/cron/generateAppointmentReminders.php" + - "ofelia.job-exec.checkout.schedule=0 30 23 * * *" + - "ofelia.job-exec.checkout.command=php php/cron/endOfDayCheckout.php" + admin: + environment: + - ORMS_ENABLED=True + - ORMS_HOST=https://${APP_HOST}/orms + admin-legacy: + environment: + - ORMS_ENABLED=1 + - ORMS_HOST=https://${APP_HOST}/orms + listener: + environment: + - ORMS_ENABLED=1 + - CHECKIN_ROOM=OPAL PHONE APP + # URL notified when a questionnaire is completed + - QUESTIONNAIRE_COMPLETED_URL=http://orms:8080/orms/php/api/public/v1/patient/notifyNewQuestionnaireResponse + - ORMS_CHECKIN_URL=http://orms:8080/orms/php/api/public/v1/patient/checkInToLocation diff --git a/config/traefik/dynamic-config/services.yaml.jinja b/config/traefik/dynamic-config/services.yaml.jinja index 81bed78..010d158 100644 --- a/config/traefik/dynamic-config/services.yaml.jinja +++ b/config/traefik/dynamic-config/services.yaml.jinja @@ -56,6 +56,17 @@ http: tls: {} {%- endif %} + {% if use_orms -%} + orms: + rule: 'Host(`{{ env "APP_HOST" }}`) && PathPrefix(`/orms`)' + entryPoints: + - web-secure + middlewares: + - redirect-missing-slash + service: orms + tls: {} + {%- endif %} + middlewares: redirect-root: redirectRegex: @@ -113,3 +124,10 @@ http: servers: - url: http://adminer:8080 {%- endif %} + + {% if use_orms - %} + orms: + loadBalancer: + servers: + - url: http://orms:8080 + {%- endif %} diff --git a/copier.yaml b/copier.yaml index db819f3..dc6ad15 100644 --- a/copier.yaml +++ b/copier.yaml @@ -309,6 +309,10 @@ _tasks: - command: | echo "Running init_db script to initialize DB..." ENVIRONMENT="{{ environment }}" DB_ROOT_USER="{{ db_root_user }}" DB_ROOT_PASSWORD="{{ db_root_password }}" DB_HOST="{{ db_host }}" DB_PORT="{{ db_port }}" DB_USER="{{ db_user }}" DB_PASSWORD="{{ db_password }}" DB_NAME=admin bash ./scripts/init_db.sh + - command: | + echo "Running init_orms script to initialize ORMS DB..." + ENVIRONMENT="{{ environment }}" DB_ROOT_USER="{{ db_root_user }}" DB_ROOT_PASSWORD="{{ db_root_password }}" DB_HOST="{{ db_host }}" DB_PORT="{{ db_port }}" DB_USER="{{ db_user }}" DB_PASSWORD="{{ db_password }}" DB_NAME=admin bash ./scripts/init_orms.sh + when: "{{ use_orms }}" # run admin - docker compose up -d admin # Migrate schemas and initialize data diff --git a/scripts/init_orms.sh b/scripts/init_orms.sh new file mode 100644 index 0000000..a68d467 --- /dev/null +++ b/scripts/init_orms.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +# renovate: datasource=docker depName=alpine +ALPINE_VERSION="3.23.2" +WAIT_FOR_IT_VERSION="latest" + +echo "Waiting for DB container to be ready..." +docker run --rm -it --network opal-${ENVIRONMENT} chainguard/wait-for-it:${WAIT_FOR_IT_VERSION} --host="$DB_HOST" --port="$DB_PORT" --timeout=20 + +echo "Running container for mysql-client..." +docker run --rm --interactive \ + --env DB_ROOT_USER=${DB_ROOT_USER} \ + --env DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} \ + --env DB_HOST=${DB_HOST} \ + --env DB_USER=${DB_USER} \ + --env DB_PASSWORD=${DB_PASSWORD} \ + --network opal-${ENVIRONMENT} \ + alpine:${ALPINE_VERSION} sh -s << EOF +set -euo pipefail +apk add --no-cache mysql-client +echo "Connecting to DB server on ${DB_HOST}:${DB_PORT}..." +echo "Creating ORMS DBs..." +MYSQL_PWD=${DB_ROOT_PASSWORD} mariadb --protocol tcp --skip-ssl --user ${DB_ROOT_USER} --host ${DB_HOST} --port ${DB_PORT} <<'EOIF' +CREATE DATABASE IF NOT EXISTS \`OrmsDatabase\` /*!40100 DEFAULT CHARACTER SET latin1 */; +CREATE DATABASE IF NOT EXISTS \`OrmsLog\` /*!40100 DEFAULT CHARACTER SET latin1 */; +EOIF +echo "Successfully created ORMS DBs" +echo "Granting privileges to DB user..." +MYSQL_PWD=${DB_ROOT_PASSWORD} mariadb --protocol tcp --skip-ssl --user ${DB_ROOT_USER} --host ${DB_HOST} --port ${DB_PORT} <<'EOIF' +GRANT ALL PRIVILEGES ON \`OrmsDatabase\`.* TO '$DB_USER'@'%'; +GRANT ALL PRIVILEGES ON \`OrmsLog\`.* TO '$DB_USER'@'%'; +FLUSH PRIVILEGES; +EOIF +echo "Successfully granted privileges to DB user" +echo "Done!" +EOF diff --git a/scripts/refresh_data_orms.sh b/scripts/refresh_data_orms.sh new file mode 100644 index 0000000..69419e4 --- /dev/null +++ b/scripts/refresh_data_orms.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Description: Refresh data in a non-production environment + +set -euo pipefail + +echo "Beginning ORMS test data reset..." + +# Check for required arguments +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + echo "Valid institutions: OMI, OHIGPH" + exit 1 +fi + +institution=$1 +institution_lower=$(echo $1 | tr '[:upper:]' '[:lower:]') + +# Validate the institution +if [[ "$institution" != "OMI" && "$institution" != "OHIGPH" ]]; then + echo "Invalid institution: $institution" + echo "Valid institutions: OMI, OHIGPH" + exit 1 +fi + +set -euxo pipefail + +echo "Upgrading OrmsDB..." +docker compose run --rm db-management alembic --name ormsdb upgrade head +echo "Upgrading OrmsLogDB..." +docker compose run --rm db-management alembic --name ormslogdb upgrade head + +docker compose run --rm db-management python -m db_management.run_sql_scripts OrmsDB db_management/ormsdb/data/truncate/ +docker compose run --rm db-management python -m db_management.run_sql_scripts OrmsLogDB db_management/ormslogdb/data/truncate/ + +docker compose run --rm db-management python -m db_management.run_sql_scripts OrmsDB db_management/ormsdb/data/initial/ +docker compose run --rm db-management python -m db_management.run_sql_scripts OrmsDB db_management/ormsdb/data/test/ +docker compose run --rm db-management python -m db_management.run_sql_scripts OrmsDB db_management/ormsdb/data/test/$institution_lower/ +docker compose run --rm db-management python -m db_management.run_sql_scripts OrmsLogDB db_management/ormslogdb/data/initial/ +docker compose run --rm db-management python -m db_management.run_sql_scripts OrmsLogDB db_management/ormslogdb/data/test/ +docker compose run --rm db-management python -m db_management.run_sql_scripts OrmsLogDB db_management/ormslogdb/data/test/$institution_lower/ + +docker compose exec admin python manage.py update_orms_patients + +echo "ORMS test data successfully reset." From 6c6c02884cfd3ec8681517efcc123eff3563bb5b Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Tue, 27 Jan 2026 17:12:58 -0500 Subject: [PATCH 02/13] add test case --- README.md | 9 ++++++++- copier.yaml | 7 +++++++ ctt.toml | 9 +++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 50155b2..47f0402 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Prepare a directory for these files with the following contents: - `firebase-admin-key.json`: The private key of the service account used by the [Firebase Admin SDK](https://firebase.google.com/docs/database/admin/start#admin-sdk-authentication) - `apn.crt` and `apn.key`: The public and private certificates for the Apple Push Notification service. - The private key cannot be password-protected + The private key cannot be password-protected. ### Details @@ -226,6 +226,13 @@ Some questions are conditional based on your answer to a previous question. It is an alternative to using the cron daemon on the host. *Ofelia* sends emails for any failing job. +1. **Do you want to use the room management component (ORMS)?** + + [ORMS](https://github.com/opalmedapps/#:~:text=opal%2Drms) provides a virtual waiting room. + Patients can check-in for an appointment via a kiosk. + Clinical staff can see checked in patients and call them to a room. + The called patients can be displayed on a TV screen. + 1. **The Firebase project name** The name of the Firebase project that is used for communication with the mobile app. diff --git a/copier.yaml b/copier.yaml index b4dde9b..7816047 100644 --- a/copier.yaml +++ b/copier.yaml @@ -175,6 +175,11 @@ use_ofelia: help: Do you want to use Ofelia as a job scheduler to run period jobs? default: true +use_orms: + type: bool + help: Do you want to use the room management component (ORMS)? + default: false + firebase_project_name: type: str help: The Firebase project name @@ -400,5 +405,7 @@ _exclude: - "zizmor.yml" # - "README.md" - extensions + - "{% if not use_orms %}compose.orms.yaml{% endif %}" + - "{% if not use_ofelia %}compose.ofelia.yaml{% endif %}" - "{% if not is_test %}scripts/cleanup_db.sh{% endif %}" - "{% if not is_test %}tests{% endif %}" diff --git a/ctt.toml b/ctt.toml index 10cceed..14c2bbe 100644 --- a/ctt.toml +++ b/ctt.toml @@ -17,6 +17,7 @@ _extra_tasks = [ [output.".ctt/defaults"] _extra_tasks = [ + "! ls compose.orms.yaml", "uv run tests/admin_login.py '{{ admin_password }}'", "uv run tests/system_user_login.py '{{ labs_password }}'", "uv run tests/labs_basic_auth.py '{{ labs_password }}'", @@ -64,3 +65,11 @@ db_root_password = "root-password" # TODO: add test case with TLS enabled db_use_tls = 0 use_custom_certs = false + +# orms enabled +[output.".ctt/orms_enabled"] +use_orms = true +_extra_tasks = [ + "ls compose.orms.yaml", + "docker compose down", +] From 982f9070ce2acc1cca873613b048aad5cb5b618b Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Tue, 27 Jan 2026 17:20:46 -0500 Subject: [PATCH 03/13] fix template error in services --- config/traefik/dynamic-config/services.yaml.jinja | 4 ++-- copier.yaml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/traefik/dynamic-config/services.yaml.jinja b/config/traefik/dynamic-config/services.yaml.jinja index 010d158..f07cf48 100644 --- a/config/traefik/dynamic-config/services.yaml.jinja +++ b/config/traefik/dynamic-config/services.yaml.jinja @@ -58,7 +58,7 @@ http: {% if use_orms -%} orms: - rule: 'Host(`{{ env "APP_HOST" }}`) && PathPrefix(`/orms`)' + rule: {% raw %}'Host(`{{ env "APP_HOST" }}`) && PathPrefix(`/orms`)'{% endraw %} entryPoints: - web-secure middlewares: @@ -125,7 +125,7 @@ http: - url: http://adminer:8080 {%- endif %} - {% if use_orms - %} + {% if use_orms -%} orms: loadBalancer: servers: diff --git a/copier.yaml b/copier.yaml index 7816047..2a5d283 100644 --- a/copier.yaml +++ b/copier.yaml @@ -403,8 +403,10 @@ _exclude: - "renovate.json5" - "ruff.toml" - "zizmor.yml" + - "LICENSE" # - "README.md" - extensions + - "{% if not db_use_adminer %}compose.adminer.yaml{% endif %}" - "{% if not use_orms %}compose.orms.yaml{% endif %}" - "{% if not use_ofelia %}compose.ofelia.yaml{% endif %}" - "{% if not is_test %}scripts/cleanup_db.sh{% endif %}" From 9a3708678572de6dd7203a0ab3ac206e88df425c Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Tue, 27 Jan 2026 17:27:34 -0500 Subject: [PATCH 04/13] interactive only --- scripts/init_orms.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/init_orms.sh b/scripts/init_orms.sh index a68d467..535b150 100644 --- a/scripts/init_orms.sh +++ b/scripts/init_orms.sh @@ -6,7 +6,7 @@ ALPINE_VERSION="3.23.2" WAIT_FOR_IT_VERSION="latest" echo "Waiting for DB container to be ready..." -docker run --rm -it --network opal-${ENVIRONMENT} chainguard/wait-for-it:${WAIT_FOR_IT_VERSION} --host="$DB_HOST" --port="$DB_PORT" --timeout=20 +docker run --rm --interactive --network opal-${ENVIRONMENT} chainguard/wait-for-it:${WAIT_FOR_IT_VERSION} --host="$DB_HOST" --port="$DB_PORT" --timeout=20 echo "Running container for mysql-client..." docker run --rm --interactive \ From c39a62d16bae0a9cdcd9a82353807721bc02b88d Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 28 Jan 2026 14:36:32 -0500 Subject: [PATCH 05/13] test for orms system user --- .envs/orms.env.jinja | 2 +- ctt.toml | 3 ++- tests/system_user_login.py | 10 ++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.envs/orms.env.jinja b/.envs/orms.env.jinja index 60e1d87..c53e9fe 100644 --- a/.envs/orms.env.jinja +++ b/.envs/orms.env.jinja @@ -23,7 +23,7 @@ LOG_DATABASE_PORT=${DB_PORT} # FIREBASE SETTINGS # URL of the Firebase realtime database (NOTE! The slash is required after .com) FIREBASE_CONFIG_PATH=/config/firebase-config.json -FIREBASE_BRANCH=/orms/${INSTITUTION_CODE} +FIREBASE_BRANCH=/dev3/${INSTITUTION_CODE}/orms/ # OPAL INTERFACE ENGINE (OIE) SETTINGS # Setting to enable/disable OIE communication diff --git a/ctt.toml b/ctt.toml index 14c2bbe..49605ca 100644 --- a/ctt.toml +++ b/ctt.toml @@ -19,7 +19,7 @@ _extra_tasks = [ _extra_tasks = [ "! ls compose.orms.yaml", "uv run tests/admin_login.py '{{ admin_password }}'", - "uv run tests/system_user_login.py '{{ labs_password }}'", + "uv run tests/system_user_login.py interface-engine '{{ labs_password }}'", "uv run tests/labs_basic_auth.py '{{ labs_password }}'", "uv run tests/validate_token.py admin_token {{ admin_token }}", "uv run tests/validate_token.py listener_token {{ listener_token }}", @@ -71,5 +71,6 @@ use_custom_certs = false use_orms = true _extra_tasks = [ "ls compose.orms.yaml", + "uv run tests/system_user_login.py orms {{ orms_password }}", "docker compose down", ] diff --git a/tests/system_user_login.py b/tests/system_user_login.py index c81f589..992b1b0 100644 --- a/tests/system_user_login.py +++ b/tests/system_user_login.py @@ -9,12 +9,14 @@ import typer -def main(labs_password: str): +def main(username: str, password: str): + print(f'Attempting system user login for user: {username}') + response = requests.post( 'https://localhost/opalAdmin/user/system-login', data={ - 'username': 'interface-engine', - 'password': labs_password, + 'username': username, + 'password': password, }, timeout=5, allow_redirects=False, @@ -26,7 +28,7 @@ def main(labs_password: str): print(f'System user login failed: {response.status_code} {response.text}') raise typer.Exit(code=1) - print('System user login succeeded') + print(f'System user login succeeded for user: {username}') if __name__ == '__main__': From 6708ae2b7e82519ecb0b411b015bf150ed7966d9 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 28 Jan 2026 14:50:24 -0500 Subject: [PATCH 06/13] add orms_password and orms_token --- .envs/orms.env.jinja | 4 ++-- copier.yaml | 13 ++++++++++++- scripts/init_orms.sh | 3 +++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.envs/orms.env.jinja b/.envs/orms.env.jinja index c53e9fe..8c582ae 100644 --- a/.envs/orms.env.jinja +++ b/.envs/orms.env.jinja @@ -45,13 +45,13 @@ SMS_INCOMING_SMS_CRON_ENABLED=0 # NEW OPAL ADMIN SETTINGS NEW_OPAL_ADMIN_HOST_INTERNAL=http://admin:8000 NEW_OPAL_ADMIN_HOST_EXTERNAL=https://${APP_HOST}:${HTTPS_PORT} -NEW_OPAL_ADMIN_TOKEN= +NEW_OPAL_ADMIN_TOKEN={{ orms_token }} LEGACY_OPAL_ADMIN_HOST_INTERNAL=http://admin-legacy:8080 # API settings for direct requests from orms to legacy OA endpoints LEGACY_OPAL_ADMIN_API_USERNAME=orms -LEGACY_OPAL_ADMIN_API_PASSWORD= +LEGACY_OPAL_ADMIN_API_PASSWORD={{ orms_password }} LEGACY_OPAL_ADMIN_HOST_EXTERNAL=https://${APP_HOST}:${HTTPS_PORT}/opalAdmin # SETTINGS FOR THE WEIGHTS diff --git a/copier.yaml b/copier.yaml index 2a5d283..04d326a 100644 --- a/copier.yaml +++ b/copier.yaml @@ -228,6 +228,12 @@ interface_engine_token: default: "{% if run_setup %}{{ random_token(20) }}{% endif %}" when: false +orms_token: + type: str + default: "{{ existing_secret('.envs/orms.env', 'NEW_OPAL_ADMIN_TOKEN') or random_token(20) }}" + when: false + + admin_token: type: str default: "{{ existing_secret('.envs/admin-legacy.env', 'NEW_OPALADMIN_TOKEN') or random_token(20) }}" @@ -243,6 +249,11 @@ labs_password: default: "{% if run_setup %}{{ random_password(20) }}{% endif %}" when: false +orms_password: + type: str + default: "{% if run_setup %}{{ random_password(20) }}{% endif %}" + when: false + _message_after_copy: | Your project "{{ project_name }}" has been created successfully! @@ -361,7 +372,7 @@ _tasks: ENVIRONMENT="{{ environment }}" DB_ROOT_USER="{{ db_root_user }}" DB_ROOT_PASSWORD="{{ db_root_password }}" DB_HOST="{{ db_host }}" DB_PORT="{{ db_port }}" DB_USER="{{ db_user }}" DB_PASSWORD="{{ db_password }}" DB_NAME=admin bash ./scripts/init_db.sh - command: | echo "Running init_orms script to initialize ORMS DB..." - ENVIRONMENT="{{ environment }}" DB_ROOT_USER="{{ db_root_user }}" DB_ROOT_PASSWORD="{{ db_root_password }}" DB_HOST="{{ db_host }}" DB_PORT="{{ db_port }}" DB_USER="{{ db_user }}" DB_PASSWORD="{{ db_password }}" DB_NAME=admin bash ./scripts/init_orms.sh + ENVIRONMENT="{{ environment }}" DB_ROOT_USER="{{ db_root_user }}" DB_ROOT_PASSWORD="{{ db_root_password }}" DB_HOST="{{ db_host }}" DB_PORT="{{ db_port }}" DB_USER="{{ db_user }}" DB_PASSWORD="{{ db_password }}" DB_NAME=admin ORMS_PASSWORD={{ orms_password }} bash ./scripts/init_orms.sh when: "{{ use_orms }}" # run admin - docker compose up -d admin diff --git a/scripts/init_orms.sh b/scripts/init_orms.sh index 535b150..1444fd2 100644 --- a/scripts/init_orms.sh +++ b/scripts/init_orms.sh @@ -35,3 +35,6 @@ EOIF echo "Successfully granted privileges to DB user" echo "Done!" EOF + +# explicitly set a password for the orms user to allow logins on admin-legacy +docker compose exec admin python manage.py shell -c 'user = User.objects.get(username="orms"); user.set_password("${ORMS_PASSWORD}"); user.save();' From cdd7b22b1ef0202ff339837c6cf22e4afeb973a8 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 28 Jan 2026 15:53:13 -0500 Subject: [PATCH 07/13] move set password for orms after calling initialize --- copier.yaml | 6 +++++- scripts/init_orms.sh | 3 --- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/copier.yaml b/copier.yaml index 04d326a..aef20d9 100644 --- a/copier.yaml +++ b/copier.yaml @@ -378,7 +378,11 @@ _tasks: - docker compose up -d admin # Migrate schemas and initialize data - command: | - LISTENER_TOKEN="{{ listener_token }}" LISTENER_REGISTRATION_TOKEN="{{ listener_registration_token }}" INTERFACE_ENGINE_TOKEN="{{ interface_engine_token }}" INTERFACE_ENGINE_PASSWORD="{{ labs_password }}" ADMIN_TOKEN="{{ admin_token }}" ADMIN_PASSWORD={{ admin_password }} ./scripts/initialize.sh + LISTENER_TOKEN="{{ listener_token }}" LISTENER_REGISTRATION_TOKEN="{{ listener_registration_token }}" INTERFACE_ENGINE_TOKEN="{{ interface_engine_token }}" INTERFACE_ENGINE_PASSWORD="{{ labs_password }}" ADMIN_TOKEN="{{ admin_token }}" ADMIN_PASSWORD="{{ admin_password }}" ./scripts/initialize.sh + # explicitly set a password for the orms user to allow logins on admin-legacy + - command: | + docker compose exec admin python manage.py shell -c 'user = User.objects.get(username="orms"); user.set_password("${ORMS_PASSWORD}"); user.save();' + when: "{{ use_orms }}" # run the remaining services - command: | docker compose up -d diff --git a/scripts/init_orms.sh b/scripts/init_orms.sh index 1444fd2..535b150 100644 --- a/scripts/init_orms.sh +++ b/scripts/init_orms.sh @@ -35,6 +35,3 @@ EOIF echo "Successfully granted privileges to DB user" echo "Done!" EOF - -# explicitly set a password for the orms user to allow logins on admin-legacy -docker compose exec admin python manage.py shell -c 'user = User.objects.get(username="orms"); user.set_password("${ORMS_PASSWORD}"); user.save();' From 0201120f23cf9c0399cfd9402aff61de062b9fee Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 28 Jan 2026 15:53:52 -0500 Subject: [PATCH 08/13] ignore unknown options to account for leading dash in passwords --- tests/admin_login.py | 5 ++++- tests/labs_basic_auth.py | 5 ++++- tests/system_user_login.py | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/admin_login.py b/tests/admin_login.py index cbf9dff..7d68e34 100644 --- a/tests/admin_login.py +++ b/tests/admin_login.py @@ -8,7 +8,10 @@ import requests import typer +app = typer.Typer() + +@app.command(context_settings={'ignore_unknown_options': True}) def main(admin_password: str): response = requests.post( 'https://localhost/opalAdmin/user/validate-login', @@ -30,4 +33,4 @@ def main(admin_password: str): if __name__ == '__main__': - typer.run(main) + app() diff --git a/tests/labs_basic_auth.py b/tests/labs_basic_auth.py index d0435f3..6d3c711 100644 --- a/tests/labs_basic_auth.py +++ b/tests/labs_basic_auth.py @@ -11,7 +11,10 @@ import requests import typer +app = typer.Typer() + +@app.command(context_settings={'ignore_unknown_options': True}) def main(labs_password: str): response = requests.post( 'https://localhost/opalAdmin/labs/api/processLabForPatient.php', @@ -38,4 +41,4 @@ def main(labs_password: str): if __name__ == '__main__': - typer.run(main) + app() diff --git a/tests/system_user_login.py b/tests/system_user_login.py index 992b1b0..4576fcb 100644 --- a/tests/system_user_login.py +++ b/tests/system_user_login.py @@ -8,7 +8,10 @@ import requests import typer +app = typer.Typer() + +@app.command(context_settings={'ignore_unknown_options': True}) def main(username: str, password: str): print(f'Attempting system user login for user: {username}') @@ -32,4 +35,4 @@ def main(username: str, password: str): if __name__ == '__main__': - typer.run(main) + app() From 9d9a671f5c95099b4aa43615512dce933a69d39a Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 28 Jan 2026 16:11:29 -0500 Subject: [PATCH 09/13] debug --- copier.yaml | 2 +- ctt.toml | 94 ++++++++++++++++++++++++++--------------------------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/copier.yaml b/copier.yaml index aef20d9..f754ed3 100644 --- a/copier.yaml +++ b/copier.yaml @@ -372,7 +372,7 @@ _tasks: ENVIRONMENT="{{ environment }}" DB_ROOT_USER="{{ db_root_user }}" DB_ROOT_PASSWORD="{{ db_root_password }}" DB_HOST="{{ db_host }}" DB_PORT="{{ db_port }}" DB_USER="{{ db_user }}" DB_PASSWORD="{{ db_password }}" DB_NAME=admin bash ./scripts/init_db.sh - command: | echo "Running init_orms script to initialize ORMS DB..." - ENVIRONMENT="{{ environment }}" DB_ROOT_USER="{{ db_root_user }}" DB_ROOT_PASSWORD="{{ db_root_password }}" DB_HOST="{{ db_host }}" DB_PORT="{{ db_port }}" DB_USER="{{ db_user }}" DB_PASSWORD="{{ db_password }}" DB_NAME=admin ORMS_PASSWORD={{ orms_password }} bash ./scripts/init_orms.sh + ENVIRONMENT="{{ environment }}" DB_ROOT_USER="{{ db_root_user }}" DB_ROOT_PASSWORD="{{ db_root_password }}" DB_HOST="{{ db_host }}" DB_PORT="{{ db_port }}" DB_USER="{{ db_user }}" DB_PASSWORD="{{ db_password }}" DB_NAME=admin bash ./scripts/init_orms.sh when: "{{ use_orms }}" # run admin - docker compose up -d admin diff --git a/ctt.toml b/ctt.toml index 49605ca..2131280 100644 --- a/ctt.toml +++ b/ctt.toml @@ -15,56 +15,56 @@ _extra_tasks = [ "docker compose down", ] -[output.".ctt/defaults"] -_extra_tasks = [ - "! ls compose.orms.yaml", - "uv run tests/admin_login.py '{{ admin_password }}'", - "uv run tests/system_user_login.py interface-engine '{{ labs_password }}'", - "uv run tests/labs_basic_auth.py '{{ labs_password }}'", - "uv run tests/validate_token.py admin_token {{ admin_token }}", - "uv run tests/validate_token.py listener_token {{ listener_token }}", - "uv run tests/validate_token.py listener_registration_token {{ listener_registration_token }}", - "uv run tests/validate_token.py interface_engine_token {{ interface_engine_token }}", - # need to set legacy DBs to test mode so that refresh_data script can run - "docker compose run --rm db-management python -m db_management.run_sql_scripts OpalDB db_management/opaldb/data/test/testmode/", - "docker compose run --rm db-management python -m db_management.run_sql_scripts QuestionnaireDB db_management/questionnairedb/data/test/testmode/", - "scripts/refresh_data.sh OMI", - "docker compose down", -] +# [output.".ctt/defaults"] +# _extra_tasks = [ +# "! ls compose.orms.yaml", +# "uv run tests/admin_login.py '{{ admin_password }}'", +# "uv run tests/system_user_login.py interface-engine '{{ labs_password }}'", +# "uv run tests/labs_basic_auth.py '{{ labs_password }}'", +# "uv run tests/validate_token.py admin_token {{ admin_token }}", +# "uv run tests/validate_token.py listener_token {{ listener_token }}", +# "uv run tests/validate_token.py listener_registration_token {{ listener_registration_token }}", +# "uv run tests/validate_token.py interface_engine_token {{ interface_engine_token }}", +# # need to set legacy DBs to test mode so that refresh_data script can run +# "docker compose run --rm db-management python -m db_management.run_sql_scripts OpalDB db_management/opaldb/data/test/testmode/", +# "docker compose run --rm db-management python -m db_management.run_sql_scripts QuestionnaireDB db_management/questionnairedb/data/test/testmode/", +# "scripts/refresh_data.sh OMI", +# "docker compose down", +# ] -# the tests are excluded -[output.".ctt/no_test"] -is_test = false -_extra_tasks = [ - "! ls tests", - "docker compose down", -] +# # the tests are excluded +# [output.".ctt/no_test"] +# is_test = false +# _extra_tasks = [ +# "! ls tests", +# "docker compose down", +# ] -# DB on same host -[output.".ctt/db_same_host"] -db_host_type = "same_server" -db_port = 3307 -db_root_user = "root" -db_root_password = "root-password" -_extra_tasks = [ - # need to clean up on existing DB server - "ENVIRONMENT='{{ environment }}' DB_ROOT_USER='{{ db_root_user }}' DB_ROOT_PASSWORD='{{ db_root_password }}' DB_HOST='{{ db_host }}' DB_PORT='{{ db_port }}' DB_USER='{{ db_user }}' DB_NAME=admin ./scripts/cleanup_db.sh", - "docker compose down", -] +# # DB on same host +# [output.".ctt/db_same_host"] +# db_host_type = "same_server" +# db_port = 3307 +# db_root_user = "root" +# db_root_password = "root-password" +# _extra_tasks = [ +# # need to clean up on existing DB server +# "ENVIRONMENT='{{ environment }}' DB_ROOT_USER='{{ db_root_user }}' DB_ROOT_PASSWORD='{{ db_root_password }}' DB_HOST='{{ db_host }}' DB_PORT='{{ db_port }}' DB_USER='{{ db_user }}' DB_NAME=admin ./scripts/cleanup_db.sh", +# "docker compose down", +# ] -# DB on a different server requires a db_host -[output.".ctt/db_different_server"] -db_host_type = "separate_server" -# use the service container to pretend it is a separate server -# very tricky to test in CI with a dedicated hostname -db_host = "host.docker.internal" -db_port = 3307 -db_root_user = "root" -db_root_password = "root-password" -# force no TLS during test, requires certificate otherwise -# TODO: add test case with TLS enabled -db_use_tls = 0 -use_custom_certs = false +# # DB on a different server requires a db_host +# [output.".ctt/db_different_server"] +# db_host_type = "separate_server" +# # use the service container to pretend it is a separate server +# # very tricky to test in CI with a dedicated hostname +# db_host = "host.docker.internal" +# db_port = 3307 +# db_root_user = "root" +# db_root_password = "root-password" +# # force no TLS during test, requires certificate otherwise +# # TODO: add test case with TLS enabled +# db_use_tls = 0 +# use_custom_certs = false # orms enabled [output.".ctt/orms_enabled"] From 81654fe187b3eb8dc9480cb890dcfe3c15c0cce4 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 28 Jan 2026 17:10:11 -0500 Subject: [PATCH 10/13] debug --- ctt.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ctt.toml b/ctt.toml index 2131280..bb6effa 100644 --- a/ctt.toml +++ b/ctt.toml @@ -71,6 +71,7 @@ _extra_tasks = [ use_orms = true _extra_tasks = [ "ls compose.orms.yaml", + "docker compose exec admin python manage.py shell -c 'print(LegacyOAUser.objects.all())'", "uv run tests/system_user_login.py orms {{ orms_password }}", "docker compose down", ] From d66a58f247a13186e33d834fa42ea6c3a6db3770 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 28 Jan 2026 17:15:51 -0500 Subject: [PATCH 11/13] debug --- ctt.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctt.toml b/ctt.toml index bb6effa..6193fde 100644 --- a/ctt.toml +++ b/ctt.toml @@ -71,7 +71,7 @@ _extra_tasks = [ use_orms = true _extra_tasks = [ "ls compose.orms.yaml", - "docker compose exec admin python manage.py shell -c 'print(LegacyOAUser.objects.all())'", + "docker compose exec admin python manage.py shell -c 'print(LegacyOAUser.objects.all().values_list(\"username\"))'", "uv run tests/system_user_login.py orms {{ orms_password }}", "docker compose down", ] From 56086794d48b86e3a1ae10c78cc982965a0ab250 Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 28 Jan 2026 19:47:50 -0500 Subject: [PATCH 12/13] use orms_password variable --- copier.yaml | 2 +- ctt.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/copier.yaml b/copier.yaml index f754ed3..1e591ae 100644 --- a/copier.yaml +++ b/copier.yaml @@ -381,7 +381,7 @@ _tasks: LISTENER_TOKEN="{{ listener_token }}" LISTENER_REGISTRATION_TOKEN="{{ listener_registration_token }}" INTERFACE_ENGINE_TOKEN="{{ interface_engine_token }}" INTERFACE_ENGINE_PASSWORD="{{ labs_password }}" ADMIN_TOKEN="{{ admin_token }}" ADMIN_PASSWORD="{{ admin_password }}" ./scripts/initialize.sh # explicitly set a password for the orms user to allow logins on admin-legacy - command: | - docker compose exec admin python manage.py shell -c 'user = User.objects.get(username="orms"); user.set_password("${ORMS_PASSWORD}"); user.save();' + docker compose exec admin python manage.py shell -c 'user = User.objects.get(username="orms"); user.set_password("{{ orms_password }}"); user.save();' when: "{{ use_orms }}" # run the remaining services - command: | diff --git a/ctt.toml b/ctt.toml index 6193fde..2131280 100644 --- a/ctt.toml +++ b/ctt.toml @@ -71,7 +71,6 @@ _extra_tasks = [ use_orms = true _extra_tasks = [ "ls compose.orms.yaml", - "docker compose exec admin python manage.py shell -c 'print(LegacyOAUser.objects.all().values_list(\"username\"))'", "uv run tests/system_user_login.py orms {{ orms_password }}", "docker compose down", ] From 2236ffc9ec6f777b41a215b626e8485643931dbb Mon Sep 17 00:00:00 2001 From: Matthias Schoettle Date: Wed, 28 Jan 2026 20:23:07 -0500 Subject: [PATCH 13/13] add commented out tests --- ctt.toml | 94 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/ctt.toml b/ctt.toml index 2131280..49605ca 100644 --- a/ctt.toml +++ b/ctt.toml @@ -15,56 +15,56 @@ _extra_tasks = [ "docker compose down", ] -# [output.".ctt/defaults"] -# _extra_tasks = [ -# "! ls compose.orms.yaml", -# "uv run tests/admin_login.py '{{ admin_password }}'", -# "uv run tests/system_user_login.py interface-engine '{{ labs_password }}'", -# "uv run tests/labs_basic_auth.py '{{ labs_password }}'", -# "uv run tests/validate_token.py admin_token {{ admin_token }}", -# "uv run tests/validate_token.py listener_token {{ listener_token }}", -# "uv run tests/validate_token.py listener_registration_token {{ listener_registration_token }}", -# "uv run tests/validate_token.py interface_engine_token {{ interface_engine_token }}", -# # need to set legacy DBs to test mode so that refresh_data script can run -# "docker compose run --rm db-management python -m db_management.run_sql_scripts OpalDB db_management/opaldb/data/test/testmode/", -# "docker compose run --rm db-management python -m db_management.run_sql_scripts QuestionnaireDB db_management/questionnairedb/data/test/testmode/", -# "scripts/refresh_data.sh OMI", -# "docker compose down", -# ] +[output.".ctt/defaults"] +_extra_tasks = [ + "! ls compose.orms.yaml", + "uv run tests/admin_login.py '{{ admin_password }}'", + "uv run tests/system_user_login.py interface-engine '{{ labs_password }}'", + "uv run tests/labs_basic_auth.py '{{ labs_password }}'", + "uv run tests/validate_token.py admin_token {{ admin_token }}", + "uv run tests/validate_token.py listener_token {{ listener_token }}", + "uv run tests/validate_token.py listener_registration_token {{ listener_registration_token }}", + "uv run tests/validate_token.py interface_engine_token {{ interface_engine_token }}", + # need to set legacy DBs to test mode so that refresh_data script can run + "docker compose run --rm db-management python -m db_management.run_sql_scripts OpalDB db_management/opaldb/data/test/testmode/", + "docker compose run --rm db-management python -m db_management.run_sql_scripts QuestionnaireDB db_management/questionnairedb/data/test/testmode/", + "scripts/refresh_data.sh OMI", + "docker compose down", +] -# # the tests are excluded -# [output.".ctt/no_test"] -# is_test = false -# _extra_tasks = [ -# "! ls tests", -# "docker compose down", -# ] +# the tests are excluded +[output.".ctt/no_test"] +is_test = false +_extra_tasks = [ + "! ls tests", + "docker compose down", +] -# # DB on same host -# [output.".ctt/db_same_host"] -# db_host_type = "same_server" -# db_port = 3307 -# db_root_user = "root" -# db_root_password = "root-password" -# _extra_tasks = [ -# # need to clean up on existing DB server -# "ENVIRONMENT='{{ environment }}' DB_ROOT_USER='{{ db_root_user }}' DB_ROOT_PASSWORD='{{ db_root_password }}' DB_HOST='{{ db_host }}' DB_PORT='{{ db_port }}' DB_USER='{{ db_user }}' DB_NAME=admin ./scripts/cleanup_db.sh", -# "docker compose down", -# ] +# DB on same host +[output.".ctt/db_same_host"] +db_host_type = "same_server" +db_port = 3307 +db_root_user = "root" +db_root_password = "root-password" +_extra_tasks = [ + # need to clean up on existing DB server + "ENVIRONMENT='{{ environment }}' DB_ROOT_USER='{{ db_root_user }}' DB_ROOT_PASSWORD='{{ db_root_password }}' DB_HOST='{{ db_host }}' DB_PORT='{{ db_port }}' DB_USER='{{ db_user }}' DB_NAME=admin ./scripts/cleanup_db.sh", + "docker compose down", +] -# # DB on a different server requires a db_host -# [output.".ctt/db_different_server"] -# db_host_type = "separate_server" -# # use the service container to pretend it is a separate server -# # very tricky to test in CI with a dedicated hostname -# db_host = "host.docker.internal" -# db_port = 3307 -# db_root_user = "root" -# db_root_password = "root-password" -# # force no TLS during test, requires certificate otherwise -# # TODO: add test case with TLS enabled -# db_use_tls = 0 -# use_custom_certs = false +# DB on a different server requires a db_host +[output.".ctt/db_different_server"] +db_host_type = "separate_server" +# use the service container to pretend it is a separate server +# very tricky to test in CI with a dedicated hostname +db_host = "host.docker.internal" +db_port = 3307 +db_root_user = "root" +db_root_password = "root-password" +# force no TLS during test, requires certificate otherwise +# TODO: add test case with TLS enabled +db_use_tls = 0 +use_custom_certs = false # orms enabled [output.".ctt/orms_enabled"]