From 80656f177cea912079874a2cb7ed19b961548b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Thu, 6 Aug 2026 12:10:55 +0200 Subject: [PATCH 01/55] chore: checkpoint installation before scaffold migration --- .dockerignore | 7 + Dockerfile | 21 +- README.md | 52 +++ container_install.sh | 694 ++++++++-------------------------------- docker-compose.prod.yml | 5 + docker-compose.test.yml | 4 + install.sh | 85 +++-- 7 files changed, 262 insertions(+), 606 deletions(-) diff --git a/.dockerignore b/.dockerignore index 25b5269f..6653d9c0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -29,6 +29,13 @@ htmlcov/ !.env.example .env.prod.file install_settings.txt +# Configuration files under conf are excluded by default. Only templates and +# the explicitly non-sensitive test profiles are safe to COPY into an image. +conf/*settings*.txt +!conf/docker_test_settings.txt +!conf/template_install_settings.txt +!conf/template_settings.txt +.tmp_docker_install_conf_*.txt wetlab/logging_config.ini logs/ tmp/ diff --git a/Dockerfile b/Dockerfile index bdcabed8..06d164de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.4 FROM registry.access.redhat.com/ubi9/ubi ENV TZ=Europe/Madrid RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone @@ -75,13 +76,29 @@ RUN chmod +x /srv/iskylims/scripts/container_start.sh ARG INSTALL_TYPE=dep ARG GIT_REVISION=main ARG INSTALL_CONF=conf/docker_test_settings.txt +ARG USE_INSTALL_CONF_SECRET=false +ARG RENDER_DJANGO_SETTINGS=false # Prepare dependencies and stage the application tree in the image so the # container can restart without rerunning install-time file generation. ENV SKIP_SYSTEM_PACKAGES=1 -RUN /bin/bash install.sh --install dep --git_revision $GIT_REVISION --conf $INSTALL_CONF --skip_apache_restart \ +# Production reads the operator configuration through an ephemeral build-secret +# mount so COPY and image layers never retain it. Test builds use the bundled +# non-sensitive configuration and explicitly render test settings into the image. +RUN --mount=type=secret,id=install_conf \ + conf_path="$INSTALL_CONF"; \ + if [ "$USE_INSTALL_CONF_SECRET" = "true" ]; then \ + conf_path=/run/secrets/install_conf; \ + test -f "$conf_path" || { echo "Required install_conf build secret is missing" >&2; exit 1; }; \ + fi; \ + /bin/bash install.sh --install dep --git_revision "$GIT_REVISION" --conf "$conf_path" --skip_apache_restart \ && rm -rf /root/.cache/pip /tmp/* /var/tmp/* -RUN /bin/bash install.sh --stage install --git_revision $GIT_REVISION --conf $INSTALL_CONF --skip_apache_restart \ +RUN --mount=type=secret,id=install_conf \ + conf_path="$INSTALL_CONF"; \ + if [ "$USE_INSTALL_CONF_SECRET" = "true" ]; then conf_path=/run/secrets/install_conf; fi; \ + render_args=""; \ + if [ "$RENDER_DJANGO_SETTINGS" = "true" ]; then render_args="--render-settings"; fi; \ + /bin/bash install.sh --stage install --git_revision "$GIT_REVISION" --conf "$conf_path" --skip_apache_restart $render_args \ && rm -rf /root/.cache/pip /tmp/* /var/tmp/* # Use the virtualenv created by install.sh ENV PATH="${INSTALL_PATH}/virtualenv/bin:${PATH}" diff --git a/README.md b/README.md index d2fa315c..a8723034 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,18 @@ The standalone iSkyLIMS Apache container renders `APACHE_FORWARDED_PROTO` and `A During production install/upgrade, `container_install.sh` writes `.env.prod.file` in the repository root. This file is ignored by git and is used by Compose for variable interpolation in `docker-compose.prod.yml`. It intentionally contains Compose/runtime metadata, not database or email passwords. +The selected installation file remains the single configuration source. For +image construction, `container_install.sh` mounts it ephemerally with the +selected container engine at +`/run/secrets/install_conf`; it is not copied into an image layer. +`.dockerignore` excludes production and temporary settings files from `COPY`. +Test builds use the committed non-sensitive test profile and explicitly render +`settings.py` into the test image. Production settings are rendered on the host +and bind-mounted when the container starts. +Production configuration filenames must match `*settings*.txt`; this lets +`.dockerignore` exclude custom configurations without excluding dependency files +such as `conf/requirements.txt`. + Host directory and ownership preparation is described in [Persist logs/documents on the host](#persist-logsdocuments-on-the-host). #### Persist logs/documents on the host @@ -533,6 +545,24 @@ docker compose --env-file .env.prod.file -f docker-compose.prod.yml up -d --forc ## Developer notes +### Shared container installer library + +`container_install.sh` sources +`deployment/lib/container/common.sh`, vendored from the BU-ISCIII deployment +standards repository. This shared file owns container-engine selection, Compose +execution, rootless Podman filesystem fallbacks, configuration reads, service +resolution, and running-state diagnostics. + +Do not edit the vendored library in this repository. Keep iSkyLIMS-specific +settings rendering, Apache configuration, bind mounts, Samba/demo data, and +bootstrap behavior in `container_install.sh`. Check or update the shared copy +from the standards repository with: + +```bash +python3 scripts/scaffold.py check-lib /path/to/relecov-iskylims +python3 scripts/scaffold.py sync-lib /path/to/relecov-iskylims +``` + ### Django migrations workflow Migrations are committed to the repo. Do not run `makemigrations` during install/upgrade. @@ -598,6 +628,28 @@ Suggested steps (host Apache as reverse proxy): ### Verification of the installation +Run the automated smoke test after every clean installation, upgrade, restore, +or rollback. For the standalone local test stack: + +```bash +bash scripts/smoke_test.sh --test --engine docker +``` + +Use `--engine podman` for Podman. The script validates the Compose model, +application, database and Samba container state, Django checks, migration state, +the application root, and Swagger. + +For the standalone production Compose deployment: + +```bash +bash scripts/smoke_test.sh --engine podman --env_file .env.prod.file \ + --host iskylims.example.org +``` + +Override `--base_url` if Apache is published on another address. Use +`--skip_http` only for diagnostics; production acceptance requires the complete +HTTP checks. + Open the navigator and type "localhost" or the "server local IP" and check that iSkyLIMs is running. You can also check some of the functionality, while also checking samba and database connections using: diff --git a/container_install.sh b/container_install.sh index bf79e776..d2005dea 100644 --- a/container_install.sh +++ b/container_install.sh @@ -1,7 +1,14 @@ #!/usr/bin/bash +container_install_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$container_install_script_dir/deployment/lib/container/common.sh" +# shellcheck disable=SC1091 +source "$container_install_script_dir/deployment/lib/container/django.sh" + ISKYLIMS_VERSION="3.1.1" +# Print this application's stable outer-installer CLI and examples. usage() { cat << EOF This script installs and upgrades the iskylims app. @@ -11,7 +18,7 @@ Usage : $0 [--demo_data] [--git_revision] [--compose_file] [--install_conf] [--a --demo_data | Provide already downloaded demo data from Zenodo --git_revision | Specify the Git revision to install (default: main, or 'current' to use copied local sources) --compose_file | Compose file to use (overrides default) - --install_conf | Settings file consumed during container image build (mandatory for production) + --install_conf | Single production settings file (host rendering, ephemeral build secret, and bootstrap) --install_conf_map | Service-specific settings file: service,path (can be repeated) --action | install (default), upgrade, or fix-permissions --script | Run a Django migration script after migrations (can be repeated) @@ -97,253 +104,22 @@ migration_script=() migration_script_before=() engine="docker" -ENGINE_CMD=() -COMPOSE_CMD=() - -set_engine() { - if [ "$engine" = "docker" ]; then - if ! command -v docker >/dev/null 2>&1; then - echo "docker not found. Install docker or use --engine podman." - exit 1 - fi - ENGINE_CMD=("docker") - COMPOSE_CMD=("docker" "compose") - else - if ! command -v podman >/dev/null 2>&1; then - echo "podman not found. Install podman or use --engine docker." - exit 1 - fi - ENGINE_CMD=("podman") - if command -v podman-compose >/dev/null 2>&1; then - COMPOSE_CMD=("podman-compose") - elif podman compose version >/dev/null 2>&1; then - COMPOSE_CMD=("podman" "compose") - else - echo "podman compose not available. Install podman-compose or use --engine docker." - exit 1 - fi - fi -} - -engine_exec() { - "${ENGINE_CMD[@]}" "$@" -} - -compose_exec() { - "${COMPOSE_CMD[@]}" "$@" -} - -copy_with_podman_fallback() { - local src="$1" - local dst="$2" - - if cp "$src" "$dst" 2>/dev/null; then - return 0 - fi - - if [ "$engine" = "podman" ]; then - if podman unshare cp "$src" "$dst"; then - return 0 - fi - fi - - echo "Failed to copy '$src' to '$dst'" >&2 - return 1 -} - -chmod_with_podman_fallback() { - local mode="$1" - shift - - if chmod "$mode" "$@" 2>/dev/null; then - return 0 - fi - - if [ "$engine" = "podman" ]; then - if podman unshare chmod "$mode" "$@"; then - return 0 - fi - fi - - echo "Failed to chmod $mode: $*" >&2 - return 1 -} - -chown_with_podman_fallback() { - local owner="$1" - shift - - if chown -R "$owner" "$@" 2>/dev/null; then - return 0 - fi - - if [ "$engine" = "podman" ]; then - if podman unshare chown -R "$owner" "$@"; then - return 0 - fi - fi - - echo "Failed to chown $owner: $*" >&2 - return 1 -} - -normalize_apache_server_name() { - local value="$1" - - value="${value#http://}" - value="${value#https://}" - value="${value%%/*}" - value="${value%%:*}" - - if [ -z "$value" ] || [ "$value" = "*" ]; then - value="localhost" - fi - - echo "$value" -} - -generate_django_secret_key() { - if command -v python3 >/dev/null 2>&1; then - python3 -c "import secrets; print(''.join(secrets.choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for _ in range(50)))" - else - LC_ALL=C tr -dc 'A-Za-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c 50 - printf "\n" - fi -} - -sed_replacement_escape() { - printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g' -} - -render_django_settings_file() { - local settings_path="$1" - local secret_line="" - local tmp_file="" - local db_user db_pass db_name db_host db_port - local email_host email_port email_user email_pass email_tls - local local_server_ip dns_url - - if [ -f "$settings_path" ]; then - secret_line="$(grep -E "^SECRET_KEY[[:space:]]*=" "$settings_path" | tail -n 1)" - fi - if [ -z "$secret_line" ] || [[ "$secret_line" =~ SECRET_KEY[[:space:]]*=[[:space:]]*SECRET ]]; then - secret_line="SECRET_KEY = '$(generate_django_secret_key)'" - fi - - db_user="$(read_install_conf_value DB_USER "$host_install_conf_path")" - db_pass="$(read_install_conf_value DB_PASS "$host_install_conf_path")" - db_name="$(read_install_conf_value DB_NAME "$host_install_conf_path")" - db_host="$(read_install_conf_value DB_SERVER_IP "$host_install_conf_path")" - db_port="$(read_install_conf_value DB_PORT "$host_install_conf_path")" - email_host="$(read_install_conf_value EMAIL_HOST_SERVER "$host_install_conf_path")" - email_port="$(read_install_conf_value EMAIL_PORT "$host_install_conf_path")" - email_user="$(read_install_conf_value EMAIL_HOST_USER "$host_install_conf_path")" - email_pass="$(read_install_conf_value EMAIL_HOST_PASSWORD "$host_install_conf_path")" - email_tls="$(read_install_conf_value EMAIL_USE_TLS "$host_install_conf_path")" - local_server_ip="$(read_install_conf_value LOCAL_SERVER_IP "$host_install_conf_path")" - dns_url="$(read_install_conf_value DNS_URL "$host_install_conf_path")" - - tmp_file="$(mktemp)" - cp "$repo_root/conf/template_settings.txt" "$tmp_file" - sed -i \ - -e "s|^SECRET_KEY.*|$(sed_replacement_escape "$secret_line")|" \ - -e "s|djangouser|$(sed_replacement_escape "$db_user")|g" \ - -e "s|djangopass|$(sed_replacement_escape "$db_pass")|g" \ - -e "s|djangohost|$(sed_replacement_escape "$db_host")|g" \ - -e "s|djangoport|$(sed_replacement_escape "$db_port")|g" \ - -e "s|djangodbname|$(sed_replacement_escape "$db_name")|g" \ - -e "s|emailhostserver|$(sed_replacement_escape "$email_host")|g" \ - -e "s|emailport|$(sed_replacement_escape "$email_port")|g" \ - -e "s|emailhostuser|$(sed_replacement_escape "$email_user")|g" \ - -e "s|emailhostpassword|$(sed_replacement_escape "$email_pass")|g" \ - -e "s|emailhosttls|$(sed_replacement_escape "$email_tls")|g" \ - -e "s|localserverip|$(sed_replacement_escape "$local_server_ip")|g" \ - -e "s|localhost|$(sed_replacement_escape "$dns_url")|g" \ - "$tmp_file" - - if copy_with_podman_fallback "$tmp_file" "$settings_path"; then - if ! chmod_with_podman_fallback 0664 "$settings_path"; then - rm -f "$tmp_file" - return 1 - fi - rm -f "$tmp_file" - return 0 - fi - - rm -f "$tmp_file" - return 1 -} - -normalize_settings_bind_path() { - local value="$1" - - if [ -z "$value" ]; then - echo "$install_path/iskylims/settings.py" - return 0 - fi - - if [ -d "$value" ] || [[ "$value" = */ ]] || [[ "$value" != *.py ]]; then - echo "${value%/}/settings.py" - return 0 - fi - - echo "$value" -} - +# Supply iSkyLIMS Apache tokens. Token rendering and host-file installation are +# shared, but the set of virtual-host values is application-specific. render_apache_config() { local src="$1" local dst="$2" - local tmp_file="" - - tmp_file="$(mktemp)" - sed \ - -e "s|__ISKYLIMS_SERVER_NAME__|$apache_server_name|g" \ - -e "s|__ISKYLIMS_LOG_NAME__|$apache_log_name|g" \ - -e "s|__INSTALL_PATH__|$install_path|g" \ - -e "s|__APP_PORT__|$app_port|g" \ - -e "s|__ISKYLIMS_FORWARDED_PROTO__|$(sed_replacement_escape "$apache_forwarded_proto")|g" \ - -e "s|__ISKYLIMS_FORWARDED_PORT__|$(sed_replacement_escape "$apache_forwarded_port")|g" \ - -e "s|__GUNICORN_TIMEOUT__|$gunicorn_timeout|g" \ - -e "s|__SERVER_STATUS_SERVER_NAME__|$(sed_replacement_escape "$apache_status_server_name")|g" \ - -e "s|__SERVER_STATUS_ALIASES__|$(sed_replacement_escape "$apache_status_aliases")|g" \ - -e "s|__SERVER_STATUS_ALLOW_FROM__|$(sed_replacement_escape "$apache_status_allow_from")|g" \ - "$src" > "$tmp_file" - - if copy_with_podman_fallback "$tmp_file" "$dst"; then - if ! chmod_with_podman_fallback 0664 "$dst"; then - rm -f "$tmp_file" - return 1 - fi - rm -f "$tmp_file" - return 0 - fi - - rm -f "$tmp_file" - return 1 -} - -prepare_django_settings_bind_mount() { - local settings_path="$1" - local configured_db_host="" - - if [ "$mode" != "production" ]; then - return 0 - fi - - if [ -d "$settings_path" ]; then - echo "DJANGO_SETTINGS_PATH must resolve to a file path, but '$settings_path' is a directory." >&2 - echo "Use a full path like '$settings_path/settings.py' or remove the directory and rerun." >&2 - return 1 - fi - - mkdir -p "$(dirname "$settings_path")" - configured_db_host="$(read_install_conf_value DB_SERVER_IP "$host_install_conf_path")" - if [ ! -f "$settings_path" ] \ - || grep -Eq "SECRET_KEY[[:space:]]*=[[:space:]]*SECRET|emailhosttls|djangouser|djangopass|djangohost|djangodbname" "$settings_path" \ - || ! grep -Fq -- "\"HOST\": \"$configured_db_host\"," "$settings_path"; then - render_django_settings_file "$settings_path" - fi - chmod_with_podman_fallback 0664 "$settings_path" + render_config_template "$src" "$dst" 0664 \ + __ISKYLIMS_SERVER_NAME__ "$apache_server_name" \ + __ISKYLIMS_LOG_NAME__ "$apache_log_name" \ + __INSTALL_PATH__ "$install_path" \ + __APP_PORT__ "$app_port" \ + __ISKYLIMS_FORWARDED_PROTO__ "$apache_forwarded_proto" \ + __ISKYLIMS_FORWARDED_PORT__ "$apache_forwarded_port" \ + __GUNICORN_TIMEOUT__ "$gunicorn_timeout" \ + __SERVER_STATUS_SERVER_NAME__ "$apache_status_server_name" \ + __SERVER_STATUS_ALIASES__ "$apache_status_aliases" \ + __SERVER_STATUS_ALLOW_FROM__ "$apache_status_allow_from" } # PARSE VARIABLE ARGUMENTS WITH getopts @@ -482,79 +258,24 @@ if [ "$action" = "upgrade" ]; then skip_test_data=true fi -if [ ! -f "$compose_file" ]; then - echo "Compose file '$compose_file' not found" - exit 1 -fi - -if [ ! -f "$install_conf" ]; then - echo "Install configuration '$install_conf' not found" - exit 1 -fi +require_compose_file "$compose_file" || exit 1 repo_root="$(pwd)" build_context_dir="$repo_root" -if [ ! -d "$build_context_dir" ]; then - echo "Build context directory '$build_context_dir' not found" - exit 1 -fi - temp_install_conf="" -if [[ "$install_conf" = /* ]] && [[ "$install_conf" != "$build_context_dir/"* ]]; then - temp_install_conf="$build_context_dir/.tmp_docker_install_conf_app_$$.txt" - echo "Copying $install_conf into temporary file $temp_install_conf for Docker build/runtime." - cp "$install_conf" "$temp_install_conf" - install_conf="$temp_install_conf" - cleanup_temp_conf() { - if [ -n "$temp_install_conf" ] && [ -f "$temp_install_conf" ]; then - rm -f "$temp_install_conf" - fi - } +host_install_conf_path="" +prepare_install_configuration \ + "$install_conf" "$repo_root" "$build_context_dir" app "$mode" \ + host_install_conf_path install_conf_container temp_install_conf || exit 1 +install_conf="$host_install_conf_path" +if [ -n "$temp_install_conf" ]; then + # EXIT traps need a no-argument callback; deletion itself is shared. + cleanup_temp_conf() { cleanup_files "$temp_install_conf"; } trap cleanup_temp_conf EXIT fi -if [[ "$install_conf" = "$build_context_dir/"* ]]; then - install_conf_container="${install_conf#$build_context_dir/}" -else - install_conf_container="$install_conf" -fi - -host_install_conf_path="$install_conf" -if [[ "$host_install_conf_path" != /* ]]; then - host_install_conf_path="$repo_root/$host_install_conf_path" -fi - -read_install_conf_value() { - local key="$1" - local file="$2" - - bash -c ' - set -a - . "$1" - key="$2" - printf "%s" "${!key-}" - ' _ "$file" "$key" -} - -config_value_or_default() { - local key="$1" - local default_value="$2" - local config_value="" - local env_value="${!key:-}" - - if [ -n "$env_value" ]; then - echo "$env_value" - return 0 - fi - - config_value="$(read_install_conf_value "$key" "$host_install_conf_path")" - if [ -n "$config_value" ]; then - echo "$config_value" - else - echo "$default_value" - fi -} - +# Write this stack's complete Compose interpolation contract. The keys remain +# application-owned because they must match its production Compose file. write_compose_env_file() { if [ "$mode" != "production" ]; then return 0 @@ -566,6 +287,9 @@ write_compose_env_file() { INSTALL_TYPE=dep GIT_REVISION=$git_revision INSTALL_CONF=$install_conf_container +# Compose reads this host path to create an ephemeral build-secret mount. The +# value is used by Compose itself and is not stored in the resulting image. +INSTALL_CONF_HOST=$host_install_conf_path INSTALL_PATH=$install_path APACHE_CONF_PATH=$apache_conf_path DJANGO_SETTINGS_PATH=$django_settings_path @@ -589,14 +313,6 @@ EOF echo "Wrote Compose environment file: $compose_env_file" } -compose_with_env_exec() { - if [ "$mode" = "production" ] && [ -f "$compose_env_file" ]; then - compose_exec --env-file "$compose_env_file" "$@" - else - compose_exec "$@" - fi -} - set_engine app_repo_path="${APP_REPO_PATH:-/srv/iskylims}" @@ -608,52 +324,34 @@ if [ -z "$apache_conf_path" ]; then apache_conf_path="$install_path/conf" fi config_django_settings_path="$(read_install_conf_value "DJANGO_SETTINGS_PATH" "$host_install_conf_path")" -django_settings_path="$(normalize_settings_bind_path "${DJANGO_SETTINGS_PATH:-${config_django_settings_path:-}}")" -app_uid="$(config_value_or_default APP_UID 1212)" -app_gid="$(config_value_or_default APP_GID 1212)" -app_shell="$(config_value_or_default APP_SHELL /sbin/nologin)" -app_port="$(config_value_or_default APP_PORT 8001)" -django_debug="$(config_value_or_default DJANGO_DEBUG false)" -db_conn_max_age="$(config_value_or_default DB_CONN_MAX_AGE 60)" -web_concurrency="$(config_value_or_default WEB_CONCURRENCY 2)" -gunicorn_threads="$(config_value_or_default GUNICORN_THREADS 2)" -gunicorn_timeout="$(config_value_or_default GUNICORN_TIMEOUT 300)" -gunicorn_keepalive="$(config_value_or_default GUNICORN_KEEPALIVE 5)" +django_settings_path="$(normalize_bind_file_path "${DJANGO_SETTINGS_PATH:-${config_django_settings_path:-}}" "$install_path/iskylims/settings.py" settings.py)" +app_uid="$(config_value_or_default APP_UID "$host_install_conf_path" 1212)" +app_gid="$(config_value_or_default APP_GID "$host_install_conf_path" 1212)" +app_shell="$(config_value_or_default APP_SHELL "$host_install_conf_path" /sbin/nologin)" +app_port="$(config_value_or_default APP_PORT "$host_install_conf_path" 8001)" +django_debug="$(config_value_or_default DJANGO_DEBUG "$host_install_conf_path" false)" +db_conn_max_age="$(config_value_or_default DB_CONN_MAX_AGE "$host_install_conf_path" 60)" +web_concurrency="$(config_value_or_default WEB_CONCURRENCY "$host_install_conf_path" 2)" +gunicorn_threads="$(config_value_or_default GUNICORN_THREADS "$host_install_conf_path" 2)" +gunicorn_timeout="$(config_value_or_default GUNICORN_TIMEOUT "$host_install_conf_path" 300)" +gunicorn_keepalive="$(config_value_or_default GUNICORN_KEEPALIVE "$host_install_conf_path" 5)" config_dns_url="$(read_install_conf_value "DNS_URL" "$host_install_conf_path")" apache_server_name="$(normalize_apache_server_name "${APACHE_SERVER_NAME:-${config_dns_url:-localhost}}")" apache_log_name="$(printf '%s' "$apache_server_name" | tr -c 'A-Za-z0-9._-' '_' | sed 's/_$//')" -apache_status_server_name="$(normalize_apache_server_name "$(config_value_or_default SERVER_STATUS_SERVER_NAME "$apache_server_name")")" -apache_status_aliases="$(config_value_or_default SERVER_STATUS_ALIASES "127.0.0.1 localhost")" -apache_status_allow_from="$(config_value_or_default SERVER_STATUS_ALLOW_FROM "127.0.0.1 localhost")" -apache_forwarded_proto="$(config_value_or_default APACHE_FORWARDED_PROTO https)" -apache_forwarded_port="$(config_value_or_default APACHE_FORWARDED_PORT 443)" +apache_status_server_name="$(normalize_apache_server_name "$(config_value_or_default SERVER_STATUS_SERVER_NAME "$host_install_conf_path" "$apache_server_name")")" +apache_status_aliases="$(config_value_or_default SERVER_STATUS_ALIASES "$host_install_conf_path" "127.0.0.1 localhost")" +apache_status_allow_from="$(config_value_or_default SERVER_STATUS_ALLOW_FROM "$host_install_conf_path" "127.0.0.1 localhost")" +apache_forwarded_proto="$(config_value_or_default APACHE_FORWARDED_PROTO "$host_install_conf_path" https)" +apache_forwarded_port="$(config_value_or_default APACHE_FORWARDED_PORT "$host_install_conf_path" 443)" compose_env_file="$repo_root/.env.prod.file" app_container="" -local_head_hash="" -local_head_short="" -app_image_name="${APP_IMAGE_NAME:-iskylims_app}" -image_id_before_build="" -image_id_after_build="" - -# Check if a service exists in the compose file -# -# Parameters: -# $1 - Service name to check -# -# Returns: -# 0 if the service exists, 1 otherwise -service_exists() { - compose_with_env_exec -f "$compose_file" ps --services 2>/dev/null | grep -Fxq "$1" -} +if [ "$mode" = "production" ]; then + app_image_name="${APP_IMAGE_NAME:-relecov-iskylims:local}" +else + app_image_name="${APP_IMAGE_NAME:-iskylims_app}" +fi -# Return the name of the container for a given service name. -# The container name is different based on whether we are in test mode or not. -# -# Parameters: -# $1 - Service name to return the container name for -# -# Returns: -# The name of the container for the given service name +# Map services to this stack's explicit legacy container names. service_container_name() { local service_name="$1" if [ "$mode" = "test" ]; then @@ -672,134 +370,7 @@ service_container_name() { fi } -# Resolve the container ID for the target app service. -# -# Returns: -# Sets global variable `app_container` to a valid container name/ID. -# -# Errors: -# Exits if unable to resolve a container for `app_service`. -resolve_app_container() { - local container_name - container_name="$(service_container_name "$app_service")" - - if [ -n "$container_name" ] && engine_exec inspect -f '{{.Id}}' "$container_name" >/dev/null 2>&1; then - app_container="$container_name" - else - app_container="$(engine_exec ps -a --filter "label=com.docker.compose.service=${app_service}" --format '{{.ID}}' | head -n 1)" - fi - - if [ -z "$app_container" ]; then - echo "Error: unable to resolve container ID for service '$app_service'." >&2 - exit 1 - fi -} - -try_resolve_app_container() { - local container_name - container_name="$(service_container_name "$app_service")" - - if [ -n "$container_name" ] && engine_exec inspect -f '{{.Id}}' "$container_name" >/dev/null 2>&1; then - app_container="$container_name" - return 0 - fi - - app_container="$(engine_exec ps -a --filter "label=com.docker.compose.service=${app_service}" --format '{{.ID}}' | head -n 1)" - [ -n "$app_container" ] -} - -# Ensure target app service container exists and is running. -# -# Errors: -# Exits if container does not exist or is not running. -ensure_app_running() { - resolve_app_container - if ! engine_exec inspect -f '{{.State.Running}}' "$app_container" >/dev/null 2>&1; then - echo "Error: service '$app_service' container does not exist." - exit 1 - fi - if [ "$(engine_exec inspect -f '{{.State.Running}}' "$app_container")" != "true" ]; then - echo "Error: service '$app_service' container is not running. Showing logs:" - engine_exec logs --tail 200 "$app_container" - exit 1 - fi -} - -print_local_source_diagnostics() { - echo "Local source diagnostics:" - if command -v git >/dev/null 2>&1 && git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - local_head_hash="$(git -C "$repo_root" rev-parse HEAD)" - local_head_short="$(git -C "$repo_root" rev-parse --short HEAD)" - echo " local HEAD: $(git -C "$repo_root" log -1 --oneline)" - echo " local HEAD hash: $local_head_hash" - else - echo " local git metadata unavailable" - fi -} - -print_existing_artifact_diagnostics() { - echo "Image diagnostics before build:" - if engine_exec image inspect "$app_image_name" >/dev/null 2>&1; then - image_id_before_build="$(engine_exec image inspect -f '{{.Id}}' "$app_image_name" 2>/dev/null || true)" - echo " image before build: $image_id_before_build" - else - image_id_before_build="" - echo " image before build: none" - fi -} - -print_image_after_build() { - echo "Image diagnostics after build:" - if engine_exec image inspect "$app_image_name" >/dev/null 2>&1; then - image_id_after_build="$(engine_exec image inspect -f '{{.Id}}' "$app_image_name" 2>/dev/null || true)" - echo " image after build: $image_id_after_build" - if [ -n "$image_id_before_build" ] && [ "$image_id_before_build" = "$image_id_after_build" ]; then - echo " image id check: unchanged" - elif [ -n "$image_id_before_build" ] && [ "$image_id_before_build" != "$image_id_after_build" ]; then - echo " image id check: changed" - else - echo " image id check: created" - fi - else - echo " image after build: not found" - fi -} - -print_container_source_diagnostics() { - local label="$1" - local container_repo_head_hash="" - local container_repo_head_short="" - echo "$label" - container_repo_head_hash="$(engine_exec exec "$app_container" sh -lc " - if [ -d '$app_repo_path/.git' ]; then - cd '$app_repo_path' && git rev-parse HEAD - fi - " 2>/dev/null | tail -n 1)" - container_repo_head_short="$(engine_exec exec "$app_container" sh -lc " - if [ -d '$app_repo_path/.git' ]; then - cd '$app_repo_path' && git rev-parse --short HEAD - fi - " 2>/dev/null | tail -n 1)" - if [ -n "$container_repo_head_hash" ]; then - echo " container /srv HEAD hash: $container_repo_head_hash" - fi - if [ -n "$local_head_hash" ] && [ -n "$container_repo_head_hash" ]; then - if [ "$local_head_hash" = "$container_repo_head_hash" ]; then - echo " HEAD check: OK local=$local_head_short container=$container_repo_head_short" - else - echo " HEAD check: MISMATCH local=$local_head_short container=$container_repo_head_short" - fi - fi - engine_exec exec "$app_container" sh -lc " - echo ' /srv/iskylims HEAD:' - if [ -d '$app_repo_path/.git' ]; then - cd '$app_repo_path' && git log -1 --oneline - else - echo 'not a git checkout' - fi - " || true -} - +# Repair iSkyLIMS-specific writable directories inside its mounted app volume. prepare_app_mount_permissions() { if [ "$mode" != "production" ]; then return 0 @@ -812,87 +383,55 @@ prepare_app_mount_permissions() { chown -R '$app_uid:$app_gid' '$install_path/logs' '$install_path/static' '$install_path/documents' '$install_path/cron' '$install_path/tmp' chmod -R u+rwX,g+rwX '$install_path/logs' '$install_path/static' '$install_path/documents' chmod 700 '$install_path/cron' '$install_path/tmp' - if [ -f '$install_path/iskylims/settings.py' ]; then - chown '$app_uid:$app_gid' '$install_path/iskylims/settings.py' - chmod 0664 '$install_path/iskylims/settings.py' - fi chmod -R o+rX '$install_path/static' " + prepare_django_container_settings_permissions \ + "$app_container" "$install_path/iskylims/settings.py" "$app_uid" "$app_gid" } +# Apply ownership required by iSkyLIMS and the UBI Apache runtime on host binds. prepare_host_bind_mount_permissions() { if [ "$mode" != "production" ]; then return 0 fi - local apache_conf_file local django_settings_dir + local -a host_permission_spec echo "Preparing host bind mount permissions..." - chmod_with_podman_fallback 0755 "$apache_conf_path" - - chown_with_podman_fallback "$app_uid:$app_gid" "/var/log/local/relecov-iskylims/apps" - chmod_with_podman_fallback 0775 "/var/log/local/relecov-iskylims/apps" - - # UBI httpd runs as uid 1001 and group 0. This keeps the Apache log bind - # writable without relying on Podman's :U ownership mutation. - chown_with_podman_fallback "1001:0" "/var/log/local/relecov-iskylims/apache" - chmod_with_podman_fallback 0775 "/var/log/local/relecov-iskylims/apache" + host_permission_spec=( + "$apache_conf_path|-|0755" + "/var/log/local/relecov-iskylims/apps|$app_uid:$app_gid|0775" + # UBI httpd runs as uid 1001 and group 0. This keeps the Apache log bind + # writable without relying on Podman's :U ownership mutation. + "/var/log/local/relecov-iskylims/apache|1001:0|0775" + "$apache_conf_path/iskylims_apache_reverse_proxy.conf|-|0664" + "$apache_conf_path/iskylims_apache_logs.conf|-|0664" + "$apache_conf_path/iskylims_apache_server-status.conf|-|0664" + ) if [ -f "$django_settings_path" ]; then django_settings_dir="$(dirname "$django_settings_path")" - chmod_with_podman_fallback 0755 "$django_settings_dir" - chown_with_podman_fallback "$app_uid:$app_gid" "$django_settings_path" - chmod_with_podman_fallback 0664 "$django_settings_path" + host_permission_spec+=( + "$django_settings_dir|-|0755" + "$django_settings_path|$app_uid:$app_gid|0664" + ) fi - for apache_conf_file in \ - "$apache_conf_path/iskylims_apache_reverse_proxy.conf" \ - "$apache_conf_path/iskylims_apache_logs.conf" \ - "$apache_conf_path/iskylims_apache_server-status.conf"; do - if [ -f "$apache_conf_file" ]; then - chmod_with_podman_fallback 0664 "$apache_conf_file" - fi - done + apply_host_permission_spec "${host_permission_spec[@]}" } -# Remove stale test containers left over from previous runs. -# -# This function will only be executed in "test" mode when the engine is "podman". -# It only removes known test container names and never removes volumes. -cleanup_stale_test_containers() { - if [ "$mode" != "test" ] || [ "$engine" != "podman" ]; then - return 0 - fi - - local svc cname cstate - for svc in db app samba; do - cname="$(service_container_name "$svc")" - if [ -z "$cname" ]; then - continue - fi - if engine_exec inspect -f '{{.Id}}' "$cname" >/dev/null 2>&1; then - cstate="$(engine_exec inspect -f '{{.State.Status}}' "$cname" 2>/dev/null || true)" - if [ "$cstate" != "running" ]; then - echo "Removing stale test container '$cname' (state: ${cstate:-unknown})" - engine_exec rm -f "$cname" >/dev/null 2>&1 || true - fi - fi - done -} - -cleanup_stale_test_containers - if [ "$action" = "fix-permissions" ]; then echo "Repairing production container bind mount and volume permissions..." if ! mkdir -p "$apache_conf_path" "/var/log/local/relecov-iskylims/apache" "/var/log/local/relecov-iskylims/apps"; then echo "Error: unable to create required host bind/log directories. Check APACHE_CONF_PATH and log directory permissions." >&2 exit 1 fi - prepare_django_settings_bind_mount "$django_settings_path" + prepare_django_settings_bind_mount "$repo_root/conf/template_settings.txt" "$django_settings_path" "$host_install_conf_path" prepare_host_bind_mount_permissions write_compose_env_file - if try_resolve_app_container && [ "$(engine_exec inspect -f '{{.State.Running}}' "$app_container" 2>/dev/null)" = "true" ]; then + if app_container="$(resolve_service_container "$app_service" 2>/dev/null)" \ + && [ "$(engine_exec inspect -f '{{.State.Running}}' "$app_container" 2>/dev/null)" = "true" ]; then prepare_app_mount_permissions echo "Done repairing host bind mounts and mounted app volumes." else @@ -903,14 +442,17 @@ if [ "$action" = "fix-permissions" ]; then exit 0 fi -print_local_source_diagnostics -print_existing_artifact_diagnostics +expected_head_hash="$(repository_revision "$repo_root" full)" || true +expected_head_short="$(repository_revision "$repo_root" short)" || true +image_id_before_build="$(compose_service_image_id "$compose_file" "$app_service")" +print_prebuild_diagnostics \ + "Pre-build diagnostics" "$repo_root" "$git_revision" "$image_id_before_build" echo "Deploying containers (compose file: $compose_file) with a pre-staged app image and GIT_REVISION=$git_revision..." if ! mkdir -p "$apache_conf_path" "/var/log/local/relecov-iskylims/apache" "/var/log/local/relecov-iskylims/apps"; then echo "Error: unable to create required host bind/log directories. Check APACHE_CONF_PATH and log directory permissions." >&2 exit 1 fi -prepare_django_settings_bind_mount "$django_settings_path" +prepare_django_settings_bind_mount "$repo_root/conf/template_settings.txt" "$django_settings_path" "$host_install_conf_path" if [ -f "$repo_root/conf/iskylims_apache_reverse_proxy.conf" ]; then render_apache_config \ "$repo_root/conf/iskylims_apache_reverse_proxy.conf" \ @@ -928,22 +470,39 @@ if [ -f "$repo_root/conf/iskylims_apache_server-status.conf" ]; then fi prepare_host_bind_mount_permissions write_compose_env_file -INSTALL_TYPE="dep" GIT_REVISION="$git_revision" INSTALL_CONF="$install_conf_container" INSTALL_PATH="$install_path" APACHE_CONF_PATH="$apache_conf_path" DJANGO_SETTINGS_PATH="$django_settings_path" APP_UID="$app_uid" APP_GID="$app_gid" APP_SHELL="$app_shell" APP_PORT="$app_port" DJANGO_DEBUG="$django_debug" DB_CONN_MAX_AGE="$db_conn_max_age" WEB_CONCURRENCY="$web_concurrency" GUNICORN_THREADS="$gunicorn_threads" GUNICORN_TIMEOUT="$gunicorn_timeout" GUNICORN_KEEPALIVE="$gunicorn_keepalive" \ - compose_with_env_exec -f "$compose_file" build --no-cache \ - --build-arg INSTALL_TYPE="dep" \ - --build-arg GIT_REVISION="$git_revision" \ - --build-arg INSTALL_CONF="$install_conf_container" \ - --build-arg INSTALL_PATH="$install_path" \ - --build-arg APP_UID="$app_uid" \ - --build-arg APP_GID="$app_gid" \ - --build-arg APP_SHELL="$app_shell" -print_image_after_build +validate_compose_configuration "$compose_file" || exit 1 +if [ "$mode" = "production" ]; then + # Use the engine directly because older Compose schemas cannot express + # build secrets. The explicit tag matches docker-compose.prod.yml. + engine_build --no-cache \ + --secret "id=install_conf,src=$host_install_conf_path" \ + --build-arg INSTALL_TYPE="dep" \ + --build-arg GIT_REVISION="$git_revision" \ + --build-arg INSTALL_CONF="$install_conf_container" \ + --build-arg USE_INSTALL_CONF_SECRET="true" \ + --build-arg RENDER_DJANGO_SETTINGS="false" \ + --build-arg INSTALL_PATH="$install_path" \ + --build-arg APP_UID="$app_uid" \ + --build-arg APP_GID="$app_gid" \ + --build-arg APP_SHELL="$app_shell" \ + --tag "$app_image_name" \ + "$build_context_dir" +else + INSTALL_TYPE="dep" GIT_REVISION="$git_revision" INSTALL_CONF="$install_conf_container" INSTALL_PATH="$install_path" \ + compose_with_env_exec -f "$compose_file" build --no-cache +fi +image_id_after_build="$(compose_service_image_id "$compose_file" "$app_service")" +print_image_after_diagnostics \ + "Image diagnostics after build:" "$image_id_before_build" "$image_id_after_build" INSTALL_PATH="$install_path" APACHE_CONF_PATH="$apache_conf_path" DJANGO_SETTINGS_PATH="$django_settings_path" APP_UID="$app_uid" APP_GID="$app_gid" APP_SHELL="$app_shell" APP_PORT="$app_port" DJANGO_DEBUG="$django_debug" DB_CONN_MAX_AGE="$db_conn_max_age" WEB_CONCURRENCY="$web_concurrency" GUNICORN_THREADS="$gunicorn_threads" GUNICORN_TIMEOUT="$gunicorn_timeout" GUNICORN_KEEPALIVE="$gunicorn_keepalive" compose_with_env_exec -f "$compose_file" up -d echo "Waiting 20 seconds for starting database and web services..." sleep 20 -ensure_app_running -print_container_source_diagnostics "Container diagnostics after startup:" +app_container="$(resolve_service_container "$app_service")" || exit 1 +ensure_service_running "$app_service" "$app_container" >/dev/null +print_container_repository_diagnostics \ + "Container diagnostics after startup:" "$app_container" "$app_repo_path" \ + "$expected_head_hash" "$expected_head_short" prepare_app_mount_permissions container_install_conf_path="$install_conf_container" @@ -951,10 +510,10 @@ if [[ "$container_install_conf_path" != /* ]]; then container_install_conf_path="$app_repo_path/$container_install_conf_path" fi -if ! engine_exec exec -it "$app_container" test -f "$container_install_conf_path"; then - echo "Copying install configuration into container at $container_install_conf_path" - engine_exec cp "$host_install_conf_path" "${app_container}:$container_install_conf_path" -fi +echo "Staging protected runtime configuration at $container_install_conf_path" +stage_container_runtime_config \ + "$app_container" "$host_install_conf_path" "$container_install_conf_path" \ + "$app_uid" "$app_gid" || exit 1 script_args_before="" if [ "$run_script_before" = true ]; then @@ -978,7 +537,16 @@ else engine_exec exec -it "$app_container" bash -c "cd $app_repo_path && bash install.sh --bootstrap install --git_revision \"$git_revision\" --conf \"$install_conf_container\" --skip_apache_restart$script_args_before$script_args_after" fi -print_container_source_diagnostics "Container diagnostics after bootstrap:" +# Bootstrap consumes a temporary runtime copy. Remove that exact file so the +# operator configuration remains only on the protected host after deployment. +if [ "$mode" = "production" ]; then + remove_container_runtime_config \ + "$app_container" "$container_install_conf_path" || exit 1 +fi + +print_container_repository_diagnostics \ + "Container diagnostics after bootstrap:" "$app_container" "$app_repo_path" \ + "$expected_head_hash" "$expected_head_short" if ! engine_exec exec -it "$app_container" test -f "$install_path/manage.py"; then echo "Error: $install_path/manage.py not found after bootstrap. Showing logs:" @@ -1031,6 +599,10 @@ fi echo "Skipping crontab add/start (cron is managed by the container entrypoint)" +run_standard_smoke_test \ + "$repo_root/scripts/smoke_test.sh" "$mode" "$engine" "$compose_file" \ + "${compose_env_file:-}" || exit 1 + dns_url="" local_ip="" if [ -f "$host_install_conf_path" ]; then diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 42575950..ad987033 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -22,6 +22,7 @@ services: - iskylims_net app: + image: ${APP_IMAGE_NAME:-relecov-iskylims:local} container_name: iskylims_app restart: unless-stopped build: @@ -30,6 +31,10 @@ services: INSTALL_TYPE: ${INSTALL_TYPE:-dep} GIT_REVISION: ${GIT_REVISION:-main} INSTALL_CONF: ${INSTALL_CONF:-conf/docker_production_settings.txt} + # Production settings are read from an ephemeral build-secret mount and + # rendered on the host, never retained in the image. + USE_INSTALL_CONF_SECRET: "true" + RENDER_DJANGO_SETTINGS: "false" APP_UID: ${APP_UID:-1212} APP_GID: ${APP_GID:-1212} APP_SHELL: ${APP_SHELL:-/sbin/nologin} diff --git a/docker-compose.test.yml b/docker-compose.test.yml index bf49e7c6..1be85a02 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -44,6 +44,10 @@ services: INSTALL_TYPE: ${INSTALL_TYPE:-dep} GIT_REVISION: ${GIT_REVISION:-main} INSTALL_CONF: ${INSTALL_CONF:-conf/docker_test_settings.txt} + # Test settings are intentionally non-secret and may be included in + # the disposable test image. Production uses an ephemeral secret. + RENDER_DJANGO_SETTINGS: "true" + USE_INSTALL_CONF_SECRET: "false" APP_SHELL: ${APP_SHELL:-/bin/bash} container_name: iskylims_app ports: diff --git a/install.sh b/install.sh index cf802e44..e19ed689 100644 --- a/install.sh +++ b/install.sh @@ -1,5 +1,11 @@ #!/bin/bash +install_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$install_script_dir/deployment/lib/container/common.sh" +# shellcheck disable=SC1091 +source "$install_script_dir/deployment/lib/container/django.sh" + APP_VERSION="3.1.1" # usage: prints the command line help and usage examples. @@ -13,6 +19,8 @@ usage : $0 --upgrade --git_revision --conf --upgrade | Upgrade iskylims full/dep/app --stage | Stage app files only (install/upgrade) without DB work. Internal/container use. --bootstrap | Run DB/bootstrap steps only (install/upgrade) against an existing staged app. Internal/container use. + --render-settings | Render settings.py while staging (enabled automatically for non-container installs). + --settings-output | Override the rendered settings.py destination. --git_revision | Git revision name to run (branch, tag, commit SHA, or 'current' to use copied local sources as-is) --conf | Select custom configuration file. Default: ./install_settings.txt --tables | Load the first inital tables (from conf folder) @@ -37,8 +45,8 @@ Examples: Upgrade running migration script and update initial tables $0 --upgrade full --script --tables - Stage application files during a container image build - $0 --stage install --git_revision main --conf conf/docker_production_settings.txt + Stage and render application files for a non-sensitive test image + $0 --stage install --git_revision main --conf conf/docker_test_settings.txt --render-settings Bootstrap database/static using an already staged container image $0 --bootstrap upgrade --git_revision main --conf conf/docker_production_settings.txt @@ -151,50 +159,21 @@ root_check(){ fi } -generate_django_secret_key(){ - "$PYTHON_BIN_PATH" -c "import secrets; print(''.join(secrets.choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for _ in range(50)))" -} - -sed_replacement_escape(){ - printf '%s' "$1" | sed -e 's/[\&|]/\\&/g' -} - -# update_settings_and_urls: rewrite Django settings and urls with deployment values. +# update_settings_and_urls: install URL configuration and optionally render settings. +# Image staging skips production rendering unless --render-settings is explicit; +# production container_install.sh renders the persistent host bind source. update_settings_and_urls(){ log "INFO" "Updating settings.py and urls.py with deployment values" local project_dir="$INSTALL_PATH/$PROJECT_NAME" - local secret_line="" - local tmp_settings="" - - if [ -f "$project_dir/settings.py" ]; then - secret_line="$(grep -E "^SECRET_KEY[[:space:]]*=" "$project_dir/settings.py" | tail -n 1)" - fi - if [ -z "$secret_line" ] || [[ "$secret_line" =~ SECRET_KEY[[:space:]]*=[[:space:]]*SECRET ]]; then - secret_line="SECRET_KEY = '$(generate_django_secret_key)'" - fi - - tmp_settings="$(mktemp)" - cp conf/template_settings.txt "$tmp_settings" cp conf/urls.py "$project_dir" - - sed -i \ - -e "s|^SECRET_KEY.*|$(sed_replacement_escape "$secret_line")|" \ - -e "s|djangouser|$(sed_replacement_escape "$DB_USER")|g" \ - -e "s|djangopass|$(sed_replacement_escape "$DB_PASS")|g" \ - -e "s|djangohost|$(sed_replacement_escape "$DB_SERVER_IP")|g" \ - -e "s|djangoport|$(sed_replacement_escape "$DB_PORT")|g" \ - -e "s|djangodbname|$(sed_replacement_escape "$DB_NAME")|g" \ - -e "s|emailhostserver|$(sed_replacement_escape "$EMAIL_HOST_SERVER")|g" \ - -e "s|emailport|$(sed_replacement_escape "$EMAIL_PORT")|g" \ - -e "s|emailhostuser|$(sed_replacement_escape "$EMAIL_HOST_USER")|g" \ - -e "s|emailhostpassword|$(sed_replacement_escape "$EMAIL_HOST_PASSWORD")|g" \ - -e "s|emailhosttls|$(sed_replacement_escape "$EMAIL_USE_TLS")|g" \ - -e "s|localserverip|$(sed_replacement_escape "$LOCAL_SERVER_IP")|g" \ - -e "s|localhost|$(sed_replacement_escape "$DNS_URL")|g" \ - "$tmp_settings" - - cp "$tmp_settings" "$project_dir/settings.py" - rm -f "$tmp_settings" + if [ "$render_settings" = true ]; then + render_django_settings_file \ + "$install_script_dir/conf/template_settings.txt" \ + "${settings_output:-$project_dir/settings.py}" \ + "$conf" + else + log "INFO" "Skipping settings.py rendering for staged production image" + fi } # restore_git_ref: reset repository to branch/tag/commit active before script ran. @@ -917,6 +896,8 @@ do --upgrade) set -- "$@" -u ;; --stage) set -- "$@" -j ;; --bootstrap) set -- "$@" -l ;; + --render-settings|--render_settings) set -- "$@" -q ;; + --settings-output|--settings_output) set -- "$@" -y ;; --script) set -- "$@" -s ;; --script_before) set -- "$@" -p ;; --script_after) set -- "$@" -o ;; @@ -956,9 +937,11 @@ run_script_before=false migration_script=() migration_script_before=() skip_tables=false +render_settings="auto" +settings_output="" # PARSE VARIABLE ARGUMENTS WITH getops -options=":c:s:i:u:j:l:r:g:tdbkvhao:p:" +options=":c:s:i:u:j:l:r:g:tdbkvhaqo:p:y:" while getopts $options opt; do case $opt in i ) @@ -1036,6 +1019,12 @@ while getopts $options opt; do a ) restart_apache=false ;; + q ) + render_settings=true + ;; + y ) + settings_output=$OPTARG + ;; h ) usage exit 1 @@ -1072,11 +1061,21 @@ if [ "$workflow" != "standard" ]; then operation_scope="app" fi +# Traditional installs keep their historical rendering behavior. Container +# image staging must opt in, which only the non-secret test build does. +if [ "$render_settings" = "auto" ]; then + if [ "$workflow" = "standard" ]; then render_settings=true; else render_settings=false; fi +fi + # Default to loading initial tables on installs unless explicitly skipped. if [ "$operation" = "install" ] && [ "$skip_tables" = false ] && [ "$tables" = false ]; then tables=true fi +# Keep the configuration addressable after staging functions change directory. +if [[ "$conf" != /* ]]; then + conf="$(cd "$(dirname "$conf")" && pwd)/$(basename "$conf")" +fi load_install_config PROJECT_NAME="${PROJECT_NAME:-iskylims}" checkout_git_revision From 4f69aa1940a90133f1da31e4a64e6cde0234c208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Thu, 6 Aug 2026 13:45:27 +0200 Subject: [PATCH 02/55] initialized new deployment standard --- .bu-isciii-deployment/state.json | 51 ++ .dockerignore | 60 +- .gitignore | 131 +--- Dockerfile | 154 ++-- LEAME.md | 415 ++--------- README.md | 689 ++--------------- conf/INSTALL_SETTINGS.md | 62 ++ conf/docker_production_settings.txt | 120 ++- conf/docker_test_settings.txt | 105 +-- conf/iskylims_apache_centos_redhat.conf | 45 -- conf/iskylims_apache_logs.conf | 17 - conf/iskylims_apache_reverse_proxy.conf | 42 -- conf/iskylims_apache_server-status.conf | 14 - conf/iskylims_apache_ubuntu.conf | 45 -- conf/template_settings.py | 65 ++ container_install.sh | 947 +++++++++--------------- deployment/apache/00-logs.conf | 8 + deployment/apache/01-reverse-proxy.conf | 31 + deployment/apache/02-server-status.conf | 6 + deployment/lib/container/common.sh | 786 ++++++++++++++++++++ deployment/lib/container/django.sh | 107 +++ deployment/project.json | 27 + docker-compose.prod.yml | 140 ++-- docker-compose.test.yml | 166 +++-- scripts/container_start.sh | 111 +-- scripts/smoke_test.sh | 40 + 26 files changed, 2065 insertions(+), 2319 deletions(-) create mode 100644 .bu-isciii-deployment/state.json create mode 100644 conf/INSTALL_SETTINGS.md delete mode 100644 conf/iskylims_apache_centos_redhat.conf delete mode 100644 conf/iskylims_apache_logs.conf delete mode 100644 conf/iskylims_apache_reverse_proxy.conf delete mode 100644 conf/iskylims_apache_server-status.conf delete mode 100644 conf/iskylims_apache_ubuntu.conf create mode 100644 conf/template_settings.py mode change 100644 => 100755 container_install.sh create mode 100644 deployment/apache/00-logs.conf create mode 100644 deployment/apache/01-reverse-proxy.conf create mode 100644 deployment/apache/02-server-status.conf create mode 100644 deployment/lib/container/common.sh create mode 100644 deployment/lib/container/django.sh create mode 100644 deployment/project.json mode change 100644 => 100755 scripts/container_start.sh create mode 100755 scripts/smoke_test.sh diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json new file mode 100644 index 00000000..08f2467b --- /dev/null +++ b/.bu-isciii-deployment/state.json @@ -0,0 +1,51 @@ +{ + "config": { + "ADDONS": { + "apache": { + "VIRTUAL_HOSTS": { + "localhost": "app" + } + } + }, + "APP_NAME": "iSkyLIMS", + "APP_SLUG": "iskylims", + "DEFAULT_BRANCH": "main", + "DESCRIPTION": "Laboratory information management system for wet-lab and dry-lab sequencing workflows.", + "PYTHON_VERSION": "3.11", + "REPOSITORY_URL": "https://github.com/BU-ISCIII/iSkyLIMS.git", + "SERVICES": { + "app": { + "BUILD_CONTEXT": ".", + "DOCKERFILE": "Dockerfile", + "IMAGE": "relecov-iskylims:local", + "INSTALL_CONF": "conf/docker_production_settings.txt", + "PROFILE": "django", + "PROJECT_MODULE": "iskylims", + "TEST_INSTALL_CONF": "conf/docker_test_settings.txt" + } + }, + "TIMEZONE": "Europe/Madrid" + }, + "files": { + ".dockerignore": "461fb3f0597cc936bc0e6f9a4b50bf5e5a06fb03b12293725f57850ec1184eed", + ".gitignore": "8b93d5b7cd43c8c6cfeb8b1a9b3b0a1ef70c02b08c33859940a5f2ef86b19631", + "Dockerfile": "70c53323ca533bfc62e56a5fb3f116155757e9dafa5e33d9556257e80d70abb5", + "LEAME.md": "56279a4c2102a7ed42e34afe7c85055f6279fd9e67ee0df8a5d31c81c7ff9fb5", + "README.md": "7eeaaef44d091288de6285a306d0684b5b0e7aaab724f4a8fd9d2e48915a0d03", + "conf/INSTALL_SETTINGS.md": "9bd0fd353311652474a5b15feff79746e84732f193a7a469694a67fea71cb405", + "conf/docker_production_settings.txt": "39696a9cf4986529eb054b8f104b9956ebba9e5fd09601809706826e61e57c12", + "conf/docker_test_settings.txt": "af8db8079d135be9b95272c5d54821bdf5ad312ffd9e3047e96a391be4bd2c02", + "conf/template_settings.py": "5e286696097d51c662b6b79fbd9599bb4efa3c04f2f30b0a076686ac6583b8db", + "container_install.sh": "8f3104560fd39131bc51444ed9a72fdd0514bef177ab24f16e209211d27d68c3", + "deployment/apache/00-logs.conf": "f94afa73a965ce3c8b25796a9891bb52603f02155f75eb71bba41e9f54873438", + "deployment/apache/01-reverse-proxy.conf": "1cb982a49e14af9369c2d08f806102433eeb0a0ad8097f9dd6704043be18a797", + "deployment/apache/02-server-status.conf": "2dabf0ebe1b5a69c05cba8fc31490a15d82c89556fd22c4f85219bf752321613", + "docker-compose.prod.yml": "4e9f2a53099f626659302bcf313ff895cffdf0fa7555e0fd0ffb80441f928ab4", + "docker-compose.test.yml": "5a9c543d95b05e67205591c4ec0d6b6ab82d2dded01ddcaa2357c76c040ece8d", + "install.sh": "8ed305fd4f2c251bc02db4775ec47fc0e81b809bd5186b5e0ff33c7e99517bf5", + "scripts/container_start.sh": "7d1e7c015c5f95d25ee762493bf051b7c132d4cc10b91bf63f3fe1c5ed443d94", + "scripts/smoke_test.sh": "0a3c21b64674f9bc2178205b2a112d8e9f4a3da5603f33b97c3b20575eaff57b" + }, + "source": "BU-ISCIII deployment standards", + "standard_version": "0.1.0" +} diff --git a/.dockerignore b/.dockerignore index 6653d9c0..9f6ea929 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,60 +1,30 @@ -# Keep .git: install/build diagnostics use git metadata inside the image. +# Production installation settings are passed through a build secret. Exclude +# all local settings and allow only committed, non-sensitive templates/tests. +install_settings.txt +conf/*settings*.txt +!conf/docker_test_settings.txt +!conf/template_install_settings.txt +!conf/template_settings.txt +.tmp_docker_install_conf_*.txt +.env +.env.* +!.env.example +.env.prod.file -# Local Python environments and caches .venv/ venv/ -env/ -ENV/ virtualenv/ +node_modules/ +.npm/ +.vite/ __pycache__/ *.py[cod] .pytest_cache/ -.mypy_cache/ -.hypothesis/ -.coverage -.coverage.* -htmlcov/ -.tox/ - -# Local editor and OS files .vscode/ .idea/ -.DS_Store -*.swp -*.swo - -# Local runtime/generated deployment files -.env -.env.* -!.env.example -.env.prod.file -install_settings.txt -# Configuration files under conf are excluded by default. Only templates and -# the explicitly non-sensitive test profiles are safe to COPY into an image. -conf/*settings*.txt -!conf/docker_test_settings.txt -!conf/template_install_settings.txt -!conf/template_settings.txt -.tmp_docker_install_conf_*.txt -wetlab/logging_config.ini logs/ tmp/ documents/ -outputs/ -catboost_info/ - -# Build, package, and generated archives build/ dist/ *.egg-info/ -.eggs/ -*.egg -*.log -*.gz -*.zip -*.tar -*.tgz - -# Generated Django/runtime files at repo root -/manage.py -/static/ diff --git a/.gitignore b/.gitignore index b38b025e..e0eac952 100644 --- a/.gitignore +++ b/.gitignore @@ -1,124 +1,15 @@ -## Custom -*.xml -*.bin -*.gz -*_BAK -tmp/ -logs/ -/static/ -install_settings.txt +# Local deployment configuration and generated synchronization candidates +.env +.env.*.file +conf/production_settings.txt +# Legacy/application-specific production settings must remain local. my_prod_settings*.txt -wetlab/logging_config.ini -#--- updates introduced for iSkyLIMS in submodules packaging --- -virtualenv/ -iSkyLIMS/ -*iskylims/ -documents/ -manage.py -logs/ -logs -.vscode/ - +*.bu-isciii-update -# Byte-compiled / optimized / DLL files +# Python and runtime artifacts __pycache__/ *.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env -.env.prod.file - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ +.venv/ +virtualenv/ +logs/ +documents/ diff --git a/Dockerfile b/Dockerfile index 06d164de..df9065e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,121 +1,67 @@ # syntax=docker/dockerfile:1.4 -FROM registry.access.redhat.com/ubi9/ubi -ENV TZ=Europe/Madrid -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +FROM registry.access.redhat.com/ubi9/ubi-minimal -# Runtime user (override with build args if needed) ARG APP_UID=1212 ARG APP_GID=1212 -ARG APP_SHELL=/sbin/nologin -ARG INSTALL_PATH=/opt/iskylims -ENV INSTALL_PATH=${INSTALL_PATH} -ENV PIP_NO_CACHE_DIR=1 - -# Updates -RUN dnf -y update - -# Add EPEL for packages not available in default UBI repositories -RUN dnf -y install --setopt=install_weak_deps=False --nodocs \ - https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm - -# Essential software -RUN dnf -y install --setopt=install_weak_deps=False --nodocs \ - git wget \ - python3.11 python3.11-pip python3.11-devel python3.11-wheel \ - gcc gcc-c++ make \ - openssl-devel libffi-devel \ - mariadb mariadb-connector-c-devel \ - httpd-devel cronie \ - rsync tzdata \ - pkgconf-pkg-config \ - gnuplot-minimal \ - && dnf clean all \ - && rm -rf /var/cache/dnf /tmp/* /var/tmp/* +ARG APP_PORT=8000 +ARG APP_REPO_PATH=/srv/iskylims +ARG APP_INSTALL_PATH=/opt/iskylims +ARG GIT_REVISION=current +ARG INSTALL_CONF=conf/docker_test_settings.txt +ARG USE_INSTALL_CONF_SECRET=false +ARG RENDER_DJANGO_SETTINGS=false -# Install supercronic (rootless-friendly cron runner) +ENV APP_REPO_PATH=${APP_REPO_PATH} \ + APP_INSTALL_PATH=${APP_INSTALL_PATH} \ + INSTALL_PATH=${APP_INSTALL_PATH} \ + APP_PORT=${APP_PORT} \ + PROJECT_MODULE=iskylims \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + TZ=Europe/Madrid + +RUN microdnf -y update && \ + microdnf -y install python3.11 python3.11-pip \ + python3.11-devel gcc git rsync wget mariadb-connector-c-devel shadow-utils && \ + microdnf clean all + +ARG SUPERCRONIC_VERSION=v0.2.38 RUN set -eux; \ - SUPERCRONIC_VERSION="v0.2.38"; \ arch="$(uname -m)"; \ case "$arch" in \ x86_64) supercronic_arch="amd64" ;; \ aarch64) supercronic_arch="arm64" ;; \ *) echo "Unsupported architecture for supercronic: $arch" >&2; exit 1 ;; \ esac; \ - supercronic_url="https://github.com/aptible/supercronic/releases/download/${SUPERCRONIC_VERSION}/supercronic-linux-${supercronic_arch}"; \ - if wget --tries=3 --waitretry=2 --retry-connrefused -q -O /usr/local/bin/supercronic "${supercronic_url}"; then \ - chmod +x /usr/local/bin/supercronic; \ - else \ - rm -f /usr/local/bin/supercronic; \ - echo "supercronic download failed from ${supercronic_url}; continuing without cron support"; \ - fi; \ - rm -rf /tmp/* /var/tmp/* - -# Ensure python3 points to the desired version -RUN ln -sf /usr/bin/python3.11 /usr/bin/python3 - -# Install Illumina InterOp CLI used to generate run metric plots -RUN set -eux; \ - cd /opt; \ - wget -q https://github.com/Illumina/interop/releases/download/v1.1.15/InterOp-1.1.15-Linux-GNU.tar.gz; \ - tar -xf InterOp-1.1.15-Linux-GNU.tar.gz; \ - ln -s InterOp-1.1.15-Linux-GNU interop; \ - rm InterOp-1.1.15-Linux-GNU.tar.gz; \ - rm -rf /tmp/* /var/tmp/* - -# Set git repository -RUN mkdir /srv/iskylims -WORKDIR /srv/iskylims + wget -q -O /usr/local/bin/supercronic \ + "https://github.com/aptible/supercronic/releases/download/${SUPERCRONIC_VERSION}/supercronic-linux-${supercronic_arch}"; \ + chmod 0755 /usr/local/bin/supercronic -# Copy the local git repository to docker image directory -COPY --chown=${APP_UID}:${APP_GID} . /srv/iskylims +WORKDIR ${APP_REPO_PATH} +COPY . ${APP_REPO_PATH}/ +COPY scripts/container_start.sh /usr/local/bin/container_start.sh -ENV PATH="/usr/sbin/cron:$PATH" -RUN chmod +x /srv/iskylims/scripts/container_start.sh - -# Set default install type -ARG INSTALL_TYPE=dep -ARG GIT_REVISION=main -ARG INSTALL_CONF=conf/docker_test_settings.txt -ARG USE_INSTALL_CONF_SECRET=false -ARG RENDER_DJANGO_SETTINGS=false - -# Prepare dependencies and stage the application tree in the image so the -# container can restart without rerunning install-time file generation. -ENV SKIP_SYSTEM_PACKAGES=1 -# Production reads the operator configuration through an ephemeral build-secret -# mount so COPY and image layers never retain it. Test builds use the bundled -# non-sensitive configuration and explicitly render test settings into the image. +# Production uses an ephemeral build-secret mount: the operator configuration +# is readable by install.sh but never copied into an image layer. Test builds +# use the committed safe profile and explicitly render test settings. RUN --mount=type=secret,id=install_conf \ - conf_path="$INSTALL_CONF"; \ - if [ "$USE_INSTALL_CONF_SECRET" = "true" ]; then \ - conf_path=/run/secrets/install_conf; \ - test -f "$conf_path" || { echo "Required install_conf build secret is missing" >&2; exit 1; }; \ + conf_path="${INSTALL_CONF}"; \ + if [ "${USE_INSTALL_CONF_SECRET}" = "true" ]; then \ + conf_path=/run/secrets/install_conf; \ + test -f "$conf_path" || { echo "Required install_conf build secret is missing" >&2; exit 1; }; \ fi; \ - /bin/bash install.sh --install dep --git_revision "$GIT_REVISION" --conf "$conf_path" --skip_apache_restart \ - && rm -rf /root/.cache/pip /tmp/* /var/tmp/* -RUN --mount=type=secret,id=install_conf \ - conf_path="$INSTALL_CONF"; \ - if [ "$USE_INSTALL_CONF_SECRET" = "true" ]; then conf_path=/run/secrets/install_conf; fi; \ render_args=""; \ - if [ "$RENDER_DJANGO_SETTINGS" = "true" ]; then render_args="--render-settings"; fi; \ - /bin/bash install.sh --stage install --git_revision "$GIT_REVISION" --conf "$conf_path" --skip_apache_restart $render_args \ - && rm -rf /root/.cache/pip /tmp/* /var/tmp/* -# Use the virtualenv created by install.sh -ENV PATH="${INSTALL_PATH}/virtualenv/bin:${PATH}" - -WORKDIR ${INSTALL_PATH} - -# Create non-root user and set ownership -RUN groupadd -g ${APP_GID} iskylims && \ - useradd -m -u ${APP_UID} -g ${APP_GID} -s ${APP_SHELL} iskylims && \ - mkdir -p ${INSTALL_PATH}/cron ${INSTALL_PATH}/tmp && \ - chown -R ${APP_UID}:${APP_GID} ${INSTALL_PATH} /srv/iskylims && \ - chmod 700 ${INSTALL_PATH}/cron ${INSTALL_PATH}/tmp && \ - git config --system --add safe.directory /srv/iskylims - -# Expose -EXPOSE 8001 - -# Start the application once install.sh has populated /opt/iskylims. -USER iskylims -CMD ["/srv/iskylims/scripts/container_start.sh"] + if [ "${RENDER_DJANGO_SETTINGS}" = "true" ]; then render_args="--render-settings"; fi; \ + bash install.sh --stage install \ + --git_revision "${GIT_REVISION}" --conf "$conf_path" $render_args && \ + groupadd -g "${APP_GID}" app && \ + useradd -u "${APP_UID}" -g "${APP_GID}" -s /sbin/nologin app && \ + chmod 0755 /usr/local/bin/container_start.sh && \ + chown -R "${APP_UID}:${APP_GID}" "${APP_INSTALL_PATH}" "${APP_REPO_PATH}" + +WORKDIR ${APP_INSTALL_PATH} +USER ${APP_UID}:${APP_GID} +EXPOSE ${APP_PORT} +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD python -c "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ['APP_PORT'] + '/health/', timeout=3)" || exit 1 +CMD ["/usr/local/bin/container_start.sh"] diff --git a/LEAME.md b/LEAME.md index 5631daf5..db289c9d 100644 --- a/LEAME.md +++ b/LEAME.md @@ -1,389 +1,78 @@ -# Actualizacion de iSkyLIMS con Podman rootless +# iSkyLIMS production installation and operations -Esta guia es para la actualizacion del despliegue institucional de iSkyLIMS usando Podman rootless. Se asume que iSkyLIMS ya existe en la institucion, que la base de datos de produccion ya esta creada y que el objetivo es actualizar o recrear los contenedores de aplicacion sin crear una base de datos desde cero. +## 1. Pre-installation checks -`container_install.sh` se encarga de construir la imagen, arrancar los contenedores, preparar permisos, actualizar configuracion, aplicar migraciones, refrescar estaticos y ejecutar los pasos necesarios de actualizacion. +1. Record the reviewed revision for every service build context. +2. Verify host capacity, engine/Compose versions, DNS, TLS and network access. +3. Verify database and identity dependencies are reachable from containers. +4. Review the generated Compose model with the protected environment file. -## Indice +## 2. Host preparation -- [Actualizacion de iSkyLIMS con Podman rootless](#actualizacion-de-iskylims-con-podman-rootless) - - [Indice](#indice) - - [Requisitos](#requisitos) - - [Estructura de directorios en los servidores](#estructura-de-directorios-en-los-servidores) - - [Preparar directorios del host](#preparar-directorios-del-host) - - [Actualizar codigo](#actualizar-codigo) - - [Configurar `my_prod_settings_iskylims.txt`](#configurar-my_prod_settings_iskylimstxt) - - [Backup antes de actualizar](#backup-antes-de-actualizar) - - [Ejecutar la actualizacion](#ejecutar-la-actualizacion) - - [Comprobaciones posteriores](#comprobaciones-posteriores) - - [Rollback](#rollback) - - [Reparar permisos](#reparar-permisos) - - [Operaciones utiles](#operaciones-utiles) - - [Notas de permisos](#notas-de-permisos) +Create only the documented database, document, log, configuration and proxy +bind sources. Apply the UID/GID, modes and SELinux labels declared by each +component's independent permission specification. -## Requisitos +## 3. Configuration preparation -El despliegue usa: +Prepare one mode-`0600` production settings file per application service. +Selected infrastructure add-ons contribute clearly marked sections to one +application's file; they do not introduce additional settings files. +Confirm that no file contains unresolved `CHANGE_ME` values and no `VITE_*` +setting contains a secret. -- Podman rootless ejecutado por un usuario normal del sistema. -- `podman-compose` o `podman compose`. -- `container_install.sh` desde el repositorio de iSkyLIMS. -- `docker-compose.prod.yml`, lanzado con Podman. -- Una base de datos MySQL/MariaDB externa ya existente. -- Una configuracion Samba/storage ya existente o configurada desde la interfaz. -- Bind mounts del host para logs, configuracion Apache y `settings.py`. -- Volumenes Podman para `documents` y `static`. - -Comprueba que Podman funciona sin root: - -```bash -podman info -podman ps -``` - -No ejecutes `container_install.sh` con `sudo`. El usuario que ejecuta Podman debe ser el mismo usuario que ejecuta `container_install.sh`. - -Los ejemplos usan `podman compose`. Si tu servidor solo tiene `podman-compose`, sustituye `podman compose` por `podman-compose`. - -## Estructura de directorios en los servidores - -Los servidores de desarrollo, preproduccion y produccion siguen la misma convencion de directorios. Cada tipo de informacion tiene una ubicacion concreta para separar el codigo y la configuracion del despliegue, los datos gestionados por Podman, los bind mounts y los logs. La estructura siguiente es orientativa: solo muestra una aplicacion como ejemplo y no pretende enumerar todos los directorios o ficheros existentes. - -```text -/opt/containers_apps/ -└── iskylims/ - ├── backup/ # Opcional: backups propios del despliegue - └── iskylims/ # Clon Git, fuentes y configuracion de instalacion - -/srv/containers/ -├── backup/ # Backups centralizados, si no estan junto al despliegue -├── bind/ -│ └── iskylims/ # Bind mounts organizados por aplicacion -│ ├── iskylims_apache_conf/ -│ ├── iskylims_app_setting/ -│ └── iskylims_django_setting/ -├── shared/ # Datos compartidos entre aplicaciones, cuando proceda -└── storage/ - └── / # Almacenamiento interno rootless de Podman - ├── overlay/ - ├── overlay-containers/ - ├── overlay-images/ - └── volumes/ # Volumenes persistentes gestionados por Podman - -/var/log/local/ -└── iskylims/ - ├── apache/ # Logs del servidor web y de ModSecurity - └── apps/ # Logs de aplicacion, cron y procesos auxiliares -``` - -Uso de cada ubicacion: - -- `/opt/containers_apps//` agrupa una aplicacion o un conjunto de aplicaciones que se despliegan juntas. Contiene los clones Git, el codigo fuente y los ficheros de configuracion usados por la instalacion. La instalacion se ejecuta desde este directorio. Puede incluir un directorio `backup/` para backups propios del despliegue. -- `/srv/containers/backup/` es la ubicacion alternativa para centralizar los distintos tipos de backup. Cada despliegue debe elegir de forma coherente entre esta ruta y su directorio `backup/` bajo `/opt/containers_apps/`. -- `/srv/containers/bind//` contiene exclusivamente las rutas del host que se montan como bind mounts. Deben estar separadas por aplicacion y creadas con los propietarios y permisos requeridos antes de arrancar los contenedores. Estos permisos se gestionan mediante el script de instalación. -- `/srv/containers/storage//` contiene la estructura de almacenamiento rootless de Podman, incluidos sus metadatos, capas, imagenes y volumenes. Podman gestiona esta estructura; no se deben cambiar manualmente sus propietarios o permisos. En los servidores actuales, `` puede ser, por ejemplo, `bioinfo`. -- `/srv/containers/shared/` se reserva para datos que deban compartir varias aplicaciones. -- `/var/log/local//` centraliza los logs persistentes del host. Como norma general, `apache/` contiene los logs del servidor web y `apps/` los de la aplicacion y sus procesos auxiliares. Algunas aplicaciones pueden necesitar subdirectorios adicionales. - -Otros despliegues, como `beacon`, `localega` o `relecov-platform`, repiten esta misma separacion bajo su propio nombre. Esta convencion es importante al aplicar permisos: el codigo, los bind mounts, los volumenes gestionados por Podman y los logs no deben tratarse como si fueran el mismo tipo de almacenamiento. - -## Preparar directorios del host - -Los bind mounts son rutas reales del host. Deben existir y pertenecer al usuario que ejecuta `container_install.sh`. - -Ejemplo recomendado: - -```bash -sudo mkdir -p /var/log/local/relecov-iskylims/apps -sudo mkdir -p /var/log/local/relecov-iskylims/apache -sudo mkdir -p /srv/containers/bind/iskylims/iskylims_apache_conf -sudo mkdir -p /srv/containers/bind/iskylims/iskylims_django_setting - -sudo chown -R "_USER-RUNNING_PODMAN_:_USER-RUNNING_PODMAN_" /var/log/local/relecov-iskylims -sudo chown -R "_USER-RUNNING_PODMAN_:_USER-RUNNING_PODMAN_" /srv/containers/bind/iskylims -``` - -Si usas otras rutas para `APACHE_CONF_PATH` o `DJANGO_SETTINGS_PATH`, crea esas rutas y asignales la misma propiedad. - -`container_install.sh` ajustara despues los permisos internos con `podman unshare` y con `podman exec --user 0` cuando el contenedor este levantado. - -## Actualizar codigo - -Entra en el repositorio como el usuario que ejecuta Podman: - -```bash -cd /ruta/al/repositorio/iskylims -git pull -``` - -Si todavia no existe el repositorio en el servidor: - -```bash -git clone https://gitlab.isciii.es/bu-isciii/iSkyLIMS.git iskylims -cd iskylims -``` - -## Configurar `my_prod_settings_iskylims.txt` - -Si el fichero ya existe, revisalo y mantenlo. Si no existe, copialo desde la plantilla: - -```bash -cp conf/docker_production_settings.txt conf/my_prod_settings_iskylims.txt -``` - -Edita: - -```bash -nano conf/my_prod_settings_iskylims.txt -``` - -Valores principales: - -```bash -INSTALL_PATH='/opt/iskylims' - -APACHE_CONF_PATH='/srv/containers/bind/iskylims/iskylims_apache_conf' -DJANGO_SETTINGS_PATH='/srv/containers/bind/iskylims/iskylims_django_setting/settings.py' - -SERVER_STATUS_SERVER_NAME='' -SERVER_STATUS_ALIASES='127.0.0.1 localhost' -SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' - -APACHE_FORWARDED_PROTO='https' -APACHE_FORWARDED_PORT='443' - -APP_UID='1212' -APP_GID='1212' -APP_SHELL='/sbin/nologin' -APP_PORT='8001' - -DB_USER='' -DB_PASS='' -DB_NAME='iskylims' -DB_SERVER_IP='' -DB_PORT=3306 - -EMAIL_HOST_SERVER='docker.container.internal' -EMAIL_PORT='25' -EMAIL_HOST_USER='' -EMAIL_HOST_PASSWORD='' -EMAIL_USE_TLS='False' - -LOCAL_SERVER_IP='*' -DNS_URL='' -``` - -Notas: - -- `INSTALL_PATH` es la ruta dentro del contenedor. -- `APACHE_CONF_PATH` es una ruta del host donde se escriben los ficheros Apache renderizados. -- `DJANGO_SETTINGS_PATH` es una ruta del host para el `settings.py` montado en el contenedor. -- `DB_*` debe apuntar a la base de datos de produccion existente. -- Mantener el mismo `APP_UID` y `APP_GID` en todas las actualizaciones evita problemas de permisos. - -## Backup antes de actualizar - -Haz siempre backup antes de ejecutar la actualizacion. - -Crea una carpeta de backup: - -```bash -BACKUP_DIR=~/iskylims_backup_$(date +%Y%m%d_%H%M%S) -mkdir -p "$BACKUP_DIR" -``` - -Backup de base de datos: - -```bash -mysqldump --user= --password --host= --port= iskylims > "$BACKUP_DIR/iskylims.sql" -``` - -Backup de volumenes Podman: - -```bash -podman volume ls | grep iskylims -podman volume export iskylims_iskylims_documents > "$BACKUP_DIR/iskylims_documents.tar" -podman volume export iskylims_iskylims_static > "$BACKUP_DIR/iskylims_static.tar" -``` - -Backup de configuracion: - -```bash -cp conf/my_prod_settings_iskylims.txt "$BACKUP_DIR/" -cp .env.prod.file "$BACKUP_DIR/" 2>/dev/null || true -``` - -## Ejecutar la actualizacion - -Para la mayoria de actualizaciones: - -```bash -bash container_install.sh --engine podman --install_conf conf/my_prod_settings_iskylims.txt --action upgrade 2>&1 | tee ./iskylims_podman_upgrade_$(date +%Y%m%d_%H%M%S).log -``` - -El script: - -- construye una nueva imagen; -- arranca o recrea los contenedores necesarios; -- genera `.env.prod.file`; -- renderiza configuracion Apache; -- prepara `settings.py`; -- repara permisos de bind mounts y volumenes; -- ejecuta `install.sh --bootstrap upgrade`; -- aplica migraciones; -- refresca `collectstatic`. - -No usa la accion `install` porque este procedimiento asume una base de datos institucional ya existente. - -Si la version instalada necesita scripts de migracion de datos, sigue la [guia de actualizacion especifica para esa version](docs/upgrades/README.md) en lugar de ejecutar solamente el comando generico. - -## Comprobaciones posteriores - -Comprueba contenedores: - -```bash -podman compose --env-file .env.prod.file -f docker-compose.prod.yml ps -``` - -Revisa logs: - -```bash -podman compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app -podman compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 apache -``` - -Comprueba la aplicacion: - -```text -http://:8080 -``` - -Si se han cambiado parametros de runtime en `conf/my_prod_settings_iskylims.txt`, vuelve a ejecutar `container_install.sh` para regenerar `.env.prod.file` y recrear los contenedores de forma coherente. - -## Rollback - -Si la actualizacion falla y necesitas volver atras: - -1. Deten contenedores: - - ```bash - podman compose --env-file .env.prod.file -f docker-compose.prod.yml down - ``` - -2. Vuelve al commit o tag anterior del codigo: - - ```bash - git checkout - ``` - -3. Restaura la base de datos: - - ```bash - mysql --user= --password --host= --port= iskylims < "$BACKUP_DIR/iskylims.sql" - ``` - -4. Restaura volumenes si es necesario: - - ```bash - podman volume import iskylims_iskylims_documents "$BACKUP_DIR/iskylims_documents.tar" - podman volume import iskylims_iskylims_static "$BACKUP_DIR/iskylims_static.tar" - ``` - -5. Restaura configuracion si cambio: - - ```bash - cp "$BACKUP_DIR/my_prod_settings_iskylims.txt" conf/my_prod_settings_iskylims.txt - ``` - -6. Repara permisos, arranca y vuelve a reparar volumenes montados: - - ```bash - bash container_install.sh --engine podman --install_conf conf/my_prod_settings_iskylims.txt --action fix-permissions - podman compose --env-file .env.prod.file -f docker-compose.prod.yml up -d - bash container_install.sh --engine podman --install_conf conf/my_prod_settings_iskylims.txt --action fix-permissions - ``` - -7. Revisa logs: - - ```bash - podman compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app - podman compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 apache - ``` - -## Reparar permisos - -Ejecuta esta accion si: - -- se han recreado contenedores manualmente; -- se han restaurado volumenes; -- se han cambiado propietarios en el host; -- se han cambiado `APP_UID` o `APP_GID`; -- el contenedor no arranca por permisos de bind mounts. +## 4. First installation ```bash -bash container_install.sh --engine podman --install_conf conf/my_prod_settings_iskylims.txt --action fix-permissions +bash container_install.sh --action install --engine podman \ + --git_revision \ + --install_conf_map app,/protected/app_production_settings.txt ``` -Si el contenedor no esta arrancado, esta accion repara solo los bind mounts del host. Despues arranca los contenedores y repite la accion para reparar los volumenes montados: +## 5. Verification -```bash -bash container_install.sh --engine podman --install_conf conf/my_prod_settings_iskylims.txt --action fix-permissions -podman compose --env-file .env.prod.file -f docker-compose.prod.yml up -d -bash container_install.sh --engine podman --install_conf conf/my_prod_settings_iskylims.txt --action fix-permissions -``` +Run the generated smoke test against direct service endpoints and then verify +the public Apache/TLS endpoint, authentication redirects and one real read +workflow. Record `compose ps`, image IDs and deployed revisions. -## Operaciones utiles +## 6. Upgrade -Usa siempre `.env.prod.file` al ejecutar Podman Compose directamente: +1. Review the supported source/target versions and configuration differences. +2. Back up all assets listed below from one consistent recovery point. +3. Run `container_install.sh --action upgrade` with the new revision. +4. Stop on build, readiness, bootstrap, migration or smoke-test failure. -```bash -podman compose --env-file .env.prod.file -f docker-compose.prod.yml ps -podman compose --env-file .env.prod.file -f docker-compose.prod.yml up -d -podman compose --env-file .env.prod.file -f docker-compose.prod.yml restart app -podman compose --env-file .env.prod.file -f docker-compose.prod.yml down -``` +| Asset | Production location | Recovery requirement | +|---|---|---| +| `app` database | External production database | Database backup before migration | +| `app` documents | `HOST_DATA_PATH/documents` | Filesystem backup | +| `app` static | `app_static` named volume | Replaceable through collectstatic | -Entrar al contenedor: +## 7. Backup and restore -```bash -podman exec -it iskylims_app bash -``` +Test database dumps and filesystem/volume backups before relying on them. +Restore databases, documents and identity state from the same recovery point, +then deploy the compatible application revisions and rerun all smoke tests. -Ejecutar `collectstatic` manualmente: +## 8. Rollback -```bash -podman exec -it iskylims_app bash -lc 'cd /opt/iskylims && source virtualenv/bin/activate && python manage.py collectstatic --noinput' -``` +Use an image-only rollback only when schema compatibility is confirmed. +Otherwise stop writes, restore the complete recovery point, deploy the recorded +previous revisions and verify direct and public endpoints before reopening. -Ejecutar manualmente el bootstrap de actualizacion: +## 9. Permission repair ```bash -podman exec -it iskylims_app bash -c 'cd /srv/iskylims && bash install.sh --bootstrap upgrade --git_revision main --conf conf/my_prod_settings_iskylims.txt --tables --skip_apache_restart' +bash container_install.sh --action fix-permissions --engine podman \ + --install_conf_map app,/protected/app_production_settings.txt ``` -## Notas de permisos - -Bind mounts: - -- Son rutas reales del host. -- Deben existir antes de arrancar contenedores. -- Deben pertenecer al usuario que ejecuta Podman rootless. -- `container_install.sh` usa `podman unshare` para aplicar propietarios internos cuando hace falta. - -Volumenes Podman: - -- Los gestiona Podman en el almacenamiento rootless del usuario. -- Se reparan desde dentro del contenedor con `podman exec --user 0`. -- Si cambias `APP_UID` o `APP_GID`, ejecuta `--action fix-permissions`. - -Apache: - -- El contenedor Apache UBI usa UID `1001` y grupo `0`. -- Los logs Apache se preparan para ese usuario. -- Los ficheros Apache renderizados se dejan con permisos `0664`. +This action MUST NOT build images, migrate databases or delete data. -`settings.py`: +## 10. Incident troubleshooting -- Se monta desde el host. -- `container_install.sh` lo prepara con permisos `0664`. -- Si editas el fichero a mano, ejecuta despues `--action fix-permissions`. +1. Capture service state and logs. +2. Test direct application health. +3. Test internal DNS and dependent databases/identity services. +4. Test Apache routing and external TLS last. +5. Preserve evidence and backups before any destructive recovery action. diff --git a/README.md b/README.md index a8723034..64110c43 100644 --- a/README.md +++ b/README.md @@ -1,664 +1,113 @@ # iSkyLIMS -[![Django](https://img.shields.io/static/v1?label=Django&message=4.2&color=blue?style=plastic&logo=django)](https://github.com/django/django) -[![Python](https://img.shields.io/static/v1?label=Python&message=3.8.10&color=green?style=plastic&logo=Python)](https://www.python.org/) -[![Bootstrap](https://img.shields.io/badge/Bootstrap-v5.0-blueviolet?style=plastic&logo=Bootstrap)](https://getbootstrap.com) -[![version](https://img.shields.io/badge/version-3.1.1-orange?style=plastic&logo=GitHub)](https://github.com/BU-ISCIII/iskylims.git) - -The introduction of massive sequencing (MS) in genomics facilities has meant an exponential growth in data generation, requiring a precise tracking system, from library preparation to fastq file generation, analysis and delivery to the researcher. Software designed to handle those tasks are called Laboratory Information Management Systems (LIMS), and its software has to be adapted to their own genomics laboratory particular needs. iSkyLIMS is born with the aim of helping with the wet laboratory tasks, and implementing a workflow that guides genomics labs on their activities from library preparation to data production, reducing potential errors associated to high throughput technology, and facilitating the quality control of the sequencing. Also, iSkyLIMS connects the wet lab with dry lab facilitating data analysis by bioinformaticians. - -![Image](img/iskylims_scheme.png) - -According to existent infrastructure sequencing is performed on an Illumina NextSeq instrument. Data is stored in NetApp mass storage device and fastq files are generated (bcl2fastq) on a Sun Grid Engine High Performance Computing cluster (SGE-HPC). -Application servers run web applications for bioinformatics analysis (GALAXY), the iSkyLIMS app, and host the MySQL information tier. iSkyLIMS WetLab workflow deals with sequencing run tracking and statistics. Run tracking passes through five states: "recorded” genomics user record the new sequencing run into the system, the process will wait till run is completed by the machine and data is transferred to the mass storage device; “Sample sheet sent” sample sheet file with the sequencing run information will be copied to the run folder for bcl2fastq process; “Processing data” run parameters files are processed and data is stored in the database; “Running stats” demultiplexing data generated in bcl2fastq process is processed and stored into the database, “Completed” all data is processed and stored successfully. Statistics per sample, per project, per run and per investigation are provided, as well as annual and monthly reports. iSkyLIMS DryLab workflow deals with bioinformatics services request and statistics. User request services that can be associated with a sequencing run. Stats and services tracking is provided. - -- [iSkyLIMS](#iskylims) - - [Get the code (required)](#get-the-code-required) - - [Choose your path](#choose-your-path) - - [Minimum requirements](#minimum-requirements) - - [Docker deployment](#docker-deployment) - - [Local test stack](#local-test-stack) - - [Production container](#production-container) - - [Persist logs/documents on the host](#persist-logsdocuments-on-the-host) - - [Apache reverse proxy (container) + Gunicorn](#apache-reverse-proxy-container--gunicorn) - - [Cron jobs inside the container](#cron-jobs-inside-the-container) - - [Manage containers after installation](#manage-containers-after-installation) - - [Upgrade docker deployment](#upgrade-docker-deployment) - - [Bare-metal deployment (Ubuntu/CentOS)](#bare-metal-deployment-ubuntucentos) - - [Install](#install) - - [Clone the repository](#clone-the-repository) - - [Prepare the database](#prepare-the-database) - - [Configure install\_settings.txt](#configure-install_settingstxt) - - [Run install.sh](#run-installsh) - - [Upgrade bare-metal deployment](#upgrade-bare-metal-deployment) - - [Common operations (Docker + bare-metal)](#common-operations-docker--bare-metal) - - [Database creation, users and grants](#database-creation-users-and-grants) - - [Backups](#backups) - - [Restore / rollback](#restore--rollback) - - [What to do if something fails](#what-to-do-if-something-fails) - - [Final configuration steps](#final-configuration-steps) - - [SAMBA configurarion](#samba-configurarion) - - [Email verification](#email-verification) - - [Developer notes](#developer-notes) - - [Django migrations workflow](#django-migrations-workflow) - - [Persistent host paths](#persistent-host-paths) - - [Configure Apache server](#configure-apache-server) - - [Verification of the installation](#verification-of-the-installation) - - [iSkyLIMS documentation](#iskylims-documentation) - -For any problems or bug reporting please post us an [issue](https://github.com/BU-ISCIII/iSkyLIMS/issues) - -## Get the code (required) - -All installation paths assume you already cloned the repository: +Laboratory information management system for wet-lab and dry-lab sequencing workflows. -```bash -git clone https://github.com/BU-ISCIII/iskylims.git iskylims -cd iskylims -``` - -## Choose your path - -- **Docker (local test)**: spin up MySQL + Samba + iSkyLIMS with demo data to try the app quickly. -- **Docker (production container)**: deploy only the application container, pointing to your existing DB/Samba. -- **Bare-metal**: install or upgrade directly on Ubuntu/CentOS hosts with `install.sh`. - -## Minimum requirements - -Container deployment requirements: - -- Docker Engine + Docker Compose v2, or Podman + `podman-compose` -- git >= 2.34 to clone/update the repository -- Host MySQL/MariaDB, Apache, Python, and `lsb_release` are not required for container deployment -- For local test containers: MySQL and Samba are started as containers by `container_install.sh --test` -- For production containers: access to an external MySQL/MariaDB server and Samba share configured in the selected install config -- Host directories and permissions for logs, documents, and static files, as described in [Persist logs/documents on the host](#persist-logsdocuments-on-the-host) - -Bare-metal deployment requirements: - -- **sudo privileges** for dependency installation -- MySQL >= 8.0 or MariaDB > 10.4 -- Apache >= 2.4 -- git >= 2.34 -- Python >= 3.11 -- Local email sender configured -- Access to the Samba share where run folders live -- `lsb_release` package: - - RedHat/CentOS: `yum install redhat-lsb-core` - - Ubuntu: `apt install lsb-core lsb-release` - -## Docker deployment - -### Local test stack - -Bring up a full test stack (database, Samba, app) plus fixtures and demo data: - -```bash -bash container_install.sh --test 2>&1 | tee test.log -``` - -Use `--engine podman` to run the same flow with Podman: - -```bash -bash container_install.sh --test --engine podman 2>&1 | tee test.log -``` - -This uses `docker-compose.test.yml` by default. - -Defaults can be customised: - -- `--demo_data /path/to/iskylims_demo_data.tar.gz` to reuse a local demo archive (otherwise it is downloaded). -- `--skip_demo_data` or `--skip_test_data` to avoid loading extra data. -- `--install_type` (`full` by default) and `--git_revision` to control the build. -- `--script` to run one or more Django migration scripts through `install.sh` (repeat the flag as needed). - -Example running a migration script during Docker install: - -```bash -bash container_install.sh --test --script migrate_optional_values 2>&1 | tee test.log -``` - -When the script finishes, open `http://localhost:8001` and follow the prompt to create the Django superuser. - -The image now includes the staged application tree under `${INSTALL_PATH}`. Test containers can therefore be recreated or restarted without rerunning the file installation step; only DB/bootstrap tasks are executed by `container_install.sh`. - -### Production container - -Deploy the iSkyLIMS container against external MySQL/Samba services: - -1. Copy and edit the production settings template: - - ```bash - cp conf/docker_production_settings.txt conf/my_prod_settings_iskylims.txt - # edit conf/my_prod_settings_iskylims.txt with your DB/Samba details - ``` - -2. Create the host directories used as bind mount sources and make them manageable by the account that will run `container_install.sh`. - - At minimum, this includes `/var/log/local/relecov-iskylims/apps`, `/var/log/local/relecov-iskylims/apache`, and any custom `APACHE_CONF_PATH` or `DJANGO_SETTINGS_PATH` parent directory set in `conf/my_prod_settings_iskylims.txt`. - - ```bash - sudo mkdir -p /var/log/local/relecov-iskylims/apps /var/log/local/relecov-iskylims/apache - sudo chown -R "$USER:$USER" /var/log/local/relecov-iskylims - ``` - - For rootless Podman, `container_install.sh` uses `podman unshare` to apply container UID/GID ownership where needed. For Docker, normal host permissions apply, so the script runner must be able to create and adjust the bind mount paths. - -3. Build and run in production mode (uses `docker-compose.prod.yml` by default): - - ```bash - bash container_install.sh --install_conf conf/my_prod_settings_iskylims.txt 2>&1 | tee ./iskylims_docker_install_$(date +%Y%m%d_%H%M%S).log - ``` - - Use `--compose_file` to override the compose file or `--install_type`/`--git_revision` to change the build. - Add `--engine podman` to use Podman instead of Docker. - Tip: capture logs for troubleshooting: - - ```bash - bash container_install.sh --install_conf conf/my_prod_settings_iskylims.txt 2>&1 | tee ./iskylims_docker_install_$(date +%Y%m%d_%H%M%S).log - ``` - -4. If this is a fresh install, create the Django superuser when prompted and complete the Samba configuration in the UI. - -Production images now bake the staged iSkyLIMS application into the image itself. Host reboots or container recreation no longer require rerunning the app installation step; `container_install.sh` only performs runtime bootstrap tasks such as migrations, fixture refreshes, optional scripts, superuser creation on first install, and `collectstatic`. - -Container build/runtime values are configured in the selected install config, not by exporting shell variables. Edit these fields in `conf/my_prod_settings_iskylims.txt` before running `container_install.sh`: - -- `INSTALL_PATH`: runtime install root used by the app container, static/documents mounts, and install scripts. Default: `/opt/iskylims`. -- `APACHE_CONF_PATH`: host directory used for Apache bind-mounted config files. Leave empty to use `${INSTALL_PATH}/conf` as the host bind source; set this to a writable host path for rootless or hardened deployments. -- `SERVER_STATUS_SERVER_NAME`: Apache virtual host used for `/server-status`. Leave empty to use `DNS_URL`. -- `SERVER_STATUS_ALIASES`: aliases accepted by the server-status virtual host. Default: `127.0.0.1 localhost`. -- `SERVER_STATUS_ALLOW_FROM`: clients allowed to access `/server-status`. Default: `127.0.0.1 localhost`. -- `APACHE_FORWARDED_PROTO` / `APACHE_FORWARDED_PORT`: forwarded request scheme and port sent by Apache. Production defaults: `https` and `443`. -- `DJANGO_SETTINGS_PATH`: host path used for the bind-mounted Django `settings.py`. Leave empty to use `${INSTALL_PATH}/iskylims/settings.py` as the host bind source. If the value is a directory, ends with `/`, or does not end with `.py`, `container_install.sh` appends `settings.py`. -- `APP_UID` / `APP_GID`: runtime UID/GID for the `iskylims` user inside the container. Default: `1212:1212`. -- `APP_SHELL`: shell assigned to the runtime user during image build. Default: `/sbin/nologin`. -- `APP_PORT`: internal Gunicorn bind port for the `app` service. Default: `8001`. -- `DJANGO_DEBUG`: Django debug flag passed to the production app container. Default: `false`; keep it disabled in production. -- `DB_CONN_MAX_AGE`: Django persistent DB connection lifetime in seconds. Default: `60`. -- `WEB_CONCURRENCY`: Gunicorn worker count. Default: `2`. -- `GUNICORN_THREADS`: threads per Gunicorn worker. Default: `2`. -- `GUNICORN_TIMEOUT`: Gunicorn request timeout in seconds. Default: `300`. -- `GUNICORN_KEEPALIVE`: Gunicorn keep-alive in seconds. Default: `5`. - -The standalone iSkyLIMS Apache container renders `APACHE_FORWARDED_PROTO` and `APACHE_FORWARDED_PORT` from this settings file. When iSkyLIMS runs inside the integrated RELECOV stack, the shared `relecov_apache` proxy uses the corresponding values from `my_prod_settings_relecov.txt`. - -During production install/upgrade, `container_install.sh` writes `.env.prod.file` in the repository root. This file is ignored by git and is used by Compose for variable interpolation in `docker-compose.prod.yml`. It intentionally contains Compose/runtime metadata, not database or email passwords. - -The selected installation file remains the single configuration source. For -image construction, `container_install.sh` mounts it ephemerally with the -selected container engine at -`/run/secrets/install_conf`; it is not copied into an image layer. -`.dockerignore` excludes production and temporary settings files from `COPY`. -Test builds use the committed non-sensitive test profile and explicitly render -`settings.py` into the test image. Production settings are rendered on the host -and bind-mounted when the container starts. -Production configuration filenames must match `*settings*.txt`; this lets -`.dockerignore` exclude custom configurations without excluding dependency files -such as `conf/requirements.txt`. - -Host directory and ownership preparation is described in [Persist logs/documents on the host](#persist-logsdocuments-on-the-host). - -#### Persist logs/documents on the host - -The production compose file uses `INSTALL_PATH` from the selected install config as the app container runtime root. Apache config and Django settings bind sources use `APACHE_CONF_PATH` and `DJANGO_SETTINGS_PATH` when set. - -Persistence layout: - -- `/var/log/local/relecov-iskylims/apps` -> `${INSTALL_PATH}/logs` inside the `app` container -- `/var/log/local/relecov-iskylims/apache` -> `/var/log/httpd` inside the `apache` container -- `${APACHE_CONF_PATH:-${INSTALL_PATH}/conf}/iskylims_apache_reverse_proxy.conf` -> `/etc/httpd/conf.d/iskylims.conf` inside the `apache` container -- `${APACHE_CONF_PATH:-${INSTALL_PATH}/conf}/iskylims_apache_logs.conf` -> `/etc/httpd/conf.d/logformat.conf` inside the `apache` container -- `${APACHE_CONF_PATH:-${INSTALL_PATH}/conf}/iskylims_apache_server-status.conf` -> `/etc/httpd/conf.d/server-status.conf` inside the `apache` container -- `${DJANGO_SETTINGS_PATH:-${INSTALL_PATH}/iskylims/settings.py}` -> `${INSTALL_PATH}/iskylims/settings.py` inside the `app` container -- `iskylims_documents` named volume -> `${INSTALL_PATH}/documents` -- `iskylims_static` named volume -> `${INSTALL_PATH}/static` - -If you override the compose file, ensure these mounts exist to keep logs and documents persistent. - -Create host directories before the first deployment: - -```bash -sudo mkdir -p /var/log/local/relecov-iskylims/apps -sudo mkdir -p /var/log/local/relecov-iskylims/apache -sudo mkdir -p -sudo mkdir -p -sudo chown -R : /var/log/local/relecov-iskylims/apps -``` - -For hardened/rootless Podman hosts, run the host preparation script as the same -user that starts the containers. The script pre-creates Apache log files, fixes -rootless Podman ownership for the UBI httpd user, and applies SELinux container -labels when SELinux is enabled: - -```bash -bash hardening.sh -``` - -If an administrator runs it as root, set `PODMAN_USER` to the user that starts -the rootless containers: - -```bash -PODMAN_USER=bioinfo bash hardening.sh -``` - -#### Apache reverse proxy (container) + Gunicorn - -For production, the `app` container runs `gunicorn` (not `manage.py runserver`) and the `apache` service in `docker-compose.prod.yml` acts as the reverse proxy. - -Static files: - -- The app collects static files into `${INSTALL_PATH}/static`. -- `docker-compose.prod.yml` shares that directory with the `apache` service through the named volume `iskylims_static`. -- The reverse proxy config serves `/static` directly from `${INSTALL_PATH}/static`. - -During `container_install.sh`, `conf/iskylims_apache_reverse_proxy.conf`, `conf/iskylims_apache_logs.conf`, and `conf/iskylims_apache_server-status.conf` are rendered and copied to `${APACHE_CONF_PATH}` on the host. If `APACHE_CONF_PATH` is empty, they are copied to `${INSTALL_PATH}/conf`. The reverse proxy `ServerName`, forwarded host, and access/error log file names are generated from `DNS_URL` in the selected install config. The `/server-status` virtual host uses `SERVER_STATUS_SERVER_NAME`, `SERVER_STATUS_ALIASES`, and `SERVER_STATUS_ALLOW_FROM`; by default it is restricted to localhost. - -`container_install.sh` prepares a host-side Django `settings.py` bind source at `${DJANGO_SETTINGS_PATH}`, or at `${INSTALL_PATH}/iskylims/settings.py` when `DJANGO_SETTINGS_PATH` is empty. During the bootstrap step, `install.sh` updates that bind-mounted file from `conf/template_settings.txt` and the selected install config, preserving an existing `SECRET_KEY`. Runtime settings can then be edited and the container restarted without rebuilding the image. - -If you need a different app container runtime root, set `INSTALL_PATH` in the install config file before running `container_install.sh`. If the runtime root is not writable on the host, set `APACHE_CONF_PATH` and `DJANGO_SETTINGS_PATH` to writable host paths in the install config file. - -`container_install.sh` creates `${APACHE_CONF_PATH:-${INSTALL_PATH}/conf}`, the parent directory for `${DJANGO_SETTINGS_PATH:-${INSTALL_PATH}/iskylims/settings.py}`, `/var/log/local/relecov-iskylims/apps`, and `/var/log/local/relecov-iskylims/apache` before `compose up`, copies the three Apache config files there, prepares the bind-mounted Django settings file if it does not exist, passes runtime values into Compose, and then runs `install.sh --bootstrap ...` inside the `app` container. The container image already contains the staged Django project and virtualenv under `${INSTALL_PATH}`; the bootstrap step updates settings, applies migrations, optional scripts/fixtures, and refreshes `${INSTALL_PATH}/static`, while the Apache container keeps using the host log path `/var/log/local/relecov-iskylims/apache`. - -SELinux note for pre-production and production: - -- Ensure `/var/log/local/relecov-iskylims/apache` is writable by the container runtime and labeled for containers, for example `container_file_t`. -- If the host path is already labeled `container_file_t`, do not add `:Z` to the Apache log bind mount. `:Z` forces a relabel and may fail with `lsetxattr(... container_file_t ...): operation not permitted`. -- A quick check is: - -```bash -ls -ldZ /var/log/local/relecov-iskylims/apache -``` - -- Expected example: - -```text -system_u:object_r:container_file_t:s0 -``` - -- If Apache fails on startup with `ModSecurity: Failed to open debug log file: /var/log/httpd/modsec_debug.log`, remove any stale host file and recreate/restart the container. In practice, deleting `/var/log/local/relecov-iskylims/apache/modsec_debug.log` has been enough when the existing inode had bad permissions/label state. - -#### Cron jobs inside the container - -Cron runs via `supercronic`, started by the container entrypoint script. The script reads Django `CRONJOBS` directly, writes `${INSTALL_PATH}/cron/iskylims`, and starts `supercronic` as the non-root app user. It does not call `manage.py crontab` or the system `crontab` command during container startup. - -If you change `CRONJOBS`, rebuild or restart the container to regenerate the cron file. - -### Manage containers after installation - -After a production install, use the generated `.env.prod.file` whenever you run Compose directly. This keeps paths, UID/GID, ports, and Gunicorn settings aligned with the install config. - -Docker Compose examples: - -```bash -docker compose --env-file .env.prod.file -f docker-compose.prod.yml ps -docker compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app -docker compose --env-file .env.prod.file -f docker-compose.prod.yml restart app -docker compose --env-file .env.prod.file -f docker-compose.prod.yml up -d -``` - -Podman Compose examples: - -```bash -podman compose --env-file .env.prod.file -f docker-compose.prod.yml ps -podman compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app -podman compose --env-file .env.prod.file -f docker-compose.prod.yml restart app -podman compose --env-file .env.prod.file -f docker-compose.prod.yml up -d -``` - -If you edit container runtime values in the install config, rerun `container_install.sh --install_conf ` so `.env.prod.file` and the running containers are regenerated consistently. - -If containers were recreated manually, persistent volumes were restored, bind mount ownership changed, or `APP_UID` / `APP_GID` changed, repair permissions before running bootstrap tasks: - -```bash -bash container_install.sh --engine podman --install_conf conf/my_prod_settings_iskylims.txt --action fix-permissions -``` - -This action does not rebuild images or run migrations. It refreshes `.env.prod.file` and fixes host bind mount permissions with `podman unshare` when needed. If `iskylims_app` is already running, it also fixes mounted app volumes from inside the container as root; otherwise, start the containers and rerun the same command to repair named volumes. For Docker, use `--engine docker`. - -### Upgrade docker deployment - -Keep the same `APP_UID`/`APP_GID` values in the selected install config before running an upgrade. - -Re-deploy the application container against an existing production database: - -```bash -bash container_install.sh --install_conf conf/my_prod_settings_iskylims.txt --action upgrade 2>&1 | tee ./iskylims_docker_install_$(date +%Y%m%d_%H%M%S).log -``` +## Architecture and services -The upgrade path rebuilds/restarts the container and runs `install.sh --bootstrap upgrade --tables` inside the app container. The app files are already baked into the rebuilt image; the bootstrap phase applies migrations with `--fake-initial`, refreshes `conf/first_install_tables.json`, refreshes static files, and skips superuser/demo/test data loading. +| Service | Profile | Build context | Internal port | +|---|---|---|---:| +| `app` | `django` | `.` | settings: `APP_PORT` | -If the installed version requires data-migration scripts, follow the applicable [version-specific upgrade guide](docs/upgrades/README.md) instead of using only the generic command. +Profile behavior: -## Bare-metal deployment (Ubuntu/CentOS) +- Django services build with an ephemeral settings secret, render protected host settings, and run controlled migration/bootstrap steps. -### Install +Selected add-ons: -#### Clone the repository +- Apache generates either DNS virtual hosts from `ADDONS.apache.VIRTUAL_HOSTS` or prefix routes from `ADDONS.apache.ROUTES`. -```bash -cd -git clone https://github.com/BU-ISCIII/iskylims.git iskylims -cd iskylims -``` - -#### Prepare the database - -Create the database and application user following [Database creation, users and grants](#database-creation-users-and-grants), then note DB host/port/user/password for `install_settings.txt`. - -#### Configure install_settings.txt - -```bash -cp conf/template_install_settings.txt install_settings.txt -nano install_settings.txt -``` +All selected services and add-ons are assembled into one +`docker-compose.prod.yml` and one `docker-compose.test.yml`. -Set your database, email, server IP/URL, and logging preferences in that file. +## Requirements -#### Run install.sh +- Docker with Compose v2, or Podman with a Compose provider. +- Git and access to every declared service build context. +- One protected production settings file for every application service. +- Production DNS, TLS termination, databases, storage and backup destinations + required by the selected profiles. -iSkyLIMS is installed to `/opt/iskylims` by default. The single `install.sh` script handles both dependencies and the app; choose what you need with `--install`: +## Configuration -- `dep`: install system and Python dependencies (requires sudo). -- `app`: deploy iSkyLIMS code, update settings, run migrations, and collect static files (no sudo needed). -- `full`: run both stages in sequence. +Each application repository owns its configuration template. Copy it to a +protected, ignored file, set mode `0600`, and resolve every `CHANGE_ME` value. +Browser-visible `VITE_*` values MUST NOT contain secrets. +Apache and Keycloak settings appear as commented `APACHE_*` and `KEYCLOAK_*` +sections in the selected application's same settings file. -The staged/bootstrap split added for container images is internal. Bare-metal commands do not change: `--install` and `--upgrade` still run the complete dependency, application, and database workflow documented here. +The installer generates one protected `.env.production.file` or +`.env.test.file`. Service prefixes prevent collisions between settings such as +`APP_PORT`, `DB_HOST`, `APP_UID` and `APP_GID`. -Examples: +## Test installation ```bash -# only software dependencies -sudo bash install.sh --install dep - -# only iSkyLIMS application -bash install.sh --install app --git_revision main --tables - -# dependencies + application -sudo bash install.sh --install full --git_revision main --tables -``` - -- Add `--tables` to load the initial fixtures on first-time installs, or `--skip_tables` if you want to skip them. -- Capture logs for troubleshooting with `tee`: - - ```bash - sudo bash install.sh --install full --git_revision main --tables 2>&1 | tee ./iskylims_install_$(date +%Y%m%d_%H%M%S).log - ``` - -- If Apache is managed elsewhere, skip the automatic restart with `--skip_apache_restart`. - -### Upgrade bare-metal deployment - -Run the backup steps in [Backups](#backups) first and back up the full installation folder (for example `/opt/iskylims`). Then refresh the repository and review the current installation settings template before upgrading. - -Update system and Python dependencies: - -```bash -sudo bash install.sh --upgrade dep 2>&1 | tee install_full.log -``` - -Make sure the installation directory permissions allow the non-root step to write to `/opt/iskylims` (adapt your hardening script if paths changed). - -Upgrade the application code and database: - -```bash -bash install.sh --upgrade app --git_revision main -``` - -Upgrades regenerate migrations and apply them with `--fake-initial` so existing tables remain intact, matching the Docker workflow. - -If the installed version requires data-migration scripts, follow the applicable [version-specific upgrade guide](docs/upgrades/README.md) instead of using only the generic command. - -## Common operations (Docker + bare-metal) - -### Database creation, users and grants - -Run as MySQL root: - -```sql -CREATE DATABASE IF NOT EXISTS iskylims CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -CREATE USER IF NOT EXISTS 'iskylims'@'%' IDENTIFIED BY 'djangopass'; -CREATE USER IF NOT EXISTS 'iskylims'@'localhost' IDENTIFIED BY 'djangopass'; - -GRANT ALL PRIVILEGES ON iskylims.* TO 'iskylims'@'%'; -GRANT ALL PRIVILEGES ON iskylims.* TO 'iskylims'@'localhost'; - -FLUSH PRIVILEGES; +bash container_install.sh --test --action install --engine docker \ + --git_revision current ``` -Verification: - -```sql -SHOW GRANTS FOR 'iskylims'@'%'; -``` - -### Backups - -Database dump: +## Production installation ```bash -mysqldump -h -P -u iskylims -p iskylims > iskylims_$(date +%Y%m%d_%H%M%S).sql +bash container_install.sh --action install --engine podman \ + --git_revision \ + --install_conf_map app,/protected/app_production_settings.txt ``` -Logs archive: +## Upgrade -```bash -tar -czf iskylims_app_logs_$(date +%Y%m%d_%H%M%S).tgz -C /var/log/local/relecov-iskylims/apps . - -tar -czf iskylims_apache_logs_$(date +%Y%m%d_%H%M%S).tgz -C /var/log/local/relecov-iskylims/apache . -``` - -Documents volume archive: +Back up persistent state, review release/configuration changes, and run: ```bash -docker run --rm -v iskylims_documents:/from -v "$PWD":/to alpine \ - tar -czf /to/iskylims_documents_$(date +%Y%m%d_%H%M%S).tgz -C /from . +bash container_install.sh --action upgrade --engine podman \ + --git_revision \ + --install_conf_map app,/protected/app_production_settings.txt ``` -With Podman, use the same command replacing `docker` with `podman`. - -Suggested order before upgrades: +Django services run controlled bootstrap/migrations. React services rebuild an +immutable browser bundle and do not run Django bootstrap. -1. DB dump -2. Documents volume archive -3. Logs archive +## Persistence and permissions -### Restore / rollback +| Asset | Production location | Backup/rebuild policy | +|---|---|---| +| `app` database | External production database | Database backup before migration | +| `app` documents | `HOST_DATA_PATH/documents` | Filesystem backup | +| `app` static | `app_static` named volume | Replaceable through collectstatic | -Restore DB: +Run permission repair without building or migrating: ```bash -mysql -h -P -u iskylims -p iskylims < iskylims_YYYYMMDD_HHMMSS.sql +bash container_install.sh --action fix-permissions --engine podman \ + --install_conf_map app,/protected/app_production_settings.txt ``` -Restore documents volume: +Each application and add-on has independent host-bind and running-container +permission specifications in `container_install.sh`. -```bash -docker run --rm -v iskylims_documents:/to -v "$PWD":/from alpine \ - sh -lc "cd /to && tar -xzf /from/iskylims_documents_YYYYMMDD_HHMMSS.tgz" -``` - -With Podman, use the same command replacing `docker` with `podman`. - -Restore logs: +## Testing ```bash -mkdir -p /var/log/local/relecov-iskylims/apps -tar -xzf iskylims_app_logs_YYYYMMDD_HHMMSS.tgz -C /var/log/local/relecov-iskylims/apps - -mkdir -p /var/log/local/relecov-iskylims/apache -tar -xzf iskylims_apache_logs_YYYYMMDD_HHMMSS.tgz -C /var/log/local/relecov-iskylims/apache +bash scripts/smoke_test.sh --engine podman \ + --compose_file docker-compose.prod.yml \ + --env_file .env.production.file ``` -Bare-metal full rollback example: - -```bash -sudo rm -rf /opt/iskylims -sudo cp -r /home/dadmin/backup_prod/iSkyLIMS/ /opt/ -sudo /scripts/hardening.sh -mysql -u iskylims -p -h iskylims < /home/dadmin/backup_prod/bk_iSkyLIMS_YYYYMMDDHHMM.sql -``` - -### What to do if something fails - -When install/upgrade fails, restore the previous state and retry with logs enabled. - -Quick diagnostics: - -```bash -# bare-metal -cd /opt/iskylims -python manage.py check - -# docker -docker compose --env-file .env.prod.file -f docker-compose.prod.yml ps -docker compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app -# podman -podman compose --env-file .env.prod.file -f docker-compose.prod.yml ps -podman compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app -``` - -If you suspect a corrupted image/build cache in Docker: - -```bash -docker compose --env-file .env.prod.file -f docker-compose.prod.yml build --no-cache app -docker compose --env-file .env.prod.file -f docker-compose.prod.yml up -d --force-recreate app -``` - -## Final configuration steps - -### SAMBA configurarion - -- Login with admin account. -- Go to Massive sequencing -![go_to_wetlab](img/got_to_wetlab.png){width:50px} -- Go to Configuration -> Samba configuration -- Fill the form with the appropiate params for the samba shared folder: -![samba form](img/samba_form.png) - -### Email verification - -- Go to Massive sequencing -- Go to Configuration -> Email configuration -- Fill the form with the needed params for your email configuration and try to send a test email. - -## Developer notes - -### Shared container installer library - -`container_install.sh` sources -`deployment/lib/container/common.sh`, vendored from the BU-ISCIII deployment -standards repository. This shared file owns container-engine selection, Compose -execution, rootless Podman filesystem fallbacks, configuration reads, service -resolution, and running-state diagnostics. - -Do not edit the vendored library in this repository. Keep iSkyLIMS-specific -settings rendering, Apache configuration, bind mounts, Samba/demo data, and -bootstrap behavior in `container_install.sh`. Check or update the shared copy -from the standards repository with: - -```bash -python3 scripts/scaffold.py check-lib /path/to/relecov-iskylims -python3 scripts/scaffold.py sync-lib /path/to/relecov-iskylims -``` - -### Django migrations workflow - -Migrations are committed to the repo. Do not run `makemigrations` during install/upgrade. - -Baseline + upgrade flow for new releases: - -1. Generate baseline migrations from the last stable tag (example 3.0.0). -2. Commit the baseline migrations. -3. Generate new migrations on `develop` for schema changes and commit them. -4. Upgrades run `migrate --fake-initial` once to align existing tables, then `migrate` to apply the new migration files. - -### Persistent host paths - -See [Persist logs/documents on the host](#persist-logsdocuments-on-the-host) in the production deployment section. - -### Configure Apache server - -These steps apply to bare-metal Apache installations. Docker production deployments use the `apache` container described above and do not require copying configs into `/etc/apache2` or `/etc/httpd`. - -Copy the apache configuration file according to your distribution inside the apache configuration directory and rename it to `iskylims.conf`. - -Typical config locations: - -- Ubuntu/Debian: `/etc/apache2/sites-available/iskylims.conf` (enable with `a2ensite`) -- CentOS/RHEL: `/etc/httpd/conf.d/iskylims.conf` - -Suggested steps (host Apache as reverse proxy): - -1. Copy the example config: - - ```bash - sudo cp conf/iskylims_apache_reverse_proxy.conf /etc/apache2/sites-available/iskylims.conf - # CentOS/RHEL: - # sudo cp conf/iskylims_apache_reverse_proxy.conf /etc/httpd/conf.d/iskylims.conf - ``` - -2. Edit the config: - - - Set `ServerName` - - Ensure `ProxyPass` points to `http://localhost:8001/` - - Ensure `Alias /static/ /opt/iskylims/static/` - -3. Create the static folder on the host: - - ```bash - sudo mkdir -p /opt/iskylims/static - ``` - -4. Enable required modules (Ubuntu/Debian): - - ```bash - sudo a2enmod proxy proxy_http headers - sudo a2ensite iskylims.conf - ``` - -5. Reload Apache: - - ```bash - sudo systemctl reload apache2 - # CentOS/RHEL: - # sudo systemctl reload httpd - ``` - -### Verification of the installation - -Run the automated smoke test after every clean installation, upgrade, restore, -or rollback. For the standalone local test stack: - -```bash -bash scripts/smoke_test.sh --test --engine docker -``` - -Use `--engine podman` for Podman. The script validates the Compose model, -application, database and Samba container state, Django checks, migration state, -the application root, and Swagger. - -For the standalone production Compose deployment: - -```bash -bash scripts/smoke_test.sh --engine podman --env_file .env.prod.file \ - --host iskylims.example.org -``` - -Override `--base_url` if Apache is published on another address. Use -`--skip_http` only for diagnostics; production acceptance requires the complete -HTTP checks. - -Open the navigator and type "localhost" or the "server local IP" and check that iSkyLIMs is running. +The generated smoke dispatcher combines framework checks with direct HTTP +health checks. Add authenticated application workflows without removing the +generated baseline checks. -You can also check some of the functionality, while also checking samba and database connections using: +## Rollback and restore -- Go to [configuration test](https://iskylims.isciii.es/wetlab/configurationTest/) -- Click submit -- Check all tabs so every connectin is successful. -- Run the 3 tests for each sequencing machine: MiSeq, NextSeq and NovaSeq. +Record the deployed image/revision before every upgrade. An image-only rollback +is safe only when the previous application version supports the current schema. +Otherwise restore databases and persistent files from the same recovery point. +See [LEAME.md](LEAME.md) for the ordered production procedure. -## iSkyLIMS documentation +## Troubleshooting -iSkyLIMS documentation is available at [https://iskylims.readthedocs.io/en/latest](https://iskylims.readthedocs.io/en/latest) +- Inspect `compose ps` and service logs before restarting anything. +- Use `fix-permissions` for reviewed ownership/mode drift. +- Test direct service health before debugging Apache or external TLS. +- Verify Keycloak issuer, public hostname, clients and redirect URIs together. +- Never fake Django migrations or delete persistent volumes as a first response. diff --git a/conf/INSTALL_SETTINGS.md b/conf/INSTALL_SETTINGS.md new file mode 100644 index 00000000..6901eead --- /dev/null +++ b/conf/INSTALL_SETTINGS.md @@ -0,0 +1,62 @@ +# Installation settings for iSkyLIMS + +`docker_test_settings.txt` contains disposable local defaults. +`docker_production_settings.txt` is a template and MUST NOT contain real +production secrets. Operators copy it to an ignored, permission-restricted file. + +## Application and filesystem + +| Variable | Required | Secret | Meaning | +|---|---:|---:|---| +| `REPO_PATH` | yes | no | Staged application source inside the image/container | +| `INSTALL_PATH` | yes | no | Application runtime root inside the container | +| `PROJECT_MODULE` | generated | no | Django package declared by `project.json`; do not customize independently | +| `PYTHON_BIN_PATH` | yes | no | Python used to create the virtual environment | +| `APP_UID`, `APP_GID` | yes | no | Runtime identity and rootless volume ownership | +| `APP_PORT` | yes | no | Internal Gunicorn and host-loopback port | +| `HOST_DATA_PATH` | production | no | Persistent documents root on the host | +| `HOST_LOG_PATH` | production | no | Persistent application logs on the host | + +## Database + +| Variable | Required | Secret | Meaning | +|---|---:|---:|---| +| `DB_HOST`, `DB_PORT` | yes | no | MySQL endpoint reachable by the application | +| `DB_NAME`, `DB_USER` | yes | no | Application schema and least-privilege user | +| `DB_PASSWORD` | yes | yes | Application database password | +| `DB_ROOT_PASSWORD` | test only | yes | Root password for disposable Compose MySQL | + +Production uses an external database. The production Compose file deliberately +contains no database service and publishes no database port. + +## Django and HTTP + +| Variable | Required | Secret | Meaning | +|---|---:|---:|---| +| `DJANGO_DEBUG` | yes | no | MUST be `false` in production | +| `DJANGO_SECRET_KEY` | yes | yes | Stable Django signing key; preserve on upgrade | +| `DJANGO_ALLOWED_HOSTS` | yes | no | Exact production hostnames | +| `DJANGO_CSRF_TRUSTED_ORIGINS` | production | no | Exact HTTPS origins | +| `WEB_CONCURRENCY` | production | no | Gunicorn workers | +| `GUNICORN_THREADS` | production | no | Threads per worker | +| `GUNICORN_TIMEOUT` | production | no | Request timeout in seconds | +| `GUNICORN_KEEPALIVE` | production | no | Keep-alive time in seconds | +| `APP_START_WAIT_TIMEOUT_SECONDS` | yes | no | Maximum wait for staged application files | + +## Email + +Email settings are required when the application sends operational or account +messages. Document whether failed email blocks the user workflow and add an SMTP +test to production acceptance. + +## Project-specific settings + +Add every application setting here before declaring installation complete. +Classify secrets and identify cross-service values that must match an identity +provider, proxy, worker, or frontend. + +## Selected infrastructure add-ons + +### Apache + +`APACHE_LOG_PATH`, `APACHE_BIND_HOST`, `APACHE_PORT`, `APACHE_FORWARDED_PROTO`, `APACHE_FORWARDED_PORT`, and `APACHE_LIMIT_REQUEST_BODY` configure the generated proxy. They are operational values, not Django settings. diff --git a/conf/docker_production_settings.txt b/conf/docker_production_settings.txt index 8c311d64..7c8a37c0 100644 --- a/conf/docker_production_settings.txt +++ b/conf/docker_production_settings.txt @@ -1,78 +1,58 @@ -### Production specific installation config -# Copy this file or edit the values below so they match the external -# infrastructure you are connecting to before building the image. +# Production configuration for this application service. +# Copy to a protected ignored file, chmod 0600, and replace every CHANGE_ME. +# The same file drives the ephemeral image build secret, host settings render, +# Compose environment and temporary bootstrap copy. -### Installation path and modules settings -# container path for installation +### Application layout and runtime identity +REPO_PATH='/srv/iskylims' INSTALL_PATH='/opt/iskylims' -REQUIRED_MODULES='core drylab wetlab clinic django_utils' -MIGRATION_MODULES='core drylab wetlab django_utils' -FAKEINITIAL_MODULES='django_utils iSkyLIMS_core iSkyLIMS_wetlab iSkyLIMS_drylab' - -### Container runtime settings -# Host directory for Apache bind-mounted configuration files. -# Leave empty to use ${INSTALL_PATH}/conf as the host bind source. -# Set this to a writable host path for rootless or hardened deployments. -# Example: APACHE_CONF_PATH='/srv/containers/bind/iskylims/iskylims_apache_conf' -APACHE_CONF_PATH='' - -# Apache server-status virtual host and access control. -# Leave SERVER_STATUS_SERVER_NAME empty to use DNS_URL. Normally use vm hostname -SERVER_STATUS_SERVER_NAME='' -SERVER_STATUS_ALIASES='127.0.0.1 localhost' -SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' - -# Forwarded headers set by Apache in production. -APACHE_FORWARDED_PROTO='https' -APACHE_FORWARDED_PORT='443' - -# Host path for the bind-mounted Django settings.py. -# Leave empty to use ${INSTALL_PATH}/iskylims/settings.py as the host bind source. -# If this is a directory, ends with /, or does not end with .py, container_install.sh appends settings.py. -# Example: DJANGO_SETTINGS_PATH='/srv/containers/bind/iskylims/iskylims_django_setting/settings.py' -DJANGO_SETTINGS_PATH='' - -# UID/GID for the non-root iskylims user inside the app container. +PROJECT_MODULE='iskylims' +PYTHON_BIN_PATH='python3.11' APP_UID='1212' APP_GID='1212' - -# Shell assigned to the container runtime user during image build. -APP_SHELL='/sbin/nologin' - -# Internal app port used by Gunicorn. -APP_PORT='8001' - -# Django production debug flag. Keep disabled in production. +APP_PORT='8000' + +### Persistent host sources +# Documents live below HOST_DATA_PATH; application logs use HOST_LOG_PATH. +HOST_DATA_PATH='/srv/containers/bind/iskylims' +HOST_LOG_PATH='/var/log/local/iskylims/apps' +# Host bind source rendered by container_install.sh before Compose startup. +DJANGO_SETTINGS_PATH='/srv/containers/bind/iskylims/settings/iskylims/settings.py' + +### External production database +DB_HOST='CHANGE_ME' +DB_PORT='3306' +DB_NAME='CHANGE_ME' +DB_USER='CHANGE_ME' +DB_PASSWORD='CHANGE_ME' + +### Django security and public URLs DJANGO_DEBUG='false' -# Django persistent database connection lifetime in seconds. -DB_CONN_MAX_AGE='60' -# Gunicorn runtime tuning. +DJANGO_SECRET_KEY='CHANGE_ME' +DJANGO_ALLOWED_HOSTS='CHANGE_ME' +DJANGO_CSRF_TRUSTED_ORIGINS='https://CHANGE_ME' + +### Email +EMAIL_HOST='CHANGE_ME' +EMAIL_PORT='587' +EMAIL_HOST_USER='CHANGE_ME' +EMAIL_HOST_PASSWORD='CHANGE_ME' +EMAIL_USE_TLS='true' + +### Gunicorn and startup tuning WEB_CONCURRENCY='2' GUNICORN_THREADS='2' -GUNICORN_TIMEOUT='300' +GUNICORN_TIMEOUT='120' GUNICORN_KEEPALIVE='5' - -### (optional) Python installation path where pip and python executables are located -PYTHON_BIN_PATH='python3.11' - -### Settings required to access database (external MySQL server) -DB_USER='django' -DB_PASS='djangopass' -DB_NAME='iskylims' -DB_SERVER_IP='host.docker.internal' # hostname or IP of the production DB (use host.docker.internal for host DB) -DB_PORT=3306 - -### Settings required for sending emails -EMAIL_HOST_SERVER='host.docker.internal' -EMAIL_PORT='25' -EMAIL_HOST_USER='bioinformatica@isciii.es' -EMAIL_HOST_PASSWORD='' -EMAIL_USE_TLS='False' - -### Settings required for accessing iSkyLIMS -LOCAL_SERVER_IP='*' -DNS_URL='*' - -### Logs settings -LOG_TYPE="regular_folder" -LOG_PATH="" +APP_START_WAIT_TIMEOUT_SECONDS='100' + +### Selected infrastructure add-ons +# Apache add-on +# Used only when ADDONS.apache selects this application as CONFIG_SERVICE. +APACHE_LOG_PATH='/var/log/local/iskylims/apache' +APACHE_BIND_HOST='0.0.0.0' +APACHE_PORT='80' +APACHE_FORWARDED_PROTO='https' +APACHE_FORWARDED_PORT='443' +# 50 MiB; increase only when an application endpoint requires it. +APACHE_LIMIT_REQUEST_BODY='52428800' diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index 3349b17b..eaa27b0a 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -1,70 +1,45 @@ -### Installation path and modules settings +# Safe local/test values. Never use these credentials in production. +### Application layout and runtime identity +REPO_PATH='/srv/iskylims' INSTALL_PATH='/opt/iskylims' -REQUIRED_MODULES='core drylab wetlab clinic django_utils' -MIGRATION_MODULES='core drylab wetlab django_utils' -FAKEINITIAL_MODULES='django_utils iSkyLIMS_core iSkyLIMS_wetlab iSkyLIMS_drylab' - -### Container runtime settings -# Host directory for Apache bind-mounted configuration files. -# Leave empty to use ${INSTALL_PATH}/conf as the host bind source. -# Set this to a writable host path for rootless or hardened deployments. -# Example: APACHE_CONF_PATH='/srv/containers/bind/iskylims_apache_conf' -APACHE_CONF_PATH='' -# Apache server-status virtual host and access control. -# Leave SERVER_STATUS_SERVER_NAME empty to use DNS_URL. -SERVER_STATUS_SERVER_NAME='' -SERVER_STATUS_ALIASES='127.0.0.1 localhost' -SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' -# Forwarded headers set by the RELECOV Apache endpoint exposed on port 8081. -APACHE_FORWARDED_PROTO='http' -APACHE_FORWARDED_PORT='8081' -# Host path for the bind-mounted Django settings.py. -# Leave empty to use ${INSTALL_PATH}/iskylims/settings.py as the host bind source. -# If this is a directory, ends with /, or does not end with .py, container_install.sh appends settings.py. -# Example: DJANGO_SETTINGS_PATH='/srv/containers/bind/iskylims/iskylims_django_setting/settings.py' -DJANGO_SETTINGS_PATH='' -# UID/GID for the non-root iskylims user inside the app container. +PROJECT_MODULE='iskylims' +PYTHON_BIN_PATH='python3.11' APP_UID='1212' APP_GID='1212' -# Shell assigned to the container runtime user during image build. -APP_SHELL='/bin/bash' -# Internal app port used by Django runserver/Gunicorn. -APP_PORT='8001' -# Django production debug flag. Keep disabled in production. -DJANGO_DEBUG='false' -# Django persistent database connection lifetime in seconds. -DB_CONN_MAX_AGE='60' -# Gunicorn runtime tuning. -WEB_CONCURRENCY='2' -GUNICORN_THREADS='2' -GUNICORN_TIMEOUT='300' -GUNICORN_KEEPALIVE='5' - -### (optional) Python installation path where pip and python executables are located -PYTHON_BIN_PATH='python3' # example: /opt/python/3.9.6/bin/python3 - -### Settings required to access database - +APP_PORT='8000' + +### Disposable persistence paths +HOST_DATA_PATH='/tmp/iskylims/data' +HOST_LOG_PATH='/tmp/iskylims/logs' +# Test images render settings during build and do not mount this host path. +DJANGO_SETTINGS_PATH='/tmp/iskylims/data/settings/iskylims/settings.py' + +### Compose test database +DB_HOST='db' +DB_PORT='3306' +DB_NAME='iskylims' DB_USER='django' -DB_PASS='djangopass' -DB_NAME='iskylims_docker' -DB_SERVER_IP='db' -DB_PORT=3306 - -### Settings required for sending emails - -EMAIL_HOST_SERVER='host.docker.internal' -EMAIL_PORT='25' -EMAIL_HOST_USER='bioinformatica@isciii.es' +DB_PASSWORD='djangopass' +DB_ROOT_PASSWORD='root' + +### Django and local email defaults +DJANGO_DEBUG='true' +DJANGO_SECRET_KEY='test-only-change-me' +DJANGO_ALLOWED_HOSTS='localhost,127.0.0.1,0.0.0.0' +EMAIL_HOST='localhost' +EMAIL_PORT='1025' +EMAIL_HOST_USER='' EMAIL_HOST_PASSWORD='' -EMAIL_USE_TLS='False' - -### Settings required for accessing iSkyLIMS -# example: 172.0.0.1 -LOCAL_SERVER_IP='*' -# example: iskylims.isciii.es -DNS_URL='*' - -### Logs settings -LOG_TYPE="regular_folder" # can be symbolic link, or regular_folder -LOG_PATH="" # mandatory if LOG_TYPE="symbolic_link", where is the log folder so we can create a symbolic link in the repository folder. +EMAIL_USE_TLS='false' +GUNICORN_KEEPALIVE='5' +APP_START_WAIT_TIMEOUT_SECONDS='100' + +### Selected infrastructure add-ons +# Apache add-on +# Used only when ADDONS.apache selects this application as CONFIG_SERVICE. +APACHE_LOG_PATH='/tmp/iskylims/apache-logs' +APACHE_BIND_HOST='127.0.0.1' +APACHE_PORT='8088' +APACHE_FORWARDED_PROTO='http' +APACHE_FORWARDED_PORT='8088' +APACHE_LIMIT_REQUEST_BODY='52428800' diff --git a/conf/iskylims_apache_centos_redhat.conf b/conf/iskylims_apache_centos_redhat.conf deleted file mode 100644 index 0360afcc..00000000 --- a/conf/iskylims_apache_centos_redhat.conf +++ /dev/null @@ -1,45 +0,0 @@ -## NOTE: WSGISocketPrefix,WSGIPythonHome, WSGIScriptAlias and WSGIPythonPath cannot occur within section - -# Path to serve your app at and wsgi script path - - - ServerAdmin bioinformatica@isciii.es - ServerName iskylims.isciiides.es - DocumentRoot /opt/iskylims - - - ## Load wsgi module with library associated in virtualenv - LoadModule wsgi_module "/opt/iskylims/virtualenv/lib/python3.9/site-packages/mod_wsgi/server/mod_wsgi-py39.cpython-39-x86_64-linux-gnu.so" - - WSGIDaemonProcess iskylims.isciiides.es python-home=/opt/iskylims/virtualenv python-path=/opt/iskylims - WSGIProcessGroup iskylims.isciiides.es - WSGIScriptAlias / /opt/iskylims/iskylims/wsgi.py - WSGIPassAuthorization On - WSGIApplicationGroup %{GLOBAL} - - # Directory piece. This ensures that apache can access wsgi.py script. - - - Satisfy Any - Allow from all - - - - Alias /static /opt/iskylims/static - - - Satisfy Any - Allow from all - - - Alias /documents /opt/iskylims/documents - - - Satisfy Any - Allow from all - - - ErrorLog logs/iskylims/iskylims.isciiides.es-apache.error.log - CustomLog logs/iskylims/iskylims.isciiides.es-apache.access.log combined - - diff --git a/conf/iskylims_apache_logs.conf b/conf/iskylims_apache_logs.conf deleted file mode 100644 index 0ecafdbd..00000000 --- a/conf/iskylims_apache_logs.conf +++ /dev/null @@ -1,17 +0,0 @@ -ErrorLog "logs/apache_error.log" - - - LogFormat "%v %a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D %I %O" combined - LogFormat "%v %{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D %I %O" proxy - LogFormat "%v %{X-Forwarded-For}i %l %u %t \"%r\" %>s %b" common - LogFormat "%{Referer}i -> %U" referer - LogFormat "%{User-agent}i" agent - - - LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio - - - SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded - CustomLog "logs/apache.access.log" combined env=!forwarded - CustomLog "logs/apache.access.log" proxy env=forwarded - diff --git a/conf/iskylims_apache_reverse_proxy.conf b/conf/iskylims_apache_reverse_proxy.conf deleted file mode 100644 index 1fa9ba98..00000000 --- a/conf/iskylims_apache_reverse_proxy.conf +++ /dev/null @@ -1,42 +0,0 @@ -# Apache reverse proxy config for iSkyLIMS production container - - - ServerName __ISKYLIMS_SERVER_NAME__ - - ProxyPreserveHost On - RequestHeader set X-Forwarded-Proto "__ISKYLIMS_FORWARDED_PROTO__" - RequestHeader set X-Forwarded-Port "__ISKYLIMS_FORWARDED_PORT__" - RequestHeader set X-Forwarded-Host "__ISKYLIMS_SERVER_NAME__" - - - # Allow larger POST bodies to reach Django instead of being rejected by - # Apache first. Adjust this value if your deployment needs larger uploads. - LimitRequestBody 52428800 - - - SecRequestBodyLimit 52428800 - SecRequestBodyNoFilesLimit 52428800 - - - ProxyPass /static ! - Alias /static __INSTALL_PATH__/static - - Require all granted - - - ProxyPass /documents ! - Alias /documents __INSTALL_PATH__/documents - - Require all granted - - - ProxyPass / http://app:__APP_PORT__/ - ProxyPassReverse / http://app:__APP_PORT__/ - ProxyTimeout __GUNICORN_TIMEOUT__ - TimeOut __GUNICORN_TIMEOUT__ - - CustomLog logs/__ISKYLIMS_LOG_NAME__-apache.access.log combined env=!forwarded - CustomLog logs/__ISKYLIMS_LOG_NAME__-apache.access.log proxy env=forwarded - ErrorLog logs/__ISKYLIMS_LOG_NAME__-apache.error.log - - diff --git a/conf/iskylims_apache_server-status.conf b/conf/iskylims_apache_server-status.conf deleted file mode 100644 index 7d096929..00000000 --- a/conf/iskylims_apache_server-status.conf +++ /dev/null @@ -1,14 +0,0 @@ -# ExtendedStatus controls whether Apache will generate "full" status -# information when the "server-status" handler is called. -ExtendedStatus On - - - ServerName __SERVER_STATUS_SERVER_NAME__ - ServerAlias __SERVER_STATUS_ALIASES__ - - SetHandler server-status - Order deny,allow - Deny from all - Allow from __SERVER_STATUS_ALLOW_FROM__ - - diff --git a/conf/iskylims_apache_ubuntu.conf b/conf/iskylims_apache_ubuntu.conf deleted file mode 100644 index 6c46b24a..00000000 --- a/conf/iskylims_apache_ubuntu.conf +++ /dev/null @@ -1,45 +0,0 @@ -## NOTE: WSGISocketPrefix,WSGIPythonHome, WSGIScriptAlias and WSGIPythonPath cannot occur within section - -# Path to serve your app at and wsgi script path - - - ServerAdmin bioinformatica@isciii.es - ServerName iSkyLIMS.isciiides.es - DocumentRoot /opt/iSkyLIMS - - - ## Load wsgi module with library associated in virtualenv - LoadModule wsgi_module "/opt/iSkyLIMS/virtualenv/lib/python3.9/site-packages/mod_wsgi/server/mod_wsgi-py39.cpython-39-x86_64-linux-gnu.so" - - WSGIDaemonProcess iSkyLIMS.isciiides.es python-home=/opt/iSkyLIMS/virtualenv python-path=/opt/iSkyLIMS - WSGIProcessGroup iSkyLIMS.isciiides.es - WSGIScriptAlias / /opt/iSkyLIMS/iSkyLIMS/wsgi.py - WSGIPassAuthorization On - WSGIApplicationGroup %{GLOBAL} - - # Directory piece. This ensures that apache can access wsgi.py script. - - - Satisfy Any - Allow from all - - - - Alias /static /opt/iSkyLIMS/static - - - Satisfy Any - Allow from all - - - Alias /documents /opt/iSkyLIMS/documents - - - Satisfy Any - Allow from all - - - ErrorLog logs/iSkyLIMS/iSkyLIMS.isciiides.es-apache.error.log - CustomLog logs/iSkyLIMS/iSkyLIMS.isciiides.es-apache.access.log combined - - diff --git a/conf/template_settings.py b/conf/template_settings.py new file mode 100644 index 00000000..51630e6f --- /dev/null +++ b/conf/template_settings.py @@ -0,0 +1,65 @@ +"""Django settings template rendered by the BU-ISCIII deployment library.""" + +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent +SECRET_KEY = "PLACEHOLDER" +DEBUG = False +ALLOWED_HOSTS = [host.strip() for host in "djangoallowedhosts".split(",") if host.strip()] + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "iskylims.urls" +WSGI_APPLICATION = "iskylims.wsgi.application" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + } +] + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": "djangodbname", + "USER": "djangouser", + "PASSWORD": "djangopass", + "HOST": "djangohost", + "PORT": "djangoport", + } +} + +EMAIL_HOST = "emailhostserver" +EMAIL_PORT = emailport +EMAIL_HOST_USER = "emailhostuser" +EMAIL_HOST_PASSWORD = "emailhostpassword" +EMAIL_USE_TLS = emailhosttls + +STATIC_URL = "/static/" +STATIC_ROOT = BASE_DIR / "static" +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/container_install.sh b/container_install.sh old mode 100644 new mode 100755 index d2005dea..777807c4 --- a/container_install.sh +++ b/container_install.sh @@ -1,624 +1,387 @@ -#!/usr/bin/bash +#!/usr/bin/env bash +set -euo pipefail -container_install_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 -source "$container_install_script_dir/deployment/lib/container/common.sh" +source "$script_dir/deployment/lib/container/common.sh" # shellcheck disable=SC1091 -source "$container_install_script_dir/deployment/lib/container/django.sh" - -ISKYLIMS_VERSION="3.1.1" - -# Print this application's stable outer-installer CLI and examples. -usage() { -cat << EOF -This script installs and upgrades the iskylims app. - -Usage : $0 [--demo_data] [--git_revision] [--compose_file] [--install_conf] [--action] [--script] [--script_before] [--script_after] [--engine] [--test] - Optional input data: - --demo_data | Provide already downloaded demo data from Zenodo - --git_revision | Specify the Git revision to install (default: main, or 'current' to use copied local sources) - --compose_file | Compose file to use (overrides default) - --install_conf | Single production settings file (host rendering, ephemeral build secret, and bootstrap) - --install_conf_map | Service-specific settings file: service,path (can be repeated) - --action | install (default), upgrade, or fix-permissions - --script | Run a Django migration script after migrations (can be repeated) - --script_before | Run a Django migration script before migrations (can be repeated) - --script_after | Run a Django migration script after migrations (can be repeated) - --skip_demo_data | Skip downloading/copying demo data to samba container - --skip_test_data | Skip loading test fixtures (test/test_data.json) - --engine | Container engine to use: docker (default) or podman - --test | Use development/test compose file and sample data - -Examples: - Deploy production container pointing to an external DB/Samba: - bash $0 --install_conf conf/my_prod_settings_iskylims.txt - - Deploy production with service-specific settings mapping: - bash $0 --install_conf_map app,conf/docker_production_settings.txt - - Upgrade an existing production deployment using the same database: - bash $0 --install_conf conf/my_prod_settings_iskylims.txt --action upgrade - - Repair production bind mount and volume permissions without rebuilding or bootstrapping: - bash $0 --install_conf conf/my_prod_settings_iskylims.txt --action fix-permissions - - Install demo container system with local services - bash $0 --test - - Install test stack from current local committed sources without checking out a branch in-container - bash $0 --test --git_revision current - - Provide already downloaded data from Zenodo (compressed) for test environment - bash $0 --demo_data /path/to/iskylims_demo_data.tar.gz - -EOF +source "$script_dir/deployment/lib/container/django.sh" + +APP_VERSION="0.1.0" +APPLICATION_NAME="iSkyLIMS" + +# ============================================================================ +# GENERATED SERVICE/ADD-ON CUSTOMIZATION +# Regenerate these callbacks from the descriptor; keep application-neutral +# lifecycle mechanics below unchanged. +# ============================================================================ +install_services=(app) +permission_services=(app apache) +configured_services=(app) + +default_service_install_conf() { + case "$1" in + app) [ "$mode" = test ] && echo conf/docker_test_settings.txt || echo conf/docker_production_settings.txt ;; + *) return 1 ;; + esac } - -# translate long options to short -reset=true - -for arg in "$@" -do - if [ -n "$reset" ]; then - unset reset - set -- # this resets the "$@" array so we can rebuild it - fi - case "$arg" in - # OPTIONAL - --demo_data) set -- "$@" -d ;; - --git_revision) set -- "$@" -g ;; - --compose_file) set -- "$@" -c ;; - --install_conf) set -- "$@" -s ;; - --install_conf_map) set -- "$@" -j ;; - --action) set -- "$@" -a ;; - --script) set -- "$@" -m ;; - --script_before) set -- "$@" -b ;; - --script_after) set -- "$@" -f ;; - --skip_demo_data) set -- "$@" -n ;; - --skip_test_data) set -- "$@" -t ;; - --test) set -- "$@" -p ;; - --engine) set -- "$@" -e ;; - - # ADDITIONAL - --help) set -- "$@" -h ;; - --version) set -- "$@" -v ;; - # PASSING VALUE IN PARAMETER - *) set -- "$@" "$arg" ;; +service_build_context_dir() { + case "$1" in + app) echo . ;; + *) return 1 ;; esac -done - -# SETTING DEFAULT VALUES -demo_data=false -git_revision="main" -compose_file="" -install_conf="" -install_conf_container="" -install_conf_map_entries=() -skip_demo_data="" -skip_test_data="" -mode="production" -action="install" -run_script=false -run_script_before=false -migration_script=() -migration_script_before=() -engine="docker" - -# Supply iSkyLIMS Apache tokens. Token rendering and host-file installation are -# shared, but the set of virtual-host values is application-specific. -render_apache_config() { - local src="$1" - local dst="$2" - render_config_template "$src" "$dst" 0664 \ - __ISKYLIMS_SERVER_NAME__ "$apache_server_name" \ - __ISKYLIMS_LOG_NAME__ "$apache_log_name" \ - __INSTALL_PATH__ "$install_path" \ - __APP_PORT__ "$app_port" \ - __ISKYLIMS_FORWARDED_PROTO__ "$apache_forwarded_proto" \ - __ISKYLIMS_FORWARDED_PORT__ "$apache_forwarded_port" \ - __GUNICORN_TIMEOUT__ "$gunicorn_timeout" \ - __SERVER_STATUS_SERVER_NAME__ "$apache_status_server_name" \ - __SERVER_STATUS_ALIASES__ "$apache_status_aliases" \ - __SERVER_STATUS_ALLOW_FROM__ "$apache_status_allow_from" } - -# PARSE VARIABLE ARGUMENTS WITH getopts -options=":d:g:c:s:j:a:m:b:f:e:vhntp" -while getopts $options opt; do - case $opt in - d) - demo_data=$OPTARG - ;; - g) - git_revision=$OPTARG - ;; - c) - compose_file=$OPTARG - ;; - s) - install_conf=$OPTARG - ;; - j) - install_conf_map_entries+=("$OPTARG") - ;; - a) - action=$OPTARG - if [[ "$action" != "install" && "$action" != "upgrade" && "$action" != "fix-permissions" ]]; then - echo "Invalid action '$action'. Use install, upgrade, or fix-permissions." - exit 1 - fi - ;; - m) - run_script=true - migration_script+=("$OPTARG") - ;; - b) - run_script_before=true - migration_script_before+=("$OPTARG") - ;; - e) - engine=$OPTARG - if [[ "$engine" != "docker" && "$engine" != "podman" ]]; then - echo "Invalid engine '$engine'. Use docker or podman." - exit 1 - fi - ;; - f) - run_script=true - migration_script+=("$OPTARG") - ;; - n) - skip_demo_data=true - ;; - t) - skip_test_data=true - ;; - p) - mode="test" - ;; - h) - usage - exit 1 - ;; - v) - echo $ISKYLIMS_VERSION - exit 1 - ;; - \?) - echo "Invalid Option: -$OPTARG" 1>&2 - usage - exit 1 - ;; - : ) - echo "Option -$OPTARG requires an argument." >&2 - exit 1 - ;; - * ) - echo "Unimplemented option: -$OPTARG" >&2; - exit 1 - ;; +service_environment_prefix() { + case "$1" in + app) echo APP ;; + *) return 1 ;; esac -done -shift $((OPTIND-1)) - -if [ "$mode" = "test" ]; then - if [ -z "$compose_file" ]; then - compose_file="docker-compose.test.yml" - fi -else - if [ -z "$compose_file" ]; then - compose_file="docker-compose.prod.yml" - fi -fi - -app_service="${APP_SERVICE:-app}" -selected_install_conf="$install_conf" -for map_entry in "${install_conf_map_entries[@]}"; do - svc_name="${map_entry%%,*}" - conf_name="${map_entry#*,}" - if [ -z "$svc_name" ] || [ -z "$conf_name" ] || [ "$svc_name" = "$map_entry" ]; then - echo "Invalid --install_conf_map value '$map_entry'. Expected format: service,path" - exit 1 - fi - if [ "$svc_name" != "app" ]; then - echo "Unknown service '$svc_name' in --install_conf_map. Valid service: app" - exit 1 - fi - selected_install_conf="$conf_name" -done - -if [ "$mode" = "test" ] && [ -z "$selected_install_conf" ]; then - selected_install_conf="conf/docker_test_settings.txt" -fi -install_conf="$selected_install_conf" - -if [ "$mode" = "production" ] && [ -z "$install_conf" ]; then - echo "Production deployments require --install_conf or --install_conf_map app,." - exit 1 -fi - -if [ -z "$skip_demo_data" ]; then - if [ "$mode" = "test" ]; then - skip_demo_data=false - else - skip_demo_data=true - fi -fi - -if [ -z "$skip_test_data" ]; then - if [ "$mode" = "test" ]; then - skip_test_data=false - else - skip_test_data=true - fi -fi - -if [ "$action" = "upgrade" ]; then - skip_demo_data=true - skip_test_data=true -fi - -require_compose_file "$compose_file" || exit 1 - -repo_root="$(pwd)" -build_context_dir="$repo_root" -temp_install_conf="" -host_install_conf_path="" -prepare_install_configuration \ - "$install_conf" "$repo_root" "$build_context_dir" app "$mode" \ - host_install_conf_path install_conf_container temp_install_conf || exit 1 -install_conf="$host_install_conf_path" -if [ -n "$temp_install_conf" ]; then - # EXIT traps need a no-argument callback; deletion itself is shared. - cleanup_temp_conf() { cleanup_files "$temp_install_conf"; } - trap cleanup_temp_conf EXIT -fi - -# Write this stack's complete Compose interpolation contract. The keys remain -# application-owned because they must match its production Compose file. -write_compose_env_file() { - if [ "$mode" != "production" ]; then - return 0 - fi - - cat > "$compose_env_file" << EOF -# Generated by container_install.sh from $install_conf_container. -# Used by Docker Compose/Podman Compose for docker-compose.prod.yml interpolation. -INSTALL_TYPE=dep -GIT_REVISION=$git_revision -INSTALL_CONF=$install_conf_container -# Compose reads this host path to create an ephemeral build-secret mount. The -# value is used by Compose itself and is not stored in the resulting image. -INSTALL_CONF_HOST=$host_install_conf_path -INSTALL_PATH=$install_path -APACHE_CONF_PATH=$apache_conf_path -DJANGO_SETTINGS_PATH=$django_settings_path -APP_UID=$app_uid -APP_GID=$app_gid -APP_SHELL=$app_shell -APP_PORT=$app_port -DJANGO_DEBUG=$django_debug -DB_CONN_MAX_AGE=$db_conn_max_age -WEB_CONCURRENCY=$web_concurrency -GUNICORN_THREADS=$gunicorn_threads -GUNICORN_TIMEOUT=$gunicorn_timeout -GUNICORN_KEEPALIVE=$gunicorn_keepalive -SERVER_STATUS_SERVER_NAME=$apache_status_server_name -SERVER_STATUS_ALIASES=$apache_status_aliases -SERVER_STATUS_ALLOW_FROM=$apache_status_allow_from -APACHE_FORWARDED_PROTO=$apache_forwarded_proto -APACHE_FORWARDED_PORT=$apache_forwarded_port -EOF - - echo "Wrote Compose environment file: $compose_env_file" } - -set_engine - -app_repo_path="${APP_REPO_PATH:-/srv/iskylims}" -config_install_path="$(read_install_conf_value "INSTALL_PATH" "$host_install_conf_path")" -install_path="${config_install_path:-/opt/iskylims}" -config_apache_conf_path="$(read_install_conf_value "APACHE_CONF_PATH" "$host_install_conf_path")" -apache_conf_path="${APACHE_CONF_PATH:-${config_apache_conf_path:-}}" -if [ -z "$apache_conf_path" ]; then - apache_conf_path="$install_path/conf" -fi -config_django_settings_path="$(read_install_conf_value "DJANGO_SETTINGS_PATH" "$host_install_conf_path")" -django_settings_path="$(normalize_bind_file_path "${DJANGO_SETTINGS_PATH:-${config_django_settings_path:-}}" "$install_path/iskylims/settings.py" settings.py)" -app_uid="$(config_value_or_default APP_UID "$host_install_conf_path" 1212)" -app_gid="$(config_value_or_default APP_GID "$host_install_conf_path" 1212)" -app_shell="$(config_value_or_default APP_SHELL "$host_install_conf_path" /sbin/nologin)" -app_port="$(config_value_or_default APP_PORT "$host_install_conf_path" 8001)" -django_debug="$(config_value_or_default DJANGO_DEBUG "$host_install_conf_path" false)" -db_conn_max_age="$(config_value_or_default DB_CONN_MAX_AGE "$host_install_conf_path" 60)" -web_concurrency="$(config_value_or_default WEB_CONCURRENCY "$host_install_conf_path" 2)" -gunicorn_threads="$(config_value_or_default GUNICORN_THREADS "$host_install_conf_path" 2)" -gunicorn_timeout="$(config_value_or_default GUNICORN_TIMEOUT "$host_install_conf_path" 300)" -gunicorn_keepalive="$(config_value_or_default GUNICORN_KEEPALIVE "$host_install_conf_path" 5)" -config_dns_url="$(read_install_conf_value "DNS_URL" "$host_install_conf_path")" -apache_server_name="$(normalize_apache_server_name "${APACHE_SERVER_NAME:-${config_dns_url:-localhost}}")" -apache_log_name="$(printf '%s' "$apache_server_name" | tr -c 'A-Za-z0-9._-' '_' | sed 's/_$//')" -apache_status_server_name="$(normalize_apache_server_name "$(config_value_or_default SERVER_STATUS_SERVER_NAME "$host_install_conf_path" "$apache_server_name")")" -apache_status_aliases="$(config_value_or_default SERVER_STATUS_ALIASES "$host_install_conf_path" "127.0.0.1 localhost")" -apache_status_allow_from="$(config_value_or_default SERVER_STATUS_ALLOW_FROM "$host_install_conf_path" "127.0.0.1 localhost")" -apache_forwarded_proto="$(config_value_or_default APACHE_FORWARDED_PROTO "$host_install_conf_path" https)" -apache_forwarded_port="$(config_value_or_default APACHE_FORWARDED_PORT "$host_install_conf_path" 443)" -compose_env_file="$repo_root/.env.prod.file" -app_container="" -if [ "$mode" = "production" ]; then - app_image_name="${APP_IMAGE_NAME:-relecov-iskylims:local}" -else - app_image_name="${APP_IMAGE_NAME:-iskylims_app}" -fi - -# Map services to this stack's explicit legacy container names. -service_container_name() { - local service_name="$1" - if [ "$mode" = "test" ]; then - case "$service_name" in - db) echo "db" ;; - samba) echo "samba" ;; - app) echo "iskylims_app" ;; - *) echo "" ;; - esac - else - case "$service_name" in - app) echo "iskylims_app" ;; - samba) echo "samba" ;; - *) echo "" ;; - esac - fi +service_environment_value() { + local prefix variable + prefix="$(service_environment_prefix "$1")" || return 1 + variable="${prefix}_$2" + printf '%s\n' "${!variable:-${3:-}}" } - -# Repair iSkyLIMS-specific writable directories inside its mounted app volume. -prepare_app_mount_permissions() { - if [ "$mode" != "production" ]; then - return 0 - fi - - echo "Preparing writable app mount permissions..." - engine_exec exec --user 0 "$app_container" sh -lc " - set -e - mkdir -p '$install_path/logs' '$install_path/static' '$install_path/documents' '$install_path/cron' '$install_path/tmp' - chown -R '$app_uid:$app_gid' '$install_path/logs' '$install_path/static' '$install_path/documents' '$install_path/cron' '$install_path/tmp' - chmod -R u+rwX,g+rwX '$install_path/logs' '$install_path/static' '$install_path/documents' - chmod 700 '$install_path/cron' '$install_path/tmp' - chmod -R o+rX '$install_path/static' - " - prepare_django_container_settings_permissions \ - "$app_container" "$install_path/iskylims/settings.py" "$app_uid" "$app_gid" +service_repo_path() { + case "$1" in + app) service_environment_value "$1" REPO_PATH /srv/iskylims ;; + *) return 1 ;; + esac +} +service_install_path() { + case "$1" in + app) service_environment_value "$1" INSTALL_PATH /opt/iskylims ;; + *) return 1 ;; + esac +} +service_readiness_path() { + case "$1" in + app) echo "$(service_install_path "$1")/manage.py" ;; + *) return 1 ;; + esac +} +service_image_name() { + case "$1" in + app) echo relecov-iskylims:local ;; + *) return 1 ;; + esac +} +service_profile() { + case "$1" in + app) echo django ;; + *) return 1 ;; + esac +} +service_dockerfile() { + case "$1" in + app) echo Dockerfile ;; + *) return 1 ;; + esac +} +service_container_install_conf() { + case "$1" in + app) echo conf/.runtime_install_settings.txt ;; + *) return 1 ;; + esac +} +service_uid() { + case "$1" in + app) service_environment_value "$1" APP_UID 1212 ;; + *) return 1 ;; + esac +} +service_gid() { + case "$1" in + app) service_environment_value "$1" APP_GID 1212 ;; + *) return 1 ;; + esac } -# Apply ownership required by iSkyLIMS and the UBI Apache runtime on host binds. -prepare_host_bind_mount_permissions() { - if [ "$mode" != "production" ]; then - return 0 - fi - - local django_settings_dir - local -a host_permission_spec - - echo "Preparing host bind mount permissions..." - host_permission_spec=( - "$apache_conf_path|-|0755" - "/var/log/local/relecov-iskylims/apps|$app_uid:$app_gid|0775" - # UBI httpd runs as uid 1001 and group 0. This keeps the Apache log bind - # writable without relying on Podman's :U ownership mutation. - "/var/log/local/relecov-iskylims/apache|1001:0|0775" - "$apache_conf_path/iskylims_apache_reverse_proxy.conf|-|0664" - "$apache_conf_path/iskylims_apache_logs.conf|-|0664" - "$apache_conf_path/iskylims_apache_server-status.conf|-|0664" +prepare_compose_environment() { + local -a settings_sources=( + "APP|${install_conf_host_by_service[app]}" ) + local -a deployment_values=( + "GIT_REVISION|$git_revision" + "APP_IMAGE|relecov-iskylims:local" + "APP_INSTALL_CONF_PATH|${install_conf_host_by_service[app]}" + "APACHE_LOG_PATH|$(config_value_or_default APACHE_LOG_PATH "${install_conf_host_by_service[app]}" /var/log/local/iskylims/apache)" + "APACHE_BIND_HOST|$(config_value_or_default APACHE_BIND_HOST "${install_conf_host_by_service[app]}" 0.0.0.0)" + "APACHE_PORT|$(config_value_or_default APACHE_PORT "${install_conf_host_by_service[app]}" 80)" + "APACHE_FORWARDED_PROTO|$(config_value_or_default APACHE_FORWARDED_PROTO "${install_conf_host_by_service[app]}" https)" + "APACHE_FORWARDED_PORT|$(config_value_or_default APACHE_FORWARDED_PORT "${install_conf_host_by_service[app]}" 443)" + "APACHE_LIMIT_REQUEST_BODY|$(config_value_or_default APACHE_LIMIT_REQUEST_BODY "${install_conf_host_by_service[app]}" 52428800)" + ) + compose_env_file="$script_dir/.env.${mode}.file" + write_compose_environment_file "$compose_env_file" settings_sources deployment_values +} - if [ -f "$django_settings_path" ]; then - django_settings_dir="$(dirname "$django_settings_path")" - host_permission_spec+=( - "$django_settings_dir|-|0755" - "$django_settings_path|$app_uid:$app_gid|0664" - ) - fi - - apply_host_permission_spec "${host_permission_spec[@]}" +# Every project uses the generated interpolation file in both modes because it +# combines service-specific settings sources with collision-safe prefixes. +deployment_compose() { + compose_exec --env-file "$compose_env_file" "$@" +} +current_service_container() { + deployment_compose -f "$compose_file" ps -q "$1" | tail -n 1 } -if [ "$action" = "fix-permissions" ]; then - echo "Repairing production container bind mount and volume permissions..." - if ! mkdir -p "$apache_conf_path" "/var/log/local/relecov-iskylims/apache" "/var/log/local/relecov-iskylims/apps"; then - echo "Error: unable to create required host bind/log directories. Check APACHE_CONF_PATH and log directory permissions." >&2 - exit 1 +# Each Django service renders its own protected host settings bind. React +# services and add-ons have no Django settings source. +prepare_application_host_sources() { + local settings_output + if [ "$mode" = production ]; then + settings_output="$(service_environment_value app DJANGO_SETTINGS_PATH)" + [ -n "$settings_output" ] || { echo "DJANGO_SETTINGS_PATH is required for app" >&2; return 1; } + mkdir -p "$(dirname "$settings_output")" + prepare_django_settings_bind_mount ./conf/template_settings.py "$settings_output" "${install_conf_host_by_service[app]}" fi - prepare_django_settings_bind_mount "$repo_root/conf/template_settings.txt" "$django_settings_path" "$host_install_conf_path" - prepare_host_bind_mount_permissions - write_compose_env_file - if app_container="$(resolve_service_container "$app_service" 2>/dev/null)" \ - && [ "$(engine_exec inspect -f '{{.State.Running}}' "$app_container" 2>/dev/null)" = "true" ]; then - prepare_app_mount_permissions - echo "Done repairing host bind mounts and mounted app volumes." - else - echo "Host bind mount permissions repaired." - echo "The app container is not running, so named volumes were not repaired." - echo "Start containers with Compose, then rerun this action to repair mounted app volumes." + if [ "$mode" = production ]; then + apache_log_path="${APACHE_LOG_PATH:-/var/log/local/iskylims/apache}" + mkdir -p "$script_dir/deployment/apache" "$apache_log_path" fi - exit 0 -fi - -expected_head_hash="$(repository_revision "$repo_root" full)" || true -expected_head_short="$(repository_revision "$repo_root" short)" || true -image_id_before_build="$(compose_service_image_id "$compose_file" "$app_service")" -print_prebuild_diagnostics \ - "Pre-build diagnostics" "$repo_root" "$git_revision" "$image_id_before_build" -echo "Deploying containers (compose file: $compose_file) with a pre-staged app image and GIT_REVISION=$git_revision..." -if ! mkdir -p "$apache_conf_path" "/var/log/local/relecov-iskylims/apache" "/var/log/local/relecov-iskylims/apps"; then - echo "Error: unable to create required host bind/log directories. Check APACHE_CONF_PATH and log directory permissions." >&2 - exit 1 -fi -prepare_django_settings_bind_mount "$repo_root/conf/template_settings.txt" "$django_settings_path" "$host_install_conf_path" -if [ -f "$repo_root/conf/iskylims_apache_reverse_proxy.conf" ]; then - render_apache_config \ - "$repo_root/conf/iskylims_apache_reverse_proxy.conf" \ - "$apache_conf_path/iskylims_apache_reverse_proxy.conf" -fi -if [ -f "$repo_root/conf/iskylims_apache_logs.conf" ]; then - render_apache_config \ - "$repo_root/conf/iskylims_apache_logs.conf" \ - "$apache_conf_path/iskylims_apache_logs.conf" -fi -if [ -f "$repo_root/conf/iskylims_apache_server-status.conf" ]; then - render_apache_config \ - "$repo_root/conf/iskylims_apache_server-status.conf" \ - "$apache_conf_path/iskylims_apache_server-status.conf" -fi -prepare_host_bind_mount_permissions -write_compose_env_file -validate_compose_configuration "$compose_file" || exit 1 -if [ "$mode" = "production" ]; then - # Use the engine directly because older Compose schemas cannot express - # build secrets. The explicit tag matches docker-compose.prod.yml. - engine_build --no-cache \ - --secret "id=install_conf,src=$host_install_conf_path" \ - --build-arg INSTALL_TYPE="dep" \ - --build-arg GIT_REVISION="$git_revision" \ - --build-arg INSTALL_CONF="$install_conf_container" \ - --build-arg USE_INSTALL_CONF_SECRET="true" \ - --build-arg RENDER_DJANGO_SETTINGS="false" \ - --build-arg INSTALL_PATH="$install_path" \ - --build-arg APP_UID="$app_uid" \ - --build-arg APP_GID="$app_gid" \ - --build-arg APP_SHELL="$app_shell" \ - --tag "$app_image_name" \ - "$build_context_dir" -else - INSTALL_TYPE="dep" GIT_REVISION="$git_revision" INSTALL_CONF="$install_conf_container" INSTALL_PATH="$install_path" \ - compose_with_env_exec -f "$compose_file" build --no-cache -fi -image_id_after_build="$(compose_service_image_id "$compose_file" "$app_service")" -print_image_after_diagnostics \ - "Image diagnostics after build:" "$image_id_before_build" "$image_id_after_build" -INSTALL_PATH="$install_path" APACHE_CONF_PATH="$apache_conf_path" DJANGO_SETTINGS_PATH="$django_settings_path" APP_UID="$app_uid" APP_GID="$app_gid" APP_SHELL="$app_shell" APP_PORT="$app_port" DJANGO_DEBUG="$django_debug" DB_CONN_MAX_AGE="$db_conn_max_age" WEB_CONCURRENCY="$web_concurrency" GUNICORN_THREADS="$gunicorn_threads" GUNICORN_TIMEOUT="$gunicorn_timeout" GUNICORN_KEEPALIVE="$gunicorn_keepalive" compose_with_env_exec -f "$compose_file" up -d - -echo "Waiting 20 seconds for starting database and web services..." -sleep 20 -app_container="$(resolve_service_container "$app_service")" || exit 1 -ensure_service_running "$app_service" "$app_container" >/dev/null -print_container_repository_diagnostics \ - "Container diagnostics after startup:" "$app_container" "$app_repo_path" \ - "$expected_head_hash" "$expected_head_short" -prepare_app_mount_permissions - -container_install_conf_path="$install_conf_container" -if [[ "$container_install_conf_path" != /* ]]; then - container_install_conf_path="$app_repo_path/$container_install_conf_path" -fi +} -echo "Staging protected runtime configuration at $container_install_conf_path" -stage_container_runtime_config \ - "$app_container" "$host_install_conf_path" "$container_install_conf_path" \ - "$app_uid" "$app_gid" || exit 1 +# Keep one independently reviewable host permission specification per +# application and per selected add-on. Empty add-on specs are intentional until +# that add-on declares writable bind sources. +prepare_host_bind_source_permissions() { + [ "$mode" = production ] || return 0 + local data_path log_path settings_path uid gid + data_path="$(service_environment_value app HOST_DATA_PATH)" + log_path="$(service_environment_value app HOST_LOG_PATH)" + settings_path="$(service_environment_value app DJANGO_SETTINGS_PATH)" + [ -n "$data_path" ] || { echo "HOST_DATA_PATH is required for app" >&2; return 1; } + [ -n "$log_path" ] || { echo "HOST_LOG_PATH is required for app" >&2; return 1; } + [ -n "$settings_path" ] || { echo "DJANGO_SETTINGS_PATH is required for app" >&2; return 1; } + uid="$(service_uid app)"; gid="$(service_gid app)" + local -a app_host_bind_permission_spec=( + "$data_path/documents|$uid:$gid|0775" + "$log_path|$uid:$gid|0775" + "$(dirname "$settings_path")|-|0755" + "$settings_path|$uid:$gid|0664" + ) + apply_host_permission_spec "${app_host_bind_permission_spec[@]}" + apache_log_path="${APACHE_LOG_PATH:-/var/log/local/iskylims/apache}" + local -a apache_host_bind_permission_spec=( + "$script_dir/deployment/apache|-|0755" + "$script_dir/deployment/apache/00-logs.conf|-|0644" + "$script_dir/deployment/apache/01-reverse-proxy.conf|-|0644" + "$script_dir/deployment/apache/02-server-status.conf|-|0644" + "$apache_log_path|-|0775" + ) + apply_host_permission_spec "${apache_host_bind_permission_spec[@]}" +} -script_args_before="" -if [ "$run_script_before" = true ]; then - for val in "${migration_script_before[@]}"; do - script_args_before+=" --script_before $(printf '%q' "$val")" - done -fi +# Keep a separate running-mount specification in every service/add-on case. +prepare_running_container_mount_permissions() { + local service_name="$1" container_id="$2" + local install_path uid gid + case "$service_name" in + app) + install_path="$(service_install_path "$service_name")" + uid="$(service_uid "$service_name")"; gid="$(service_gid "$service_name")" + local -a app_running_mount_permission_spec=( + "$install_path/logs|$uid:$gid|u+rwX,g+rwX" + "$install_path/documents|$uid:$gid|u+rwX,g+rwX" + "$install_path/static|$uid:$gid|u+rwX,g+rwX,o+rX" + ) + apply_container_directory_permission_spec "$container_id" "${app_running_mount_permission_spec[@]}" + prepare_django_container_settings_permissions "$container_id" "$install_path/iskylims/settings.py" "$uid" "$gid" + ;; + apache) + local -a apache_running_mount_permission_spec=() + apply_container_directory_permission_spec "$container_id" "${apache_running_mount_permission_spec[@]}" + ;; + *) return 0 ;; + esac +} -script_args_after="" -if [ "$run_script" = true ]; then - for val in "${migration_script[@]}"; do - script_args_after+=" --script_after $(printf '%q' "$val")" - done -fi +bootstrap_service() { + local service_name="$1" container_id="$2" deployment_action="$3" + local repo_path runtime_conf uid gid status + local -a args + case "$service_name" in + app) + repo_path="$(service_repo_path "$service_name")" + runtime_conf=conf/.runtime_install_settings.txt + [[ "$runtime_conf" == /* ]] || runtime_conf="$repo_path/$runtime_conf" + uid="$(service_uid "$service_name")"; gid="$(service_gid "$service_name")" + stage_container_runtime_config "$container_id" "${install_conf_host_by_service[$service_name]}" "$runtime_conf" "$uid" "$gid" + args=(--bootstrap "$deployment_action" --git_revision "$git_revision" --conf "$runtime_conf" --skip_apache_restart) + for hook in "${migration_script_before[@]}"; do args+=(--script_before "$hook"); done + for hook in "${migration_script_after[@]}"; do args+=(--script_after "$hook"); done + status=0; engine_exec exec "$container_id" bash "$repo_path/install.sh" "${args[@]}" || status=$? + [ "$mode" = test ] || remove_container_runtime_config "$container_id" "$runtime_conf" || true + return "$status" + ;; + *) return 0 ;; + esac +} -if [ "$action" = "upgrade" ]; then - echo "Running install.sh bootstrap inside the container (upgrade mode)" - engine_exec exec -it "$app_container" bash -c "cd $app_repo_path && bash install.sh --bootstrap upgrade --git_revision \"$git_revision\" --conf \"$install_conf_container\" --tables --skip_apache_restart$script_args_before$script_args_after" -else - echo "Running install.sh bootstrap inside the container (install mode)" - engine_exec exec -it "$app_container" bash -c "cd $app_repo_path && bash install.sh --bootstrap install --git_revision \"$git_revision\" --conf \"$install_conf_container\" --skip_apache_restart$script_args_before$script_args_after" -fi +action="install"; mode="production"; engine="docker"; git_revision="current" +install_conf=""; compose_file=""; compose_env_file="" +install_conf_map_entries=(); migration_script_before=(); migration_script_after=() +demo_data=""; skip_demo_data=""; skip_test_data="" -# Bootstrap consumes a temporary runtime copy. Remove that exact file so the -# operator configuration remains only on the protected host after deployment. -if [ "$mode" = "production" ]; then - remove_container_runtime_config \ - "$app_container" "$container_install_conf_path" || exit 1 -fi +usage() { + cat <<'EOF' +Install, upgrade, or repair the application deployment. + +Options: + --action install|upgrade|fix-permissions + --test + --engine docker|podman + --git_revision + --install_conf First application service only. + --install_conf_map Repeat for every service override. + --compose_file + --script_before + --script_after + --script + --demo_data + --skip_demo_data + --skip_test_data + --help + --version +EOF +} +die() { echo "ERROR: $*" >&2; exit 1; } + +# 1. Parse the canonical outer-installer interface. +while (($#)); do + case "$1" in + --action) action="${2:-}"; shift 2 ;; + --test) mode="test"; shift ;; + --engine) engine="${2:-}"; shift 2 ;; + --git_revision) git_revision="${2:-}"; shift 2 ;; + --install_conf) install_conf="${2:-}"; shift 2 ;; + --install_conf_map) install_conf_map_entries+=("${2:-}"); shift 2 ;; + --compose_file) compose_file="${2:-}"; shift 2 ;; + --script_before) migration_script_before+=("${2:-}"); shift 2 ;; + --script_after|--script) migration_script_after+=("${2:-}"); shift 2 ;; + --demo_data) demo_data="${2:-}"; shift 2 ;; + --skip_demo_data) skip_demo_data=true; shift ;; + --skip_test_data) skip_test_data=true; shift ;; + --help) usage; exit 0 ;; + --version) echo "$APP_VERSION"; exit 0 ;; + *) die "Unknown option: $1" ;; + esac +done -print_container_repository_diagnostics \ - "Container diagnostics after bootstrap:" "$app_container" "$app_repo_path" \ - "$expected_head_hash" "$expected_head_short" +# 2. Validate arguments before modifying deployment state. +[[ "$action" =~ ^(install|upgrade|fix-permissions)$ ]] || die "Invalid action: $action" +[[ "$engine" =~ ^(docker|podman)$ ]] || die "Invalid engine: $engine" +[ -z "$demo_data" ] || die "--demo_data requires an application-owned deployment hook" -if ! engine_exec exec -it "$app_container" test -f "$install_path/manage.py"; then - echo "Error: $install_path/manage.py not found after bootstrap. Showing logs:" - engine_exec logs --tail 200 "$app_container" - exit 1 -fi +# 3. Resolve one protected configuration source per configured component. +cd "$script_dir" +declare -A install_conf_host_by_service=() +for service_name in "${configured_services[@]}"; do + install_conf_host_by_service["$service_name"]="$(default_service_install_conf "$service_name")" +done +if [ -n "$install_conf" ]; then install_conf_host_by_service["${install_services[0]}"]="$install_conf"; fi +for mapping in "${install_conf_map_entries[@]}"; do + [[ "$mapping" == *,* ]] || die "Invalid --install_conf_map: $mapping" + service_name="${mapping%%,*}"; path="${mapping#*,}" + array_contains "$service_name" "${configured_services[@]}" || die "Unknown mapped service: $service_name" + install_conf_host_by_service["$service_name"]="$path" +done +for service_name in "${configured_services[@]}"; do + path="${install_conf_host_by_service[$service_name]}" + [[ "$path" = /* ]] || path="$script_dir/$path" + [ -f "$path" ] || die "Configuration for $service_name not found: $path" + if [ "$mode" = production ] \ + && grep -Eq '^[A-Z0-9_]+=.*CHANGE_ME' "$path"; then + die "Production configuration for $service_name contains CHANGE_ME: $path" + fi + install_conf_host_by_service["$service_name"]="$(cd "$(dirname "$path")" && pwd)/$(basename "$path")" +done -if [ "$skip_test_data" = false ]; then - engine_exec exec -it "$app_container" python3 manage.py loaddata test/test_data.json - engine_exec exec -it "$app_container" python3 manage.py shell -c " -from django.contrib.auth.models import Group, User -admin = User.objects.get(username='admin') -admin.groups.add( - Group.objects.get(name='WetlabManager'), - Group.objects.get(name='ServiceManager'), -) -print('admin groups:', list(admin.groups.values_list('name', flat=True))) -" -else - echo "Skipping test data fixtures as requested" +# 4. Select the engine, prepare host sources and validate the final Compose model. +set_engine "$engine" +compose_file="${compose_file:-docker-compose.$([ "$mode" = test ] && echo test || echo prod).yml}" +require_compose_file "$compose_file" +prepare_compose_environment +load_compose_environment_file "$compose_env_file" +prepare_application_host_sources +prepare_host_bind_source_permissions +deployment_compose -f "$compose_file" config --quiet \ + || die "Compose configuration validation failed: $compose_file" + +# 5. Dispatch permission-only repair without building or bootstrapping. +if [ "$action" = fix-permissions ]; then + for service_name in "${permission_services[@]}"; do + container_id="$(current_service_container "$service_name" 2>/dev/null || true)" + [ -z "$container_id" ] || prepare_running_container_mount_permissions "$service_name" "$container_id" + done + echo "Permissions repaired without build or bootstrap." + exit 0 fi -if [ "$skip_demo_data" = false ] && service_exists "samba"; then - echo "Downloading and copying test files to the Samba container" - if [ "$demo_data" == "false" ]; then - wget https://zenodo.org/record/8091169/files/iskylims_demo_data.tar.gz - demo_data="./iskylims_demo_data.tar.gz" +# 6. Build application services in declared order. Production builds use the +# engine directly: Django receives its settings as +# an ephemeral build secret, while React receives only its public VITE value. +# This avoids requiring Compose implementations to support build.secrets. +for service_name in "${install_services[@]}"; do + if [ "$mode" = test ]; then + deployment_compose -f "$compose_file" build --no-cache "$service_name" + continue fi - engine_exec cp "$demo_data" samba:/mnt - engine_exec exec -it samba tar -xf /mnt/iskylims_demo_data.tar.gz -C /mnt - # Ensure extracted demo data can be traversed/read through SMB by non-owner users. - engine_exec exec -it samba sh -lc ' - for root in /mnt/test_ngs_data /mnt/Runs; do - if [ -d "$root" ]; then - find "$root" -type d -exec chmod o+rx {} + - find "$root" -type f -exec chmod o+r {} + - fi - done - ' - - echo "Deleting compressed test file" - engine_exec exec -it samba rm /mnt/iskylims_demo_data.tar.gz - - if [ "$demo_data" == "false" ]; then - rm -f "$demo_data" + context="$(service_build_context_dir "$service_name")" + dockerfile="$(service_dockerfile "$service_name")" + profile="$(service_profile "$service_name")" + if [ "$profile" = django ]; then + engine_build --no-cache --file "$context/$dockerfile" \ + --secret "id=install_conf,src=${install_conf_host_by_service[$service_name]}" \ + --build-arg GIT_REVISION="$git_revision" \ + --build-arg INSTALL_CONF="$(service_container_install_conf "$service_name")" \ + --build-arg USE_INSTALL_CONF_SECRET=true \ + --build-arg RENDER_DJANGO_SETTINGS=false \ + --build-arg APP_REPO_PATH="$(service_repo_path "$service_name")" \ + --build-arg APP_INSTALL_PATH="$(service_install_path "$service_name")" \ + --build-arg APP_PORT="$(service_environment_value "$service_name" APP_PORT 8000)" \ + --build-arg APP_UID="$(service_uid "$service_name")" \ + --build-arg APP_GID="$(service_gid "$service_name")" \ + --tag "$(service_image_name "$service_name")" "$context" + else + vite_api_url="$(service_environment_value "$service_name" VITE_API_BASE_URL)" + engine_build --no-cache --file "$context/$dockerfile" \ + --build-arg GIT_REVISION="$git_revision" \ + --build-arg VITE_API_BASE_URL="$vite_api_url" \ + --tag "$(service_image_name "$service_name")" "$context" fi -else - echo "Skipping Samba demo data load (flag enabled or service not present)" -fi - -echo "Skipping crontab add/start (cron is managed by the container entrypoint)" - -run_standard_smoke_test \ - "$repo_root/scripts/smoke_test.sh" "$mode" "$engine" "$compose_file" \ - "${compose_env_file:-}" || exit 1 +done +# 7. Start the complete topology from one Compose file. +deployment_compose -f "$compose_file" up -d + +# 8. Wait for every application service readiness contract. +for service_name in "${install_services[@]}"; do + container_id="$(current_service_container "$service_name")" + [ -n "$container_id" ] || die "Unable to resolve $service_name container" + ensure_service_running "$service_name" "$container_id" >/dev/null + readiness_path="$(service_readiness_path "$service_name")" + deadline=$((SECONDS + 120)) + until engine_exec exec "$container_id" test -f "$readiness_path"; do + ((SECONDS < deadline)) || { engine_exec logs --tail 200 "$container_id"; die "$service_name readiness timeout"; } + sleep 2 + done +done -dns_url="" -local_ip="" -if [ -f "$host_install_conf_path" ]; then - dns_url=$(grep -E "^DNS_URL=" "$host_install_conf_path" | tail -n 1 | cut -d= -f2- | sed "s/^['\"]//;s/['\"]$//") - local_ip=$(grep -E "^LOCAL_SERVER_IP=" "$host_install_conf_path" | tail -n 1 | cut -d= -f2- | sed "s/^['\"]//;s/['\"]$//") -fi +# 9. Repair running mounts for applications and selected add-ons. +for service_name in "${permission_services[@]}"; do + container_id="$(current_service_container "$service_name")" + prepare_running_container_mount_permissions "$service_name" "$container_id" +done -access_urls=() -if [ -n "$dns_url" ] && [ "$dns_url" != "*" ]; then - access_urls+=("http://${dns_url}:${app_port}") -fi -if [ -n "$local_ip" ] && [ "$local_ip" != "*" ]; then - access_urls+=("http://${local_ip}:${app_port}") -fi -if [ ${#access_urls[@]} -eq 0 ]; then - access_urls+=("http://localhost:${app_port}") -fi +# 10. Bootstrap only application profiles that require runtime bootstrap. +for service_name in "${install_services[@]}"; do + container_id="$(current_service_container "$service_name")" + bootstrap_service "$service_name" "$container_id" "$action" || die "$service_name bootstrap failed" +done -echo "You can now access iSkyLIMS via: ${access_urls[*]}" +# 11. Execute the common smoke dispatcher with generated profile checks. +smoke_args=(--engine "$engine" --compose_file "$compose_file" --env_file "$compose_env_file") +[ "$mode" = test ] && smoke_args+=(--test) +bash "$script_dir/scripts/smoke_test.sh" "${smoke_args[@]}" +echo "$action completed successfully for $APPLICATION_NAME." diff --git a/deployment/apache/00-logs.conf b/deployment/apache/00-logs.conf new file mode 100644 index 00000000..ad288787 --- /dev/null +++ b/deployment/apache/00-logs.conf @@ -0,0 +1,8 @@ +# Shared Apache log formats and persistent log destinations. +ErrorLog "logs/apache_error.log" +LogLevel warn +LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + + LogFormat "%v %{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D %I %O" proxy + +CustomLog "logs/apache.access.log" combined diff --git a/deployment/apache/01-reverse-proxy.conf b/deployment/apache/01-reverse-proxy.conf new file mode 100644 index 00000000..6425b793 --- /dev/null +++ b/deployment/apache/01-reverse-proxy.conf @@ -0,0 +1,31 @@ +# Generated reverse-proxy contract. Edit ADDONS.apache in project.json, +# not this synchronized output. +# Separate virtual hosts are recommended for multi-app deployments and +# identity providers because each service keeps its native root URL. + + + ServerName localhost + ProxyRequests Off + ProxyPreserveHost On + AllowEncodedSlashes NoDecode + RequestHeader set X-Forwarded-Proto "${APACHE_FORWARDED_PROTO}" + RequestHeader set X-Forwarded-Port "${APACHE_FORWARDED_PORT}" + RequestHeader set X-Forwarded-Host "localhost" + LimitRequestBody ${APACHE_LIMIT_REQUEST_BODY} + ProxyPass /static/ ! + Alias /static/ /var/www/app/static/ + + Require all granted + + ProxyPass /documents/ ! + Alias /documents/ /var/www/app/documents/ + + Require all granted + + ProxyPass / http://app:${APP_APP_PORT}/ + ProxyPassReverse / http://app:${APP_APP_PORT}/ + ProxyTimeout 120 + TimeOut 120 + ErrorLog "logs/localhost.error.log" + CustomLog "logs/localhost.access.log" combined + diff --git a/deployment/apache/02-server-status.conf b/deployment/apache/02-server-status.conf new file mode 100644 index 00000000..6d1ee65f --- /dev/null +++ b/deployment/apache/02-server-status.conf @@ -0,0 +1,6 @@ +# Local-only operational endpoint; never expose it through ProxyPass. +ExtendedStatus On + + SetHandler server-status + Require local + diff --git a/deployment/lib/container/common.sh b/deployment/lib/container/common.sh new file mode 100644 index 00000000..b5522c8d --- /dev/null +++ b/deployment/lib/container/common.sh @@ -0,0 +1,786 @@ +#!/usr/bin/env bash + +# BU-ISCIII shared container installer library. +# +# This file is centrally managed. Applications must not edit vendored copies. +# Application container_install.sh scripts provide the global variables used by +# these helpers (engine, mode, compose_env_file, ENGINE_CMD and COMPOSE_CMD). + +BU_ISCIII_CONTAINER_LIB_VERSION="0.1.0" + +# Select and validate the requested container engine and its Compose frontend. +# This keeps Docker/Podman detection identical in every outer installer. +set_engine() { + if [ "$engine" = "docker" ]; then + if ! command -v docker >/dev/null 2>&1; then + echo "docker not found. Install docker or use --engine podman." + exit 1 + fi + ENGINE_CMD=("docker") + COMPOSE_CMD=("docker" "compose") + else + if ! command -v podman >/dev/null 2>&1; then + echo "podman not found. Install podman or use --engine docker." + exit 1 + fi + ENGINE_CMD=("podman") + if command -v podman-compose >/dev/null 2>&1; then + COMPOSE_CMD=("podman-compose") + elif podman compose version >/dev/null 2>&1; then + COMPOSE_CMD=("podman" "compose") + else + echo "podman compose not available. Install podman-compose or use --engine docker." + exit 1 + fi + fi +} + +# Run a container-engine command using the engine selected by set_engine. +engine_exec() { + "${ENGINE_CMD[@]}" "$@" +} + +# Build an image with the selected engine. Docker explicitly enables BuildKit +# because Dockerfile secret mounts are unavailable in the legacy builder. +engine_build() { + if [ "${engine:-docker}" = "docker" ]; then + DOCKER_BUILDKIT=1 engine_exec build "$@" + else + engine_exec build "$@" + fi +} + +# Run a Compose command using the frontend selected by set_engine. +compose_exec() { + "${COMPOSE_CMD[@]}" "$@" +} + +# Run Compose with the generated production environment file when available. +# Test Compose files intentionally continue to use their normal environment. +compose_with_env_exec() { + if [ "${mode:-production}" = "production" ] \ + && [ -n "${compose_env_file:-}" ] \ + && [ -f "$compose_env_file" ]; then + compose_exec --env-file "$compose_env_file" "$@" + else + compose_exec "$@" + fi +} + +# Return the image ID associated with one service in an explicit Compose file. +# Both arguments are required so the helper has no hidden application globals. +# Arguments: compose file path, service name. +compose_service_image_id() { + local compose_path="$1" + local service_name="$2" + compose_with_env_exec -f "$compose_path" images -q "$service_name" 2>/dev/null \ + | tail -n 1 +} + +# Fail early when the selected Compose file does not exist. +# Arguments: Compose file path. +require_compose_file() { + local compose_path="$1" + if [ ! -f "$compose_path" ]; then + echo "Compose file '$compose_path' not found" >&2 + return 1 + fi +} + +# Validate the fully interpolated Compose model after application environment +# preparation. Arguments: Compose file path. +validate_compose_configuration() { + local compose_path="$1" + require_compose_file "$compose_path" || return 1 + if ! compose_with_env_exec -f "$compose_path" config --quiet; then + echo "Compose configuration validation failed: $compose_path" >&2 + return 1 + fi +} + +# Return a repository's full or short HEAD without printing diagnostics. +# Arguments: repository path, optional format (`full` or `short`). +repository_revision() { + local repository_path="$1" + local format="${2:-full}" + command -v git >/dev/null 2>&1 || return 1 + git -C "$repository_path" rev-parse --is-inside-work-tree >/dev/null 2>&1 \ + || return 1 + if [ "$format" = "short" ]; then + git -C "$repository_path" rev-parse --short HEAD + else + git -C "$repository_path" rev-parse HEAD + fi +} + +# Print the actual local checkout and the revision requested by the operator. +# Arguments: display label, repository path, expected branch/tag/commit/current. +print_repository_diagnostics() { + local label="$1" + local repository_path="$2" + local expected_revision="$3" + local actual_hash="" + + echo "$label" + echo " requested revision: $expected_revision" + actual_hash="$(repository_revision "$repository_path" full)" || true + if [ -n "$actual_hash" ]; then + echo " local HEAD: $(git -C "$repository_path" log -1 --oneline)" + echo " local HEAD hash: $actual_hash" + else + echo " local git metadata unavailable" + fi +} + +# Print an image ID before a build. The caller resolves the ID from an explicit +# image name or Compose service and passes it in, keeping lookup policy separate. +# Arguments: display label, image ID/name (empty means no existing image). +print_image_before_diagnostics() { + local label="$1" + local image_reference="$2" + echo "$label" + if [ -n "$image_reference" ]; then + echo " image before build: $image_reference" + else + echo " image before build: none" + fi +} + +# Compare explicit pre-build and post-build image IDs/names. +# Arguments: display label, previous image reference, current image reference. +print_image_after_diagnostics() { + local label="$1" + local previous_reference="$2" + local current_reference="$3" + echo "$label" + if [ -z "$current_reference" ]; then + echo " image after build: not found" + elif [ -n "$previous_reference" ] && [ "$previous_reference" = "$current_reference" ]; then + echo " image after build: $current_reference" + echo " image id check: unchanged" + elif [ -n "$previous_reference" ]; then + echo " image after build: $current_reference" + echo " image id check: changed" + else + echo " image after build: $current_reference" + echo " image id check: created" + fi +} + +# Print the standard repository and existing-image report before a build. +# Arguments: label, repository path, requested revision, existing image ID/name. +print_prebuild_diagnostics() { + local label="$1" + local repository_path="$2" + local expected_revision="$3" + local image_reference="$4" + + print_repository_diagnostics "$label source:" "$repository_path" "$expected_revision" + print_image_before_diagnostics "$label image before build:" "$image_reference" +} + +# Compare a container's source checkout with an explicit expected revision. +# Arguments: label, container ID/name, repository path inside the container, +# expected full hash, expected short hash. +print_container_repository_diagnostics() { + local label="$1" + local container_id="$2" + local repository_path="$3" + local expected_hash="$4" + local expected_short="$5" + local container_hash="" + local container_short="" + + echo "$label" + container_hash="$(engine_exec exec "$container_id" sh -lc " + if [ -d '$repository_path/.git' ]; then + cd '$repository_path' && git rev-parse HEAD + fi + " 2>/dev/null | tail -n 1)" + container_short="$(engine_exec exec "$container_id" sh -lc " + if [ -d '$repository_path/.git' ]; then + cd '$repository_path' && git rev-parse --short HEAD + fi + " 2>/dev/null | tail -n 1)" + if [ -n "$container_hash" ]; then + echo " container source HEAD hash: $container_hash" + fi + if [ -n "$expected_hash" ] && [ -n "$container_hash" ]; then + if [ "$expected_hash" = "$container_hash" ]; then + echo " HEAD check: OK local=$expected_short container=$container_short" + else + echo " HEAD check: MISMATCH local=$expected_short container=$container_short" + fi + fi + engine_exec exec "$container_id" sh -lc " + echo ' $repository_path HEAD:' + if [ -d '$repository_path/.git' ]; then + cd '$repository_path' && git log -1 --oneline + else + echo 'not a git checkout' + fi + " || true +} + +# Copy a host file, entering Podman's user namespace when mapped ownership +# prevents an ordinary host-side copy. +copy_with_podman_fallback() { + local src="$1" + local dst="$2" + + if cp "$src" "$dst" 2>/dev/null; then + return 0 + fi + if [ "$engine" = "podman" ] && podman unshare cp "$src" "$dst"; then + return 0 + fi + echo "Failed to copy '$src' to '$dst'" >&2 + return 1 +} + +# Change host-path modes, using Podman unshare for rootless mapped paths. +chmod_with_podman_fallback() { + local mode_value="$1" + shift + + if chmod "$mode_value" "$@" 2>/dev/null; then + return 0 + fi + if [ "$engine" = "podman" ] && podman unshare chmod "$mode_value" "$@"; then + return 0 + fi + echo "Failed to chmod $mode_value: $*" >&2 + return 1 +} + +# Change host-path ownership recursively, using Podman unshare when required. +chown_with_podman_fallback() { + local owner="$1" + shift + + if chown -R "$owner" "$@" 2>/dev/null; then + return 0 + fi + if [ "$engine" = "podman" ] && podman unshare chown -R "$owner" "$@"; then + return 0 + fi + echo "Failed to chown $owner: $*" >&2 + return 1 +} + +# Apply an application-owned host permission specification consistently. +# Each argument is one `path|owner|mode` entry. Use `-` when ownership or mode +# must remain unchanged. Missing paths are skipped so optional bind sources can +# be declared alongside required ones without wrapper-owned condition loops. +apply_host_permission_spec() { + local entry path owner mode extra + + for entry in "$@"; do + IFS='|' read -r path owner mode extra <<< "$entry" + if [ -z "$path" ] || [ -z "$owner" ] || [ -z "$mode" ] || [ -n "$extra" ]; then + echo "Invalid host permission entry '$entry'; expected path|owner|mode." >&2 + return 1 + fi + [ -e "$path" ] || continue + if [ "$owner" != "-" ]; then + chown_with_podman_fallback "$owner" "$path" || return 1 + fi + if [ "$mode" != "-" ]; then + chmod_with_podman_fallback "$mode" "$path" || return 1 + fi + done +} + +# Create and repair application-writable directories as seen inside a running +# container. Each argument is one `path|owner|mode` entry. Ownership and mode +# are applied recursively because mounted directory contents may pre-exist. +# Arguments: container ID/name, followed by directory specifications. +apply_container_directory_permission_spec() { + local container_id="$1" + shift + local entry path owner mode extra + + for entry in "$@"; do + IFS='|' read -r path owner mode extra <<< "$entry" + if [ -z "$path" ] || [ -z "$owner" ] || [ -z "$mode" ] || [ -n "$extra" ]; then + echo "Invalid container directory permission entry '$entry'; expected path|owner|mode." >&2 + return 1 + fi + engine_exec exec --user 0 "$container_id" sh -c ' + path="$1" + owner="$2" + mode="$3" + mkdir -p "$path" + chown -R "$owner" "$path" + chmod -R "$mode" "$path" + ' _ "$path" "$owner" "$mode" || return 1 + done +} + +# Copy an installation configuration into a running container and immediately +# restrict it to the application identity. The destination parent must already +# exist as part of the staged application source. +# Arguments: container ID/name, host file, container path, UID, GID. +stage_container_runtime_config() { + local container_id="$1" + local host_path="$2" + local container_path="$3" + local application_uid="$4" + local application_gid="$5" + + if [ ! -f "$host_path" ]; then + echo "Runtime installation configuration not found: $host_path" >&2 + return 1 + fi + if [ -z "$container_path" ] || [ "$container_path" = "/" ]; then + echo "Invalid runtime installation configuration destination: '$container_path'" >&2 + return 1 + fi + engine_exec cp "$host_path" "${container_id}:$container_path" || return 1 + engine_exec exec --user 0 "$container_id" \ + chown "$application_uid:$application_gid" "$container_path" || return 1 + engine_exec exec --user 0 "$container_id" chmod 0600 "$container_path" +} + +# Remove the exact temporary runtime configuration staged for bootstrap. +# Arguments: container ID/name, container path. +remove_container_runtime_config() { + local container_id="$1" + local container_path="$2" + + if [ -z "$container_path" ] || [ "$container_path" = "/" ]; then + echo "Refusing to remove invalid runtime configuration path: '$container_path'" >&2 + return 1 + fi + engine_exec exec --user 0 "$container_id" rm -f -- "$container_path" +} + +# Run an application's smoke-test script with the standard deployment context. +# Arguments: script, mode, engine, Compose file, optional environment file, +# followed by application-specific smoke-test arguments. +run_standard_smoke_test() { + local smoke_script="$1" + local deployment_mode="$2" + local container_engine="$3" + local compose_path="$4" + local environment_path="$5" + shift 5 + local -a smoke_args=(--engine "$container_engine" --compose_file "$compose_path") + + if [ ! -f "$smoke_script" ]; then + echo "Smoke-test script not found: $smoke_script" >&2 + return 1 + fi + if [ "$deployment_mode" = "test" ]; then + smoke_args+=(--test) + elif [ -n "$environment_path" ]; then + smoke_args+=(--env_file "$environment_path") + fi + echo "Running deployment smoke test: $smoke_script" + bash "$smoke_script" "${smoke_args[@]}" "$@" +} + +# Convert a URL or host:port value into a valid Apache ServerName host. +normalize_apache_server_name() { + local value="$1" + + value="${value#http://}" + value="${value#https://}" + value="${value%%/*}" + value="${value%%:*}" + if [ -z "$value" ] || [ "$value" = "*" ]; then + value="localhost" + fi + echo "$value" +} + +# Escape a value before placing it in the replacement side of a sed command. +sed_replacement_escape() { + printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g' +} + +# Escape a literal value before placing it in the search side of a sed command. +sed_search_escape() { + printf '%s' "$1" | sed -e 's/[][\\.^$*+?{}()|]/\\&/g' +} + +# Read one shell-style installation setting in an isolated Bash process. +# Isolation prevents configuration assignments from changing installer state. +read_install_conf_value() { + local key="$1" + local file="$2" + [ -f "$file" ] || return 0 + bash -c ' + set -a + . "$1" + key="$2" + printf "%s" "${!key-}" + ' _ "$file" "$key" +} + +# Return the first non-empty value among compatible setting names. This allows +# the shared renderer to bridge established RELECOV names and the standard +# scaffold names without duplicating application configuration files. +read_install_conf_first() { + local file="$1" + shift + local key value + for key in "$@"; do + value="$(read_install_conf_value "$key" "$file")" + if [ -n "$value" ]; then + echo "$value" + return 0 + fi + done +} + +# Return an environment override, then a configuration value, then a default. +# Arguments: setting name, configuration file, default value. +config_value_or_default() { + local key="$1" + local file="$2" + local default_value="$3" + local env_value="${!key:-}" + local config_value="" + + if [ -n "$env_value" ]; then + echo "$env_value" + return 0 + fi + config_value="$(read_install_conf_value "$key" "$file")" + if [ -n "$config_value" ]; then + echo "$config_value" + else + echo "$default_value" + fi +} + +# Return a required setting using the same environment-over-file precedence. +# This is used while building the generated Compose environment so a missing +# secret or host path fails before Compose changes deployment state. +config_value() { + local key="$1" + local file="$2" + local value + value="$(config_value_or_default "$key" "$file" "")" + if [ -z "$value" ]; then + echo "Required setting $key is missing from environment and $file" >&2 + return 1 + fi + printf '%s\n' "$value" +} + +# Normalize a bind-mount setting to a file path. Operators may configure either +# the final file or its parent directory; the supplied default is app-owned. +normalize_bind_file_path() { + local value="$1" + local default_path="$2" + local filename="$3" + local expected_suffix=".${filename##*.}" + + if [ -z "$value" ]; then + echo "$default_path" + elif [ -d "$value" ] || [[ "$value" = */ ]] || [[ "$value" != *"$expected_suffix" ]]; then + echo "${value%/}/$filename" + else + echo "$value" + fi +} + +# Create a configuration file from a plain-text template. Arguments are: +# source template, destination file, destination mode, then repeating literal +# PLACEHOLDER VALUE pairs. Every placeholder occurrence is replaced by its +# value; values are not executed or sourced as shell code. The completed file +# is installed through the Docker/Podman-compatible host file helpers. +render_config_template() { + local src="$1" + local dst="$2" + local file_mode="$3" + shift 3 + local tmp_file token value escaped_token escaped_value + + if [ $(( $# % 2 )) -ne 0 ]; then + echo "render_config_template requires PLACEHOLDER VALUE pairs" >&2 + return 2 + fi + + tmp_file="$(mktemp)" + cp "$src" "$tmp_file" + while [ "$#" -gt 0 ]; do + token="$1" + value="$2" + shift 2 + escaped_token="$(sed_search_escape "$token")" + escaped_value="$(sed_replacement_escape "$value")" + sed -i "s|$escaped_token|$escaped_value|g" "$tmp_file" + done + if copy_with_podman_fallback "$tmp_file" "$dst" \ + && chmod_with_podman_fallback "$file_mode" "$dst"; then + rm -f "$tmp_file" + return 0 + fi + rm -f "$tmp_file" + return 1 +} + +# Quote one value for literal use in a Compose environment file. Single-quoted +# dotenv values are not interpolated; embedded apostrophes are escaped. +compose_environment_quote() { + local value="$1" + if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then + echo "Compose environment values must not contain newlines." >&2 + return 1 + fi + value="${value//\'/\\\'}" + printf "'%s'" "$value" +} + +# Generate one protected Compose environment file from application settings. +# Arguments: output path, settings-source array name, explicit-value array name. +# +# Settings sources contain `PREFIX|path` entries. Every uppercase assignment in +# the file is emitted as PREFIX_KEY, or as KEY when PREFIX is empty. Explicit +# values contain `KEY|value` entries for derived values such as image names and +# Git revisions. Duplicate output keys, invalid names, and multiline values are +# rejected. The result is installed atomically with mode 0600. +write_compose_environment_file() { + local output_path="$1" + local sources_name="$2" + local values_name="$3" + local -n settings_sources_ref="$sources_name" + local -n explicit_values_ref="$values_name" + local output_dir temporary_file entry prefix settings_path key output_key value quoted_value + local -A emitted_keys=() + + output_dir="$(dirname "$output_path")" + [ -d "$output_dir" ] || { + echo "Compose environment output directory not found: $output_dir" >&2 + return 1 + } + temporary_file="$(mktemp "$output_dir/.compose-env.XXXXXX")" || return 1 + chmod 0600 "$temporary_file" || { rm -f "$temporary_file"; return 1; } + + for entry in "${settings_sources_ref[@]}"; do + if [[ "$entry" != *"|"* ]]; then + echo "Invalid Compose settings source '$entry'; expected PREFIX|path." >&2 + rm -f "$temporary_file" + return 1 + fi + prefix="${entry%%|*}" + settings_path="${entry#*|}" + if [ -n "$prefix" ] && [[ ! "$prefix" =~ ^[A-Z_][A-Z0-9_]*$ ]]; then + echo "Invalid Compose environment prefix: $prefix" >&2 + rm -f "$temporary_file" + return 1 + fi + if [ ! -f "$settings_path" ]; then + echo "Compose settings source not found: $settings_path" >&2 + rm -f "$temporary_file" + return 1 + fi + + while IFS= read -r key; do + [ -n "$key" ] || continue + if [ -n "$prefix" ]; then output_key="${prefix}_${key}"; else output_key="$key"; fi + if [ -n "${emitted_keys[$output_key]:-}" ]; then + echo "Duplicate Compose environment variable: $output_key" >&2 + rm -f "$temporary_file" + return 1 + fi + value="$(read_install_conf_value "$key" "$settings_path")" + quoted_value="$(compose_environment_quote "$value")" \ + || { rm -f "$temporary_file"; return 1; } + printf '%s=%s\n' "$output_key" "$quoted_value" >> "$temporary_file" \ + || { rm -f "$temporary_file"; return 1; } + emitted_keys["$output_key"]=1 + done < <(sed -nE \ + 's/^[[:space:]]*(export[[:space:]]+)?([A-Z_][A-Z0-9_]*)[[:space:]]*=.*/\2/p' \ + "$settings_path" | sort -u) + done + + for entry in "${explicit_values_ref[@]}"; do + if [[ "$entry" != *"|"* ]]; then + echo "Invalid explicit Compose value '$entry'; expected KEY|value." >&2 + rm -f "$temporary_file" + return 1 + fi + output_key="${entry%%|*}" + value="${entry#*|}" + if [[ ! "$output_key" =~ ^[A-Z_][A-Z0-9_]*$ ]]; then + echo "Invalid Compose environment variable name: $output_key" >&2 + rm -f "$temporary_file" + return 1 + fi + if [ -n "${emitted_keys[$output_key]:-}" ]; then + echo "Duplicate Compose environment variable: $output_key" >&2 + rm -f "$temporary_file" + return 1 + fi + quoted_value="$(compose_environment_quote "$value")" \ + || { rm -f "$temporary_file"; return 1; } + printf '%s=%s\n' "$output_key" "$quoted_value" >> "$temporary_file" \ + || { rm -f "$temporary_file"; return 1; } + emitted_keys["$output_key"]=1 + done + + mv -f "$temporary_file" "$output_path" || { rm -f "$temporary_file"; return 1; } + chmod 0600 "$output_path" +} + +# Load the protected dotenv file generated by write_compose_environment_file. +# Direct image builds and host preparation then consume exactly the same +# prefixed deployment values that Compose interpolates later. +load_compose_environment_file() { + local file="$1" + local disable_allexport_after="true" + [ -f "$file" ] || { + echo "Compose environment file not found: $file" >&2 + return 1 + } + [[ $- == *a* ]] && disable_allexport_after="false" + set -a + # shellcheck disable=SC1090 + source "$file" + [ "$disable_allexport_after" = false ] || set +a +} + +# Validate an installation configuration and, when it is outside the selected +# build context, copy it to a temporary context file. All results are explicit +# named output variables; applications still choose service contexts/defaults. +# Arguments: input path, base directory, build context, service, mode, +# host-path output name, context-relative output name, +# temporary-path output name. +prepare_install_configuration() { + local input_path="$1" + local base_directory="$2" + local build_context="$3" + local service_name="$4" + local deployment_mode="$5" + local -n host_path_result="$6" + local -n context_path_result="$7" + local -n temporary_path_result="$8" + local resolved_host_path="" + local resolved_context="" + local temporary_name="" + + host_path_result="" + context_path_result="" + temporary_path_result="" + + if [ ! -d "$build_context" ]; then + echo "Build context directory '$build_context' for service '$service_name' not found" >&2 + return 1 + fi + resolved_context="$(cd "$build_context" && pwd -P)" + + if [[ "$input_path" = /* ]]; then + resolved_host_path="$input_path" + else + resolved_host_path="${base_directory%/}/$input_path" + fi + if [ ! -f "$resolved_host_path" ]; then + echo "Install configuration '$input_path' for service '$service_name' not found" >&2 + return 1 + fi + resolved_host_path="$(cd "$(dirname "$resolved_host_path")" && pwd -P)/$(basename "$resolved_host_path")" + + if [ "$deployment_mode" = "production" ] \ + && [[ "$(basename "$resolved_host_path")" != *settings*.txt ]]; then + echo "Production configuration filenames must match *settings*.txt so .dockerignore excludes them from COPY: $resolved_host_path" >&2 + return 1 + fi + + if [[ "$resolved_host_path" != "$resolved_context/"* ]]; then + if [ "$deployment_mode" = "production" ]; then + temporary_name=".tmp_docker_install_conf_${service_name}_$$.txt" + else + # Test configuration is explicitly non-sensitive and must remain in + # the context because test builds do not receive a build secret. + temporary_name=".tmp_docker_test_install_conf_${service_name}_$$.txt" + fi + temporary_path_result="$resolved_context/$temporary_name" + echo "Copying $resolved_host_path into temporary file $temporary_path_result for service '$service_name'." >&2 + cp "$resolved_host_path" "$temporary_path_result" || return 1 + resolved_host_path="$temporary_path_result" + fi + + host_path_result="$resolved_host_path" + context_path_result="${resolved_host_path#$resolved_context/}" +} + +# Remove explicitly supplied temporary files. It is intended for EXIT traps; +# callers retain ownership of the list and no directory is ever removed. +cleanup_files() { + local file + for file in "$@"; do + if [ -n "$file" ] && [ -f "$file" ]; then + rm -f "$file" + fi + done +} + +# Return success when the first argument exactly matches one of the remaining +# arguments. An empty value list is valid and returns failure. +# Arguments: value to find, zero or more candidate values. +array_contains() { + local wanted="$1" + shift + local candidate + for candidate in "$@"; do + if [ "$candidate" = "$wanted" ]; then + return 0 + fi + done + return 1 +} + +# Test whether a service is declared in the selected Compose model. A stopped +# service still exists, so this deliberately uses config rather than ps. +service_exists() { + compose_with_env_exec -f "$compose_file" config --services 2>/dev/null \ + | grep -Fxq "$1" +} + +# Resolve a service to a container name/ID, supporting application legacy names +# first and standard Compose service labels as the portable fallback. +resolve_service_container() { + local service_name="$1" + local service_container="" + local container_name="" + + # Applications may define service_container_name to support explicit legacy + # container_name values. Compose labels are the generic fallback. + if declare -F service_container_name >/dev/null 2>&1; then + container_name="$(service_container_name "$service_name")" + fi + if [ -n "$container_name" ] \ + && engine_exec inspect -f '{{.Id}}' "$container_name" >/dev/null 2>&1; then + service_container="$container_name" + else + service_container="$(engine_exec ps -a \ + --filter "label=com.docker.compose.service=${service_name}" \ + --format '{{.ID}}' | head -n 1)" + fi + if [ -z "$service_container" ]; then + echo "Error: unable to resolve container ID for service '$service_name'." >&2 + return 1 + fi + echo "$service_container" +} + +# Fail with useful logs unless a resolved service container is running. +ensure_service_running() { + local service_name="$1" + local service_container="$2" + + if ! engine_exec inspect -f '{{.State.Running}}' "$service_container" >/dev/null 2>&1; then + echo "Error: service '$service_name' container does not exist." >&2 + exit 1 + fi + if [ "$(engine_exec inspect -f '{{.State.Running}}' "$service_container")" != "true" ]; then + echo "Error: service '$service_name' container is not running. Showing logs:" >&2 + engine_exec logs --tail 200 "$service_container" >&2 || true + exit 1 + fi + echo "$service_container" +} diff --git a/deployment/lib/container/django.sh b/deployment/lib/container/django.sh new file mode 100644 index 00000000..d2f4577b --- /dev/null +++ b/deployment/lib/container/django.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +# BU-ISCIII shared host-side Django deployment helpers. +# +# These functions belong to container_install.sh, not the in-container +# install.sh: Compose requires the settings bind-mount source to exist on the +# host before the application container can start. The inner installer cannot +# create or repair that host path because it runs after mounts are established. + +BU_ISCIII_DJANGO_CONTAINER_LIB_VERSION="0.1.0" + +# Generate a cryptographically random Django-compatible SECRET_KEY without +# requiring Django itself to be installed on the deployment host. It belongs in +# this profile because its alphabet and purpose are Django-specific. +generate_django_secret_key() { + if command -v python3 >/dev/null 2>&1; then + python3 -c "import secrets; print(''.join(secrets.choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for _ in range(50)))" + else + LC_ALL=C tr -dc 'A-Za-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c 50 + printf "\n" + fi +} + +# Render a Django settings template from the standard BU-ISCIII installation +# keys while preserving an existing non-placeholder SECRET_KEY. +# Arguments: template path, destination settings path, install-conf path. +render_django_settings_file() { + local template_path="$1" + local settings_path="$2" + local install_conf_path="$3" + local secret_line="" + local template_secret_line="" + + if [ -f "$settings_path" ]; then + secret_line="$(grep -E "^SECRET_KEY[[:space:]]*=" "$settings_path" | tail -n 1 || true)" + fi + if [ -z "$secret_line" ] || [[ "$secret_line" =~ SECRET_KEY[[:space:]]*=[[:space:]]*SECRET ]]; then + secret_line="SECRET_KEY = '$(generate_django_secret_key)'" + fi + + template_secret_line="$(grep -E "^SECRET_KEY[[:space:]]*=" "$template_path" | head -n 1)" + if [ -z "$template_secret_line" ]; then + echo "Django template '$template_path' has no SECRET_KEY assignment." >&2 + return 1 + fi + render_config_template "$template_path" "$settings_path" 0664 \ + "$template_secret_line" "$secret_line" \ + djangouser "$(read_install_conf_first "$install_conf_path" DB_USER)" \ + djangopass "$(read_install_conf_first "$install_conf_path" DB_PASS DB_PASSWORD)" \ + djangohost "$(read_install_conf_first "$install_conf_path" DB_SERVER_IP DB_HOST)" \ + djangoport "$(read_install_conf_value DB_PORT "$install_conf_path")" \ + djangodbname "$(read_install_conf_value DB_NAME "$install_conf_path")" \ + emailhostserver "$(read_install_conf_first "$install_conf_path" EMAIL_HOST_SERVER EMAIL_HOST)" \ + emailport "$(read_install_conf_value EMAIL_PORT "$install_conf_path")" \ + emailhostuser "$(read_install_conf_value EMAIL_HOST_USER "$install_conf_path")" \ + emailhostpassword "$(read_install_conf_value EMAIL_HOST_PASSWORD "$install_conf_path")" \ + emailhosttls "$(read_install_conf_value EMAIL_USE_TLS "$install_conf_path")" \ + djangoallowedhosts "$(read_install_conf_value DJANGO_ALLOWED_HOSTS "$install_conf_path")" \ + localserverip "$(read_install_conf_value LOCAL_SERVER_IP "$install_conf_path")" \ + localhost "$(read_install_conf_first "$install_conf_path" DNS_URL DJANGO_ALLOWED_HOSTS)" +} + +# Ensure the production settings bind source exists and reflects the selected +# database configuration before Compose starts the application container. +# Arguments: template path, destination settings path, install-conf path. +prepare_django_settings_bind_mount() { + local template_path="$1" + local settings_path="$2" + local install_conf_path="$3" + local configured_db_host="" + + [ "${mode:-production}" = "production" ] || return 0 + if [ -d "$settings_path" ]; then + echo "DJANGO_SETTINGS_PATH must resolve to a file path, but '$settings_path' is a directory." >&2 + return 1 + fi + + mkdir -p "$(dirname "$settings_path")" + configured_db_host="$(read_install_conf_first "$install_conf_path" DB_SERVER_IP DB_HOST)" + if [ ! -f "$settings_path" ] \ + || grep -Eq "SECRET_KEY[[:space:]]*=[[:space:]]*SECRET|emailhosttls|djangouser|djangopass|djangohost|djangodbname" "$settings_path" \ + || ! grep -Fq -- "\"HOST\": \"$configured_db_host\"," "$settings_path"; then + render_django_settings_file "$template_path" "$settings_path" "$install_conf_path" + fi + chmod_with_podman_fallback 0664 "$settings_path" +} + +# Apply ownership and mode to a Django settings file visible inside a running +# container. The caller supplies the exact application path and identity; +# writable-directory policy remains application-owned. +# Arguments: container ID/name, settings path, application UID, application GID. +prepare_django_container_settings_permissions() { + local container_id="$1" + local settings_path="$2" + local application_uid="$3" + local application_gid="$4" + + [ -n "$settings_path" ] || return 0 + engine_exec exec --user 0 "$container_id" sh -c ' + settings_path="$1" + owner="$2" + if [ -f "$settings_path" ]; then + chown "$owner" "$settings_path" + chmod 0664 "$settings_path" + fi + ' _ "$settings_path" "$application_uid:$application_gid" +} diff --git a/deployment/project.json b/deployment/project.json new file mode 100644 index 00000000..132dce00 --- /dev/null +++ b/deployment/project.json @@ -0,0 +1,27 @@ +{ + "APP_NAME": "iSkyLIMS", + "APP_SLUG": "iskylims", + "DESCRIPTION": "Laboratory information management system for wet-lab and dry-lab sequencing workflows.", + "REPOSITORY_URL": "https://github.com/BU-ISCIII/iSkyLIMS.git", + "DEFAULT_BRANCH": "main", + "PYTHON_VERSION": "3.11", + "TIMEZONE": "Europe/Madrid", + "SERVICES": { + "app": { + "PROFILE": "django", + "BUILD_CONTEXT": ".", + "DOCKERFILE": "Dockerfile", + "IMAGE": "relecov-iskylims:local", + "INSTALL_CONF": "conf/docker_production_settings.txt", + "TEST_INSTALL_CONF": "conf/docker_test_settings.txt", + "PROJECT_MODULE": "iskylims" + } + }, + "ADDONS": { + "apache": { + "VIRTUAL_HOSTS": { + "localhost": "app" + } + } + } +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ad987033..d0fa5c1f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,73 +1,97 @@ -version: '3.8' - services: - apache: - container_name: iskylims_apache - restart: unless-stopped - image: registry.access.redhat.com/ubi9/httpd-24 - depends_on: - - app - ports: - - "8080:8080" - volumes: - - /etc/localtime:/etc/localtime:ro - - /usr/share/zoneinfo:/usr/share/zoneinfo:ro - - ${APACHE_CONF_PATH:-/opt/iskylims/conf}/iskylims_apache_reverse_proxy.conf:/etc/httpd/conf.d/01-iskylims.conf:ro - - ${APACHE_CONF_PATH:-/opt/iskylims/conf}/iskylims_apache_logs.conf:/etc/httpd/conf.d/00-logformat.conf:ro - - ${APACHE_CONF_PATH:-/opt/iskylims/conf}/iskylims_apache_server-status.conf:/etc/httpd/conf.d/02-server-status.conf:ro - - /var/log/local/iskylims/apache:/var/log/httpd - - iskylims_static:${INSTALL_PATH:-/opt/iskylims}/static:ro - - iskylims_documents:${INSTALL_PATH:-/opt/iskylims}/documents:ro - networks: - - iskylims_net - + # BEGIN BU-ISCIII SERVICE: app (django) app: - image: ${APP_IMAGE_NAME:-relecov-iskylims:local} - container_name: iskylims_app - restart: unless-stopped + image: ${APP_IMAGE:-relecov-iskylims:local} build: - context: . + context: "." + dockerfile: "Dockerfile" args: - INSTALL_TYPE: ${INSTALL_TYPE:-dep} - GIT_REVISION: ${GIT_REVISION:-main} - INSTALL_CONF: ${INSTALL_CONF:-conf/docker_production_settings.txt} - # Production settings are read from an ephemeral build-secret mount and - # rendered on the host, never retained in the image. + GIT_REVISION: ${GIT_REVISION:-current} + INSTALL_CONF: "conf/.runtime_install_settings.txt" USE_INSTALL_CONF_SECRET: "true" RENDER_DJANGO_SETTINGS: "false" - APP_UID: ${APP_UID:-1212} - APP_GID: ${APP_GID:-1212} - APP_SHELL: ${APP_SHELL:-/sbin/nologin} - INSTALL_PATH: ${INSTALL_PATH:-/opt/iskylims} - expose: - - "${APP_PORT:-8001}" + APP_UID: ${APP_APP_UID:-1212} + APP_GID: ${APP_APP_GID:-1212} + APP_PORT: ${APP_APP_PORT:-8000} + APP_REPO_PATH: ${APP_REPO_PATH:-/srv/iskylims} + APP_INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} + restart: unless-stopped + user: "${APP_APP_UID:-1212}:${APP_APP_GID:-1212}" environment: - DJANGO_SETTINGS_MODULE: iskylims.settings - DJANGO_DEBUG: ${DJANGO_DEBUG:-false} APP_MODE: prod - APP_PORT: ${APP_PORT:-8001} - DB_CONN_MAX_AGE: ${DB_CONN_MAX_AGE:-60} - WEB_CONCURRENCY: ${WEB_CONCURRENCY:-2} - GUNICORN_THREADS: ${GUNICORN_THREADS:-2} - GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-300} - GUNICORN_KEEPALIVE: ${GUNICORN_KEEPALIVE:-5} - INSTALL_PATH: ${INSTALL_PATH:-/opt/iskylims} - user: "${APP_UID:-1005}:${APP_GID:-1005}" + INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} + APP_INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} + APP_PORT: ${APP_APP_PORT:-8000} + PROJECT_MODULE: "iskylims" + DB_HOST: ${APP_DB_HOST:?APP_DB_HOST is required} + DB_PORT: ${APP_DB_PORT:-3306} + DB_NAME: ${APP_DB_NAME:?APP_DB_NAME is required} + DB_USER: ${APP_DB_USER:?APP_DB_USER is required} + DB_PASSWORD: ${APP_DB_PASSWORD:?APP_DB_PASSWORD is required} + DJANGO_SECRET_KEY: ${APP_DJANGO_SECRET_KEY:?APP_DJANGO_SECRET_KEY is required} + DJANGO_ALLOWED_HOSTS: ${APP_DJANGO_ALLOWED_HOSTS:?APP_DJANGO_ALLOWED_HOSTS is required} + volumes: + - ${APP_HOST_DATA_PATH:?APP_HOST_DATA_PATH is required}/documents:${APP_INSTALL_PATH:-/opt/iskylims}/documents:Z + - ${APP_HOST_LOG_PATH:?APP_HOST_LOG_PATH is required}:${APP_INSTALL_PATH:-/opt/iskylims}/logs:Z + - ${APP_DJANGO_SETTINGS_PATH:?APP_DJANGO_SETTINGS_PATH is required}:${APP_INSTALL_PATH:-/opt/iskylims}/iskylims/settings.py:Z + - app_static:${APP_INSTALL_PATH:-/opt/iskylims}/static:z + healthcheck: + test: ["CMD", "python", "-c", "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ['APP_PORT'] + '/health/', timeout=3)"] + interval: 10s + timeout: 5s + retries: 12 + ports: + - "127.0.0.1:${APP_APP_PORT:-8000}:${APP_APP_PORT:-8000}" + networks: [deployment_net] + # END BU-ISCIII SERVICE: app + # BEGIN BU-ISCIII ADDON: apache + apache: + image: registry.access.redhat.com/ubi9/httpd-24:latest + restart: unless-stopped + depends_on: + app: + condition: service_healthy + environment: + # REQUIRED reverse-proxy contract consumed by the generated Apache conf. + APACHE_FORWARDED_PROTO: ${APACHE_FORWARDED_PROTO:-https} + APACHE_FORWARDED_PORT: ${APACHE_FORWARDED_PORT:-443} + APACHE_LIMIT_REQUEST_BODY: ${APACHE_LIMIT_REQUEST_BODY:-52428800} + # REQUIRED application ports used by the generated proxy configuration. + APP_APP_PORT: ${APP_APP_PORT:-8000} + ports: + # Public binding can be restricted to loopback when another host proxy + # terminates TLS before forwarding traffic to this container. + - "${APACHE_BIND_HOST:-0.0.0.0}:${APACHE_PORT:-80}:8080" + expose: + - "8080" + healthcheck: + test: ["CMD-SHELL", "bash -ec '= WAIT_TIMEOUT_SECONDS)); then + echo "Timed out after ${WAIT_TIMEOUT_SECONDS}s waiting for ${description}: ${path}" >&2 + ls -la "$(dirname "$path")" >&2 || true + exit 1 + fi + sleep 2 + done +} -if [ ! -f "${APP_DIR}/manage.py" ]; then - echo "Application entrypoint not found at ${APP_DIR}/manage.py" >&2 - ls -la "${APP_DIR}" >&2 || true - exit 1 -fi - -if [ ! -f "${APP_DIR}/virtualenv/bin/activate" ]; then - echo "Virtualenv activation script not found at ${APP_DIR}/virtualenv/bin/activate" >&2 - ls -la "${APP_DIR}/virtualenv" >&2 || true - exit 1 -fi +wait_for_file "${APP_DIR}/manage.py" "Django application entrypoint" +wait_for_file "${APP_DIR}/virtualenv/bin/activate" "virtualenv activation script" +# shellcheck disable=SC1091 source "${APP_DIR}/virtualenv/bin/activate" - mkdir -p "${CRON_DIR}" "${TMP_DIR}" safe_chmod() { @@ -35,11 +47,9 @@ safe_chmod() { shift local path for path in "$@"; do - if [ ! -e "${path}" ]; then - continue - fi - if [ -O "${path}" ]; then - chmod "${mode}" "${path}" + [[ -e "$path" ]] || continue + if [[ -O "$path" ]]; then + chmod "$mode" "$path" else echo "Skipping chmod ${mode} on ${path}: not owned by $(id -un)." fi @@ -48,82 +58,75 @@ safe_chmod() { safe_chmod 700 "${CRON_DIR}" "${TMP_DIR}" -if [ "$APP_MODE" = "dev" ]; then +if [[ "$APP_MODE" == "dev" ]]; then exec python "${APP_DIR}/manage.py" runserver "0.0.0.0:${APP_PORT}" fi -if [ -f "${CRON_DISABLED_FILE}" ]; then +if [[ -f "$CRON_DISABLED_FILE" ]]; then echo "Cron is disabled by ${CRON_DISABLED_FILE}. Skipping supercronic start." elif command -v supercronic >/dev/null 2>&1; then - # Build supercronic's crontab directly from Django settings. Avoid the - # system crontab command because it depends on PAM behavior that varies - # across rootless container hosts. python - <<'PY' > "${CRON_FILE}" import os import shlex +import sys -os.environ.setdefault( - "DJANGO_SETTINGS_MODULE", - os.environ.get("DJANGO_SETTINGS_MODULE", "iskylims.settings"), -) +app_dir = os.environ["APP_INSTALL_PATH"] +settings_module = os.environ["DJANGO_SETTINGS_MODULE"] +sys.path.insert(0, app_dir) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module) import django django.setup() from django.conf import settings -app_dir = os.environ.get("INSTALL_PATH", "/opt/iskylims") python_bin = os.path.join(app_dir, "virtualenv", "bin", "python") -settings_module = os.environ.get("DJANGO_SETTINGS_MODULE", "iskylims.settings") command_suffix = getattr(settings, "CRONTAB_COMMAND_SUFFIX", "") for job in getattr(settings, "CRONJOBS", []): if len(job) < 2: continue - - schedule = job[0] - dotted_path = job[1] + schedule, dotted_path = job[:2] job_suffix = job[2] if len(job) > 2 else "" module_name, function_name = dotted_path.rsplit(".", 1) python_code = ( "import os; " f"os.environ.setdefault('DJANGO_SETTINGS_MODULE', {settings_module!r}); " "import django; django.setup(); " - f"from {module_name} import {function_name} as cron_job; " - "cron_job()" + f"from {module_name} import {function_name} as cron_job; cron_job()" ) command = ( f"cd {shlex.quote(app_dir)} && " f"DJANGO_SETTINGS_MODULE={shlex.quote(settings_module)} " f"{shlex.quote(python_bin)} -c {shlex.quote(python_code)}" ) - suffixes = " ".join(s for s in (job_suffix, command_suffix) if s) + suffixes = " ".join(value for value in (job_suffix, command_suffix) if value) if suffixes: command = f"{command} {suffixes}" - print(f"{schedule} {command}") PY - if [ -s "${CRON_FILE}" ]; then - safe_chmod 600 "${CRON_FILE}" - : > "${CRON_LOG}" - supercronic "${CRON_FILE}" > "${CRON_LOG}" 2>&1 & + if [[ -s "$CRON_FILE" ]]; then + safe_chmod 600 "$CRON_FILE" + : > "$CRON_LOG" + supercronic "$CRON_FILE" > "$CRON_LOG" 2>&1 & CRON_PID=$! sleep 1 - if ! kill -0 "${CRON_PID}" 2>/dev/null; then - echo "supercronic failed to start. Check ${CRON_LOG} for details." + if ! kill -0 "$CRON_PID" 2>/dev/null; then + echo "supercronic failed to start. Check ${CRON_LOG}." >&2 + exit 1 fi else - echo "No cron entries found. Skipping crond start." + echo "No Django CRONJOBS found. Skipping supercronic start." fi else - echo "supercronic not found. Skipping cron." + echo "supercronic not found. Scheduled jobs are disabled." fi -if [ -n "${WEB_CONCURRENCY:-}" ]; then - GUNICORN_WORKERS="${WEB_CONCURRENCY}" +if [[ -n "${WEB_CONCURRENCY:-}" ]]; then + GUNICORN_WORKERS="$WEB_CONCURRENCY" else cpu_count="$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || echo 1)" - if [ "${cpu_count}" -le 2 ]; then + if ((cpu_count <= 2)); then GUNICORN_WORKERS=2 else GUNICORN_WORKERS=4 @@ -132,8 +135,10 @@ fi exec gunicorn "${PROJECT_MODULE}.wsgi:application" \ --bind "0.0.0.0:${APP_PORT}" \ - --workers "${GUNICORN_WORKERS}" \ - --threads "${GUNICORN_THREADS}" \ - --keep-alive "${GUNICORN_KEEPALIVE}" \ - --timeout "${GUNICORN_TIMEOUT}" \ - --worker-tmp-dir /dev/shm + --workers "$GUNICORN_WORKERS" \ + --threads "$GUNICORN_THREADS" \ + --keep-alive "$GUNICORN_KEEPALIVE" \ + --timeout "$GUNICORN_TIMEOUT" \ + --worker-tmp-dir /dev/shm \ + --access-logfile - \ + --error-logfile - diff --git a/scripts/smoke_test.sh b/scripts/smoke_test.sh new file mode 100755 index 00000000..0ce9fb88 --- /dev/null +++ b/scripts/smoke_test.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +engine="docker"; mode="production"; compose_file=""; env_file="" +while (($#)); do + case "$1" in + --test) mode="test"; shift ;; + --engine) engine="${2:-}"; shift 2 ;; + --compose_file) compose_file="${2:-}"; shift 2 ;; + --env_file) env_file="${2:-}"; shift 2 ;; + --help) echo "Usage: $0 [--test] [--engine docker|podman]"; exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done +compose_file="${compose_file:-docker-compose.$([ "$mode" = test ] && echo test || echo prod).yml}" +if [ "$engine" = docker ]; then compose=(docker compose); else compose=(podman compose); fi +args=(-f "$compose_file"); [ -z "$env_file" ] || args=(--env-file "$env_file" "${args[@]}") +# The generated dotenv file is mode 0600 and contains shell-safe quoted values. +# Source it so direct host checks use the same service ports as Compose. +if [ -n "$env_file" ]; then + set -a + # shellcheck disable=SC1090 + source "$env_file" + set +a +fi +compose_run() { "${compose[@]}" "${args[@]}" "$@"; } +fail() { echo "FAIL: $*" >&2; exit 1; } +compose_run config --quiet + container_id="$(compose_run ps -q app)" + [ -n "$container_id" ] || fail "Service app has no container" + compose_run exec -T app bash -lc 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check && ! python manage.py showmigrations --plan | grep -F '"'"'[ ]'"'"'' + echo "PASS: app Django checks and migrations" +check_url() { + local service="$1" url="$2" + curl --fail --silent --show-error --location --max-time 20 --output /dev/null "$url" \ + || { echo "FAIL: $service health endpoint: $url" >&2; return 1; } + echo "PASS: $service health endpoint" +} + check_url app "http://127.0.0.1:${APP_APP_PORT:-8000}/health/" +echo "iSkyLIMS deployment smoke test passed." From b1b537606900237d2087e336e0e9890de8f87c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 10 Aug 2026 13:13:26 +0200 Subject: [PATCH 03/55] feat: modernize Django deployment lifecycle --- conf/template_settings.py | 123 ++++++++++++++++++--- conf/template_settings.txt | 197 ---------------------------------- conf/urls.py | 3 + deployment_health/README.md | 17 +++ deployment_health/__init__.py | 1 + deployment_health/urls.py | 10 ++ deployment_health/views.py | 8 ++ install.sh | 137 ++++++++++++++++++----- 8 files changed, 263 insertions(+), 233 deletions(-) delete mode 100644 conf/template_settings.txt create mode 100644 deployment_health/README.md create mode 100644 deployment_health/__init__.py create mode 100644 deployment_health/urls.py create mode 100644 deployment_health/views.py diff --git a/conf/template_settings.py b/conf/template_settings.py index 51630e6f..4b389fda 100644 --- a/conf/template_settings.py +++ b/conf/template_settings.py @@ -1,20 +1,61 @@ -"""Django settings template rendered by the BU-ISCIII deployment library.""" +"""Django settings template for iSkyLIMS. -from pathlib import Path +Keep application behavior and the exact Django application list in this file. +The BU-ISCIII deployment renderer replaces environment-specific database, +email, host, CSRF and secret values from the selected installation settings. +""" -BASE_DIR = Path(__file__).resolve().parent.parent +import os + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# The renderer replaces this complete line and preserves the generated secret +# during upgrades. Never put a real production secret in this repository. SECRET_KEY = "PLACEHOLDER" -DEBUG = False -ALLOWED_HOSTS = [host.strip() for host in "djangoallowedhosts".split(",") if host.strip()] +DEBUG = djangodebug +ALLOWED_HOSTS = [ + host.strip() for host in "djangoallowedhosts".split(",") if host.strip() +] +CSRF_TRUSTED_ORIGINS = [ + origin.strip() + for origin in "djangocsrftrustedorigins".split(",") + if origin.strip() +] +# iSkyLIMS local applications. Add new applications here when their models, +# URLs, templates, signals or management commands must be registered by Django. +# Prefer "package.apps.AppConfigClass" when an application defines AppConfig. INSTALLED_APPS = [ + "core", + # "clinic", # Enable only when the clinic application is deployed. + "wetlab", + "drylab", + "django_utils", + # Third-party applications required by iSkyLIMS. + "mptt", + "crispy_forms", + "crispy_bootstrap5", + "django_crontab", + "django_mptt_admin", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "django_extensions", + "rest_framework", + "drf_yasg", + "django_cleanup", ] + +# Application names shown by the iSkyLIMS interface. When adding an application +# to INSTALLED_APPS, add it here only if it must appear in that interface. +APPS_NAMES = [ + ["wetlab", "Genomics unit: massive sequencing"], + ["drylab", "Bioinformatics unit: analysis requests"], +] + MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -31,13 +72,16 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], + # iSkyLIMS stores user-managed dry-lab service templates persistently. + "DIRS": [BASE_DIR + "/documents/drylab/services_templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ + "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", + "django.template.context_processors.i18n", ], }, } @@ -46,20 +90,75 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", - "NAME": "djangodbname", "USER": "djangouser", "PASSWORD": "djangopass", - "HOST": "djangohost", "PORT": "djangoport", + "NAME": "djangodbname", + "HOST": "djangohost", + "CONN_MAX_AGE": dbconnmaxage, + "TEST": { + "NAME": "iSkyLIMS_test", + }, } } +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +# Swagger currently uses HTTP Basic authentication. To use authorization tokens, +# extend SECURITY_DEFINITIONS with an apiKey entry and test schema access. +SWAGGER_SETTINGS = {"SECURITY_DEFINITIONS": {"basic": {"type": "basic"}}} + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "Europe/Madrid" +USE_I18N = True +USE_L10N = True +USE_TZ = False + +STATIC_URL = "/static/" +STATIC_ROOT = os.path.join(BASE_DIR, "static/") +MEDIA_URL = "/documents/" +MEDIA_ROOT = os.path.join(BASE_DIR, "documents/") + +CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" +CRISPY_TEMPLATE_PACK = "bootstrap5" +LOGIN_REDIRECT_URL = "/" + +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_HOST = "emailhostserver" -EMAIL_PORT = emailport +EMAIL_PORT = "emailport" EMAIL_HOST_USER = "emailhostuser" EMAIL_HOST_PASSWORD = "emailhostpassword" EMAIL_USE_TLS = emailhosttls +ALLOWED_EMAIL_DOMAINS = ["isciii.es", "externos.isciii.es"] -STATIC_URL = "/static/" -STATIC_ROOT = BASE_DIR / "static" -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +# django-crontab writes into the persistent application log directory. Add new +# jobs as (schedule, callable, output redirection) tuples and verify them with +# `python manage.py crontab show` after deployment. +LOG_CRONTAB_FILE = os.path.join(BASE_DIR, "logs", "crontab.log") +LOG_CLEAN_FILE = os.path.join(BASE_DIR, "logs", "crontab_cleanup.log") +CRONJOBS = [ + ("*/15 * * * *", "wetlab.cron.looking_for_new_runs", ">>" + LOG_CRONTAB_FILE), +] +CRONTAB_COMMAND_SUFFIX = "2>&1" + +# Maximum request body retained in memory before Django streams to disk. +DATA_UPLOAD_MAX_MEMORY_SIZE = 10_000_000 + +# Trust this header only because Apache overwrites X-Forwarded-Proto before +# forwarding requests to Django. Never expose Gunicorn directly to untrusted clients. +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/conf/template_settings.txt b/conf/template_settings.txt deleted file mode 100644 index 6367cb73..00000000 --- a/conf/template_settings.txt +++ /dev/null @@ -1,197 +0,0 @@ -""" -Django settings for iSkyLIMS project. - -Generated by 'django-admin startproject' using Django 1.11.4. - -For more information on this file, see -https://docs.djangoproject.com/en/1.11/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.11/ref/settings/ -""" - -import os - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = SECRET - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = ["localhost", "127.0.0.1", "localserverip"] - -# Application definition - -INSTALLED_APPS = [ - "core", - # "clinic", - "wetlab", - "drylab", - "django_utils", - "mptt", - "crispy_forms", - "crispy_bootstrap5", - "django_crontab", - "django_mptt_admin", - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "django_extensions", - "rest_framework", - "drf_yasg", - "django_cleanup", -] - -APPS_NAMES = [ - ["wetlab", "Genomics unit: massive sequencing"], - ["drylab", "Bioinformatics unit: analysis requests"], -] - - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -ROOT_URLCONF = "iskylims.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR + "/documents/drylab/services_templates"], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "django.template.context_processors.i18n", - ], - }, - }, -] - -WSGI_APPLICATION = "iskylims.wsgi.application" - - -# Database -# https://docs.djangoproject.com/en/1.11/ref/settings/#databases -DATABASES = { - "default": { - "ENGINE": "django.db.backends.mysql", - "USER": "djangouser", - "PASSWORD": "djangopass", - "PORT": "djangoport", - "NAME": "djangodbname", - "HOST": "djangohost", - "TEST": { - "NAME": "iSkyLIMS_test", - }, - }, -} - - -# Password validation -# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] -SWAGGER_SETTINGS = {"SECURITY_DEFINITIONS": {"basic": {"type": "basic"}}} - -""" For using token in the authorization request - 'api_key': { - 'type': 'apiKey', - 'in': 'header', - 'name': 'Authorization' - } -""" -# 'PERSIST_AUTH': True - -# Internationalization -# https://docs.djangoproject.com/en/1.11/topics/i18n/ - -LANGUAGE_CODE = "en-us" -TIME_ZONE = "Europe/Madrid" - -USE_I18N = True - -USE_L10N = True - -USE_TZ = False - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.11/howto/static-files/ - -STATIC_URL = "/static/" -STATIC_ROOT = os.path.join(BASE_DIR, "static/") - -# Media settings -MEDIA_URL = "/documents/" -MEDIA_ROOT = os.path.join(BASE_DIR, "documents/") - -# Crispy forms settings -CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" -CRISPY_TEMPLATE_PACK = "bootstrap5" - -# Redirect to home URL after login (Default redirects to /accounts/profile/) -LOGIN_REDIRECT_URL = "/" - -#EMAIL_BACKEND = ( -# "django.core.mail.backends.console.EmailBackend" # During development only -#) - -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' - -# EMAIL settings -EMAIL_HOST = "emailhostserver" -EMAIL_PORT = "emailport" -EMAIL_HOST_USER = "emailhostuser" -EMAIL_HOST_PASSWORD = "emailhostpassword" -EMAIL_USE_TLS = emailhosttls -ALLOWED_EMAIL_DOMAINS = ["isciii.es", "externos.isciii.es"] - -LOG_CRONTAB_FILE = os.path.join(BASE_DIR, "logs", "crontab.log") -LOG_CLEAN_FILE = os.path.join(BASE_DIR, "logs", "crontab_cleanup.log") - - -# Crontab settings -CRONJOBS = [ - ("*/15 * * * *", "wetlab.cron.looking_for_new_runs", ">>" + LOG_CRONTAB_FILE), -] - -CRONTAB_COMMAND_SUFFIX = "2>&1" - -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -DATA_UPLOAD_MAX_MEMORY_SIZE = 10000000 - -# Needed when using a proxy for https forwading -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') \ No newline at end of file diff --git a/conf/urls.py b/conf/urls.py index 6e313553..bdcf49ca 100644 --- a/conf/urls.py +++ b/conf/urls.py @@ -26,6 +26,9 @@ def get_schema(self, request=None, public=False): urlpatterns = [ + # Stable unauthenticated endpoint consumed by Compose and deployment smoke + # tests. Database readiness is verified separately during bootstrap. + path("health/", include("deployment_health.urls")), path("", include("core.urls")), path( "background", diff --git a/deployment_health/README.md b/deployment_health/README.md new file mode 100644 index 00000000..7ed4b80d --- /dev/null +++ b/deployment_health/README.md @@ -0,0 +1,17 @@ +# Django deployment health endpoint + +Register the centrally managed health URL module once in the application's +root `urls.py`: + +```python +from django.urls import include, path + +urlpatterns = [ + path("health/", include("deployment_health.urls")), + # Application-owned routes follow. +] +``` + +Do not add `deployment_health` to `INSTALLED_APPS`: it has no models, templates +or application startup hooks. Compose and the deployment smoke test require +`GET /health/` to return a successful response. diff --git a/deployment_health/__init__.py b/deployment_health/__init__.py new file mode 100644 index 00000000..ca536c2f --- /dev/null +++ b/deployment_health/__init__.py @@ -0,0 +1 @@ +"""Framework-neutral deployment health endpoint for Django services.""" diff --git a/deployment_health/urls.py b/deployment_health/urls.py new file mode 100644 index 00000000..b8cf7974 --- /dev/null +++ b/deployment_health/urls.py @@ -0,0 +1,10 @@ +"""URLs for the standard deployment health endpoint.""" + +from django.urls import path + +from .views import health_check + + +urlpatterns = [ + path("", health_check, name="deployment-health"), +] diff --git a/deployment_health/views.py b/deployment_health/views.py new file mode 100644 index 00000000..ee88a385 --- /dev/null +++ b/deployment_health/views.py @@ -0,0 +1,8 @@ +"""Minimal health views used by container orchestration and smoke tests.""" + +from django.http import JsonResponse + + +def health_check(_request): + """Report HTTP process readiness without querying external dependencies.""" + return JsonResponse({"status": "ok"}) diff --git a/install.sh b/install.sh index e19ed689..28d265ba 100644 --- a/install.sh +++ b/install.sh @@ -6,6 +6,12 @@ source "$install_script_dir/deployment/lib/container/common.sh" # shellcheck disable=SC1091 source "$install_script_dir/deployment/lib/container/django.sh" +# All repository-relative inputs (conf, application modules and requirements) +# belong to the source tree containing this script. Container bootstrap invokes +# this file by absolute path while its working directory is INSTALL_PATH, so +# normalize the working directory before resolving any of those inputs. +cd "$install_script_dir" + APP_VERSION="3.1.1" # usage: prints the command line help and usage examples. @@ -89,13 +95,17 @@ db_check(){ exit 1 fi - "$mysqladmin_bin" -h $DB_SERVER_IP -u$DB_USER -p$DB_PASS -P$DB_PORT processlist > /dev/null + MYSQL_PWD="$DB_PASSWORD" "$mysqladmin_bin" \ + --host="$DB_HOST" --user="$DB_USER" --port="$DB_PORT" \ + processlist > /dev/null if ! [ $? -eq 0 ]; then log "ERROR" "Unable to connect to database. Check if your database is running and accessible" exit 1 fi - RESULT=`"$mysqlshow_bin" --user=$DB_USER --password=$DB_PASS --host=$DB_SERVER_IP --port=$DB_PORT | grep -o $DB_NAME` + RESULT="$(MYSQL_PWD="$DB_PASSWORD" "$mysqlshow_bin" \ + --user="$DB_USER" --host="$DB_HOST" --port="$DB_PORT" \ + | grep -o "$DB_NAME")" if ! [ "$RESULT" == "$DB_NAME" ] ; then log "ERROR" "iskylims database is not defined yet" @@ -168,7 +178,7 @@ update_settings_and_urls(){ cp conf/urls.py "$project_dir" if [ "$render_settings" = true ]; then render_django_settings_file \ - "$install_script_dir/conf/template_settings.txt" \ + "$install_script_dir/conf/template_settings.py" \ "${settings_output:-$project_dir/settings.py}" \ "$conf" else @@ -178,8 +188,9 @@ update_settings_and_urls(){ # restore_git_ref: reset repository to branch/tag/commit active before script ran. restore_git_ref() { + [ -n "${initial_git_ref:-}" ] || return 0 echo "Restoring to initial git reference: $initial_git_ref" - git checkout "$initial_git_ref" --quiet + git -C "$install_script_dir" checkout "$initial_git_ref" --quiet } # load_tables: wrapper to call Django loaddata with optional verbosity. @@ -224,10 +235,16 @@ ensure_git_safe_directory() { fi } -# Ensure to recover current git branch/tag/SHA on script exit -ensure_git_safe_directory -initial_git_ref=$(git rev-parse --abbrev-ref HEAD || git rev-parse HEAD) -trap restore_git_ref EXIT +# A staged container image intentionally excludes .git. Git state is therefore +# optional: source staging may select a revision in a real checkout, whereas +# runtime bootstrap operates only on the immutable files already in the image. +initial_git_ref="" +if git -C "$install_script_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + ensure_git_safe_directory + initial_git_ref="$(git -C "$install_script_dir" rev-parse --abbrev-ref HEAD \ + || git -C "$install_script_dir" rev-parse HEAD)" + trap restore_git_ref EXIT +fi #================================================================ #SET TEMINAL COLORS @@ -300,15 +317,18 @@ checkout_git_revision() { printf "${YELLOW}Using copied local working tree without git checkout.${NC}\n" return 0 fi - if git rev-parse --verify "$git_branch" >/dev/null 2>&1; then + if [ -z "$initial_git_ref" ]; then + abort_install "Git revision $git_branch cannot be selected because the staged source contains no .git metadata." + fi + if git -C "$install_script_dir" rev-parse --verify "$git_branch" >/dev/null 2>&1; then if [[ $git_branch != $initial_git_ref ]]; then local local_changes - local_changes=$(git status --porcelain) + local_changes=$(git -C "$install_script_dir" status --porcelain) if [[ -n $local_changes ]]; then abort_install "Unable to switch to $git_branch. Commit or stash local changes first." fi printf "${YELLOW}Switching to revision %s.${NC}\n" "$git_branch" - git checkout "$git_branch" --quiet + git -C "$install_script_dir" checkout "$git_branch" --quiet else printf "${YELLOW}Using current revision: '%s'.${NC}\n" "$git_branch" fi @@ -507,6 +527,7 @@ install_system_packages() { apt-get install -y \ apt-utils wget \ libmysqlclient-dev \ + default-mysql-client \ python3-venv \ libpq-dev \ python3-dev python3-pip python3-wheel \ @@ -514,12 +535,59 @@ install_system_packages() { gnuplot elif [[ $linux_distribution == "CentOS" || $linux_distribution == "RedHatEnterprise" || $linux_distribution == "centos" || $linux_distribution == "rhel" || $linux_distribution == "fedora" ]]; then - echo "Software installation for Centos/RedHat" - yum groupinstall "Development tools" - yum install zlib-devel bzip2-devel openssl-devel \ - wget httpd-devel mysql-libs sqlite sqlite-devel \ - mariadb-devel libffi-devel \ - gnuplot cifs-utils + set -euo pipefail + + echo "Installing OS dependencies" + # Enable EPEL 9 for packages not provided by UBI repositories + echo "Installing EPEL repository" + + wget -O /tmp/epel-release.rpm \ + https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm + + rpm -Uvh /tmp/epel-release.rpm + rm -f /tmp/epel-release.rpm + + microdnf repolist + + cat >/etc/yum.repos.d/centos-stream-baseos.repo <<'EOF' +[centos-stream-baseos] +name=CentOS Stream 9 - BaseOS +baseurl=https://mirror.stream.centos.org/9-stream/BaseOS/$basearch/os/ +enabled=1 +gpgcheck=0 + +[centos-stream-appstream] +name=CentOS Stream 9 - AppStream +baseurl=https://mirror.stream.centos.org/9-stream/AppStream/$basearch/os/ +enabled=1 +gpgcheck=0 + +[centos-stream-crb] +name=CentOS Stream 9 - CRB +baseurl=https://mirror.stream.centos.org/9-stream/CRB/$basearch/os/ +enabled=1 +gpgcheck=0 +EOF + + microdnf install -y \ + gcc \ + gcc-c++ \ + make \ + tar \ + zlib-devel \ + bzip2-devel \ + openssl-devel \ + wget \ + httpd-devel \ + sqlite \ + sqlite-devel \ + mariadb \ + mariadb-connector-c-devel \ + libffi-devel \ + gnuplot \ + cifs-utils + + microdnf clean all fi } @@ -721,7 +789,11 @@ run_dependency_stage() { fi install_system_packages mkdir -p $INSTALL_PATH - linux_distribution=$(lsb_release -i | cut -f 2-) + if command -v lsb_release >/dev/null 2>&1; then + linux_distribution=$(lsb_release -i | cut -f 2-) + else + linux_distribution=$(awk -F= '/^ID=/{gsub(/"/,""); print $2}' /etc/os-release) + fi if [[ $linux_distribution == "Ubuntu" ]]; then apache_group="www-data" else @@ -768,7 +840,7 @@ stage_upgrade_application_files() { rsync -rlv conf/ $INSTALL_PATH/conf/ rsync -rlv --fuzzy --delay-updates --delete-delay \ --exclude "logs" --exclude "documents" --exclude "__pycache__" \ - README.md LICENSE test conf $REQUIRED_MODULES $INSTALL_PATH + README.md LICENSE test conf deployment_health $REQUIRED_MODULES $INSTALL_PATH cd $INSTALL_PATH ensure_virtualenv_ready @@ -799,8 +871,8 @@ install_application_files() { stage_install_application_files() { log_section "Starting iSkyLIMS install version: ${APP_VERSION}" - user=${SUDO_USER:-$USER} - group=$(groups | cut -d" " -f1) + user="${SUDO_USER:-$(id -un)}" + group="$(id -gn)" if command -v lsb_release >/dev/null 2>&1; then linux_distribution=$(lsb_release -i | cut -f 2-) @@ -839,7 +911,7 @@ stage_install_application_files() { fi fi - rsync -rlv README.md LICENSE test conf $REQUIRED_MODULES $INSTALL_PATH + rsync -rlv README.md LICENSE test conf deployment_health $REQUIRED_MODULES $INSTALL_PATH cd $INSTALL_PATH @@ -921,7 +993,7 @@ done # SETTING DEFAULT VALUES ren_app=false tables=false -git_branch=$initial_git_ref +git_branch=${initial_git_ref:-current} conf="./install_settings.txt" install=true install_type="full" @@ -1077,12 +1149,29 @@ if [[ "$conf" != /* ]]; then conf="$(cd "$(dirname "$conf")" && pwd)/$(basename "$conf")" fi load_install_config + +# Keep one canonical database host and password in deployment configuration. +# Legacy iSkyLIMS functions still use DB_SERVER_IP and DB_PASS internally, so +# derive those aliases here instead of asking operators to duplicate secrets. +DB_HOST="${DB_HOST:-${DB_SERVER_IP:-}}" +DB_PASSWORD="${DB_PASSWORD:-${DB_PASS:-}}" +: "${DB_HOST:?DB_HOST is required}" +: "${DB_PASSWORD:?DB_PASSWORD is required}" +DB_SERVER_IP="$DB_HOST" +DB_PASS="$DB_PASSWORD" + PROJECT_NAME="${PROJECT_NAME:-iskylims}" -checkout_git_revision +# Bootstrap consumes the application staged during image build. It must not +# attempt to inspect or change a Git checkout, which is deliberately absent +# from the production image. +if [ "$workflow" != "bootstrap" ]; then + checkout_git_revision +fi user=${SUDO_USER:-$USER} if [ "$workflow" = "stage" ]; then check_stage_requirements + run_dependency_stage "$operation" if [ "$workflow_mode" = "install" ]; then stage_install_application_files else From cdce6fc813049c42e60290e67a6dc7d2d6d26b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 10 Aug 2026 13:13:32 +0200 Subject: [PATCH 04/55] feat: render Apache deployment configuration at runtime --- .dockerignore | 2 + .gitignore | 2 + Dockerfile | 4 +- conf/apache/00-logs.conf | 17 ++++ conf/apache/01-reverse-proxy.conf | 73 +++++++++++++++ conf/apache/02-server-status.conf | 14 +++ container_install.sh | 118 ++++++++++++++++-------- deployment/apache/00-logs.conf | 8 -- deployment/apache/01-reverse-proxy.conf | 31 ------- deployment/apache/02-server-status.conf | 6 -- deployment/lib/container/common.sh | 54 ++++++++++- deployment/lib/container/django.sh | 32 ++++--- docker-compose.prod.yml | 50 +++++----- docker-compose.test.yml | 62 ++++++------- scripts/container_start.sh | 4 +- scripts/smoke_test.sh | 33 +++++-- 16 files changed, 339 insertions(+), 171 deletions(-) create mode 100644 conf/apache/00-logs.conf create mode 100644 conf/apache/01-reverse-proxy.conf create mode 100644 conf/apache/02-server-status.conf delete mode 100644 deployment/apache/00-logs.conf delete mode 100644 deployment/apache/01-reverse-proxy.conf delete mode 100644 deployment/apache/02-server-status.conf diff --git a/.dockerignore b/.dockerignore index 9f6ea929..efb8c832 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,6 +10,8 @@ conf/*settings*.txt .env.* !.env.example .env.prod.file +# Runtime-rendered add-on bind sources are not image build inputs. +deployment/apache/*.conf .venv/ venv/ diff --git a/.gitignore b/.gitignore index e0eac952..6d55e1d7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ conf/production_settings.txt # Legacy/application-specific production settings must remain local. my_prod_settings*.txt *.bu-isciii-update +# Final add-on bind sources are regenerated from repository-owned conf files. +deployment/apache/*.conf # Python and runtime artifacts __pycache__/ diff --git a/Dockerfile b/Dockerfile index df9065e9..284c2d52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM registry.access.redhat.com/ubi9/ubi-minimal ARG APP_UID=1212 ARG APP_GID=1212 -ARG APP_PORT=8000 +ARG APP_PORT=8001 ARG APP_REPO_PATH=/srv/iskylims ARG APP_INSTALL_PATH=/opt/iskylims ARG GIT_REVISION=current @@ -22,7 +22,7 @@ ENV APP_REPO_PATH=${APP_REPO_PATH} \ RUN microdnf -y update && \ microdnf -y install python3.11 python3.11-pip \ - python3.11-devel gcc git rsync wget mariadb-connector-c-devel shadow-utils && \ + python3.11-devel tar gcc git rsync wget mariadb-connector-c-devel shadow-utils && \ microdnf clean all ARG SUPERCRONIC_VERSION=v0.2.38 diff --git a/conf/apache/00-logs.conf b/conf/apache/00-logs.conf new file mode 100644 index 00000000..0ecafdbd --- /dev/null +++ b/conf/apache/00-logs.conf @@ -0,0 +1,17 @@ +ErrorLog "logs/apache_error.log" + + + LogFormat "%v %a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D %I %O" combined + LogFormat "%v %{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D %I %O" proxy + LogFormat "%v %{X-Forwarded-For}i %l %u %t \"%r\" %>s %b" common + LogFormat "%{Referer}i -> %U" referer + LogFormat "%{User-agent}i" agent + + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + + + SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded + CustomLog "logs/apache.access.log" combined env=!forwarded + CustomLog "logs/apache.access.log" proxy env=forwarded + diff --git a/conf/apache/01-reverse-proxy.conf b/conf/apache/01-reverse-proxy.conf new file mode 100644 index 00000000..2a73af98 --- /dev/null +++ b/conf/apache/01-reverse-proxy.conf @@ -0,0 +1,73 @@ +# Apache reverse proxy source configuration for iskylims. +# +# This file is copied to conf/apache/01-reverse-proxy.conf by the scaffold. +# Customize its VirtualHost and route blocks in the application repository. +# container_install.sh replaces every shell-style uppercase variable from the protected +# deployment environment and writes deployment/apache/01-reverse-proxy.conf. +# +# This baseline routes the service selected by ADDONS.apache.CONFIG_SERVICE. +# For a single application, or for that selected default service, use the short +# values INSTALL_PATH, APACHE_UPSTREAM_SERVICE, and APACHE_UPSTREAM_PORT below. +# INSTALL_PATH is read from the selected service's production/test settings. +# +# In a multi-application deployment, every service already exports its own +# prefix: api provides API_APP_PORT and API_INSTALL_PATH, web provides +# WEB_APP_PORT and WEB_INSTALL_PATH, and so on. Use those service-owned values +# directly in additional VirtualHost/path blocks; do not create APACHE_API_* or +# APACHE_WEB_* copies. +# +# MULTI-APPLICATION ROUTING SUMMARY +# 1. Put static/document ProxyPass exclusions and Alias directives before a +# matching catch-all ProxyPass. +# 2. A Django service named api uses API_INSTALL_PATH for mounted assets and +# API_APP_PORT for proxying to http://api:/. +# 3. A Django service named analysis similarly uses ANALYSIS_INSTALL_PATH and +# ANALYSIS_APP_PORT. Each Django service has its own mounted asset paths. +# 4. A React service named web normally needs only a proxy to +# http://web:/ because Nginx serves its compiled assets. +# 5. Use distinct URL prefixes or VirtualHosts so catch-all routes do not +# overlap. Complete copyable examples live in the Apache add-on README. + + ServerName ${APACHE_SERVER_NAME} + + ProxyRequests Off + ProxyPreserveHost On + AllowEncodedSlashes NoDecode + RequestHeader set X-Forwarded-Proto "${APACHE_FORWARDED_PROTO}" + RequestHeader set X-Forwarded-Port "${APACHE_FORWARDED_PORT}" + RequestHeader set X-Forwarded-Host "${APACHE_SERVER_NAME}" + + # Keep Apache and ModSecurity request-body limits aligned. + LimitRequestBody ${APACHE_LIMIT_REQUEST_BODY} + + SecRequestBodyLimit ${APACHE_LIMIT_REQUEST_BODY} + SecRequestBodyNoFilesLimit ${APACHE_LIMIT_REQUEST_BODY} + + + # These exclusions must precede the catch-all ProxyPass. The installer + # INSTALL_PATH comes from CONFIG_SERVICE. Compose mounts the selected + # service's static and documents sources at these exact container paths. + ProxyPass /static/ ! + Alias /static/ "${INSTALL_PATH}/static/" + + Require all granted + + + ProxyPass /documents/ ! + Alias /documents/ "${INSTALL_PATH}/documents/" + + Require all granted + + + # Default single-service route. Change or duplicate this block when the + # deployment exposes multiple applications or path prefixes. Prefixed + # values such as API_INSTALL_PATH remain available for additional blocks. + ProxyPass / http://${APACHE_UPSTREAM_SERVICE}:${APACHE_UPSTREAM_PORT}/ + ProxyPassReverse / http://${APACHE_UPSTREAM_SERVICE}:${APACHE_UPSTREAM_PORT}/ + ProxyTimeout ${APACHE_PROXY_TIMEOUT} + TimeOut ${APACHE_PROXY_TIMEOUT} + + CustomLog logs/${APACHE_LOG_STEM}-apache.access.log combined env=!forwarded + CustomLog logs/${APACHE_LOG_STEM}-apache.access.log proxy env=forwarded + ErrorLog logs/${APACHE_LOG_STEM}-apache.error.log + diff --git a/conf/apache/02-server-status.conf b/conf/apache/02-server-status.conf new file mode 100644 index 00000000..d03211e7 --- /dev/null +++ b/conf/apache/02-server-status.conf @@ -0,0 +1,14 @@ +# ExtendedStatus controls whether Apache will generate "full" status +# information when the "server-status" handler is called. +ExtendedStatus On + + + ServerName ${SERVER_STATUS_SERVER_NAME} + ServerAlias ${SERVER_STATUS_ALIASES} + + SetHandler server-status + Order deny,allow + Deny from all + Allow from ${SERVER_STATUS_ALLOW_FROM} + + diff --git a/container_install.sh b/container_install.sh index 777807c4..170da360 100755 --- a/container_install.sh +++ b/container_install.sh @@ -8,7 +8,7 @@ source "$script_dir/deployment/lib/container/common.sh" source "$script_dir/deployment/lib/container/django.sh" APP_VERSION="0.1.0" -APPLICATION_NAME="iSkyLIMS" +APPLICATION_NAME="Example Application" # ============================================================================ # GENERATED SERVICE/ADD-ON CUSTOMIZATION @@ -32,28 +32,28 @@ service_build_context_dir() { esac } service_environment_prefix() { - case "$1" in - app) echo APP ;; - *) return 1 ;; - esac + local prefix + array_contains "$1" "${install_services[@]}" || return 1 + prefix="${1^^}" + printf '%s\n' "${prefix//-/_}" } service_environment_value() { local prefix variable prefix="$(service_environment_prefix "$1")" || return 1 variable="${prefix}_$2" - printf '%s\n' "${!variable:-${3:-}}" + if [ -n "${!variable:-}" ]; then + printf '%s\n' "${!variable}" + elif [ "$#" -ge 3 ]; then + printf '%s\n' "$3" + else + die "$variable is required in the rendered service settings" + fi } service_repo_path() { - case "$1" in - app) service_environment_value "$1" REPO_PATH /srv/iskylims ;; - *) return 1 ;; - esac + service_environment_value "$1" REPO_PATH } service_install_path() { - case "$1" in - app) service_environment_value "$1" INSTALL_PATH /opt/iskylims ;; - *) return 1 ;; - esac + service_environment_value "$1" INSTALL_PATH } service_readiness_path() { case "$1" in @@ -86,16 +86,10 @@ service_container_install_conf() { esac } service_uid() { - case "$1" in - app) service_environment_value "$1" APP_UID 1212 ;; - *) return 1 ;; - esac + service_environment_value "$1" APP_UID } service_gid() { - case "$1" in - app) service_environment_value "$1" APP_GID 1212 ;; - *) return 1 ;; - esac + service_environment_value "$1" APP_GID } prepare_compose_environment() { @@ -105,13 +99,21 @@ prepare_compose_environment() { local -a deployment_values=( "GIT_REVISION|$git_revision" "APP_IMAGE|relecov-iskylims:local" - "APP_INSTALL_CONF_PATH|${install_conf_host_by_service[app]}" - "APACHE_LOG_PATH|$(config_value_or_default APACHE_LOG_PATH "${install_conf_host_by_service[app]}" /var/log/local/iskylims/apache)" - "APACHE_BIND_HOST|$(config_value_or_default APACHE_BIND_HOST "${install_conf_host_by_service[app]}" 0.0.0.0)" - "APACHE_PORT|$(config_value_or_default APACHE_PORT "${install_conf_host_by_service[app]}" 80)" - "APACHE_FORWARDED_PROTO|$(config_value_or_default APACHE_FORWARDED_PROTO "${install_conf_host_by_service[app]}" https)" - "APACHE_FORWARDED_PORT|$(config_value_or_default APACHE_FORWARDED_PORT "${install_conf_host_by_service[app]}" 443)" - "APACHE_LIMIT_REQUEST_BODY|$(config_value_or_default APACHE_LIMIT_REQUEST_BODY "${install_conf_host_by_service[app]}" 52428800)" + "APACHE_CONF_PATH|$(config_value_or_default APACHE_CONF_PATH "${install_conf_host_by_service[app]}" '')" + "APACHE_LOG_PATH|$(config_value_or_default APACHE_LOG_PATH "${install_conf_host_by_service[app]}" '')" + "APACHE_BIND_HOST|$(config_value_or_default APACHE_BIND_HOST "${install_conf_host_by_service[app]}" '')" + "APACHE_PORT|$(config_value_or_default APACHE_PORT "${install_conf_host_by_service[app]}" '')" + "APACHE_SERVER_NAME|$(config_value_or_default APACHE_SERVER_NAME "${install_conf_host_by_service[app]}" '')" + "APACHE_UPSTREAM_SERVICE|$(config_value_or_default APACHE_UPSTREAM_SERVICE "${install_conf_host_by_service[app]}" '')" + "APACHE_UPSTREAM_PORT|$(config_value_or_default APACHE_UPSTREAM_PORT "${install_conf_host_by_service[app]}" '')" + "APACHE_PROXY_TIMEOUT|$(config_value_or_default APACHE_PROXY_TIMEOUT "${install_conf_host_by_service[app]}" '')" + "APACHE_LOG_STEM|$(config_value_or_default APACHE_LOG_STEM "${install_conf_host_by_service[app]}" '')" + "SERVER_STATUS_SERVER_NAME|$(config_value_or_default SERVER_STATUS_SERVER_NAME "${install_conf_host_by_service[app]}" '')" + "SERVER_STATUS_ALIASES|$(config_value_or_default SERVER_STATUS_ALIASES "${install_conf_host_by_service[app]}" '')" + "SERVER_STATUS_ALLOW_FROM|$(config_value_or_default SERVER_STATUS_ALLOW_FROM "${install_conf_host_by_service[app]}" '')" + "APACHE_FORWARDED_PROTO|$(config_value_or_default APACHE_FORWARDED_PROTO "${install_conf_host_by_service[app]}" '')" + "APACHE_FORWARDED_PORT|$(config_value_or_default APACHE_FORWARDED_PORT "${install_conf_host_by_service[app]}" '')" + "APACHE_LIMIT_REQUEST_BODY|$(config_value_or_default APACHE_LIMIT_REQUEST_BODY "${install_conf_host_by_service[app]}" '')" ) compose_env_file="$script_dir/.env.${mode}.file" write_compose_environment_file "$compose_env_file" settings_sources deployment_values @@ -123,7 +125,7 @@ deployment_compose() { compose_exec --env-file "$compose_env_file" "$@" } current_service_container() { - deployment_compose -f "$compose_file" ps -q "$1" | tail -n 1 + resolve_service_container "$1" } # Each Django service renders its own protected host settings bind. React @@ -136,9 +138,43 @@ prepare_application_host_sources() { mkdir -p "$(dirname "$settings_output")" prepare_django_settings_bind_mount ./conf/template_settings.py "$settings_output" "${install_conf_host_by_service[app]}" fi + # conf/apache contains the application-owned Apache sources. Render every + # deployment value only after the protected settings environment is loaded, + # then expose the completed files as Compose bind sources. + local apache_source_dir="$script_dir/conf/apache" + local apache_output_dir="$script_dir/deployment/apache" + local apache_conf_name apache_config_service apache_log_path + apache_config_service=app + [ -d "$apache_source_dir" ] || { + echo "Apache source configuration directory not found: $apache_source_dir" >&2 + return 1 + } + mkdir -p "$apache_output_dir" + + export APACHE_SERVER_NAME="${APACHE_SERVER_NAME:?APACHE_SERVER_NAME is required}" + export APACHE_UPSTREAM_SERVICE="${APACHE_UPSTREAM_SERVICE:-$apache_config_service}" + export APACHE_UPSTREAM_PORT="${APACHE_UPSTREAM_PORT:-$(service_environment_value "$apache_config_service" APP_PORT)}" + # For the default route, INSTALL_PATH means the service selected by + # ADDONS.apache.CONFIG_SERVICE. Multi-service routes use their explicit + # API_INSTALL_PATH, WEB_INSTALL_PATH, etc. values instead. + export INSTALL_PATH="$(service_install_path "$apache_config_service")" + export APACHE_PROXY_TIMEOUT="${APACHE_PROXY_TIMEOUT:-$(service_environment_value "$apache_config_service" GUNICORN_TIMEOUT 120)}" + export APACHE_LOG_STEM="${APACHE_LOG_STEM:-$(normalize_apache_server_name "$APACHE_SERVER_NAME")}" + + for apache_conf_name in 00-logs.conf 01-reverse-proxy.conf 02-server-status.conf; do + [ -f "$apache_source_dir/$apache_conf_name" ] || { + echo "Apache source configuration not found: $apache_source_dir/$apache_conf_name" >&2 + return 1 + } + render_environment_config_template \ + "$apache_source_dir/$apache_conf_name" \ + "$apache_output_dir/$apache_conf_name" 0644 || return 1 + done + + # Production bind-mounts Apache logs from the host; tests use a named volume. if [ "$mode" = production ]; then - apache_log_path="${APACHE_LOG_PATH:-/var/log/local/iskylims/apache}" - mkdir -p "$script_dir/deployment/apache" "$apache_log_path" + apache_log_path="${APACHE_LOG_PATH:?APACHE_LOG_PATH is required}" + mkdir -p "$apache_log_path" fi } @@ -162,7 +198,9 @@ prepare_host_bind_source_permissions() { "$settings_path|$uid:$gid|0664" ) apply_host_permission_spec "${app_host_bind_permission_spec[@]}" - apache_log_path="${APACHE_LOG_PATH:-/var/log/local/iskylims/apache}" + # Generated proxy configuration is read-only in Apache. Its host files need + # traversal/read permissions, while the production log bind must be writable. + apache_log_path="${APACHE_LOG_PATH:?APACHE_LOG_PATH is required}" local -a apache_host_bind_permission_spec=( "$script_dir/deployment/apache|-|0755" "$script_dir/deployment/apache/00-logs.conf|-|0644" @@ -190,6 +228,8 @@ prepare_running_container_mount_permissions() { prepare_django_container_settings_permissions "$container_id" "$install_path/iskylims/settings.py" "$uid" "$gid" ;; apache) + # Apache currently needs no ownership repair inside its running + # container. Keep an explicit add-on policy ready for future mounts. local -a apache_running_mount_permission_spec=() apply_container_directory_permission_spec "$container_id" "${apache_running_mount_permission_spec[@]}" ;; @@ -204,6 +244,7 @@ bootstrap_service() { case "$service_name" in app) repo_path="$(service_repo_path "$service_name")" + # Fixed temporary in-container path; this is not operator configuration. runtime_conf=conf/.runtime_install_settings.txt [[ "$runtime_conf" == /* ]] || runtime_conf="$repo_path/$runtime_conf" uid="$(service_uid "$service_name")"; gid="$(service_gid "$service_name")" @@ -306,7 +347,7 @@ prepare_compose_environment load_compose_environment_file "$compose_env_file" prepare_application_host_sources prepare_host_bind_source_permissions -deployment_compose -f "$compose_file" config --quiet \ +deployment_compose -f "$compose_file" config \ || die "Compose configuration validation failed: $compose_file" # 5. Dispatch permission-only repair without building or bootstrapping. @@ -340,7 +381,7 @@ for service_name in "${install_services[@]}"; do --build-arg RENDER_DJANGO_SETTINGS=false \ --build-arg APP_REPO_PATH="$(service_repo_path "$service_name")" \ --build-arg APP_INSTALL_PATH="$(service_install_path "$service_name")" \ - --build-arg APP_PORT="$(service_environment_value "$service_name" APP_PORT 8000)" \ + --build-arg APP_PORT="$(service_environment_value "$service_name" APP_PORT)" \ --build-arg APP_UID="$(service_uid "$service_name")" \ --build-arg APP_GID="$(service_gid "$service_name")" \ --tag "$(service_image_name "$service_name")" "$context" @@ -352,8 +393,11 @@ for service_name in "${install_services[@]}"; do --tag "$(service_image_name "$service_name")" "$context" fi done -# 7. Start the complete topology from one Compose file. -deployment_compose -f "$compose_file" up -d +# 7. Start the complete topology from one Compose file. Recreate containers so +# a freshly built image is actually deployed; podman-compose 1.0.x may otherwise +# restart an existing named container that still references the previous image. +# Named volumes and bind-mounted persistent data are preserved. +deployment_compose -f "$compose_file" up -d --force-recreate # 8. Wait for every application service readiness contract. for service_name in "${install_services[@]}"; do diff --git a/deployment/apache/00-logs.conf b/deployment/apache/00-logs.conf deleted file mode 100644 index ad288787..00000000 --- a/deployment/apache/00-logs.conf +++ /dev/null @@ -1,8 +0,0 @@ -# Shared Apache log formats and persistent log destinations. -ErrorLog "logs/apache_error.log" -LogLevel warn -LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined - - LogFormat "%v %{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D %I %O" proxy - -CustomLog "logs/apache.access.log" combined diff --git a/deployment/apache/01-reverse-proxy.conf b/deployment/apache/01-reverse-proxy.conf deleted file mode 100644 index 6425b793..00000000 --- a/deployment/apache/01-reverse-proxy.conf +++ /dev/null @@ -1,31 +0,0 @@ -# Generated reverse-proxy contract. Edit ADDONS.apache in project.json, -# not this synchronized output. -# Separate virtual hosts are recommended for multi-app deployments and -# identity providers because each service keeps its native root URL. - - - ServerName localhost - ProxyRequests Off - ProxyPreserveHost On - AllowEncodedSlashes NoDecode - RequestHeader set X-Forwarded-Proto "${APACHE_FORWARDED_PROTO}" - RequestHeader set X-Forwarded-Port "${APACHE_FORWARDED_PORT}" - RequestHeader set X-Forwarded-Host "localhost" - LimitRequestBody ${APACHE_LIMIT_REQUEST_BODY} - ProxyPass /static/ ! - Alias /static/ /var/www/app/static/ - - Require all granted - - ProxyPass /documents/ ! - Alias /documents/ /var/www/app/documents/ - - Require all granted - - ProxyPass / http://app:${APP_APP_PORT}/ - ProxyPassReverse / http://app:${APP_APP_PORT}/ - ProxyTimeout 120 - TimeOut 120 - ErrorLog "logs/localhost.error.log" - CustomLog "logs/localhost.access.log" combined - diff --git a/deployment/apache/02-server-status.conf b/deployment/apache/02-server-status.conf deleted file mode 100644 index 6d1ee65f..00000000 --- a/deployment/apache/02-server-status.conf +++ /dev/null @@ -1,6 +0,0 @@ -# Local-only operational endpoint; never expose it through ProxyPass. -ExtendedStatus On - - SetHandler server-status - Require local - diff --git a/deployment/lib/container/common.sh b/deployment/lib/container/common.sh index b5522c8d..c4de4b48 100644 --- a/deployment/lib/container/common.sh +++ b/deployment/lib/container/common.sh @@ -512,7 +512,7 @@ render_config_template() { shift 2 escaped_token="$(sed_search_escape "$token")" escaped_value="$(sed_replacement_escape "$value")" - sed -i "s|$escaped_token|$escaped_value|g" "$tmp_file" + sed -E -i "s|$escaped_token|$escaped_value|g" "$tmp_file" done if copy_with_podman_fallback "$tmp_file" "$dst" \ && chmod_with_podman_fallback "$file_mode" "$dst"; then @@ -523,6 +523,36 @@ render_config_template() { return 1 } +# Render every ${UPPER_CASE_VARIABLE} found in a repository-owned configuration +# source from the already loaded deployment environment. This keeps application +# topology editable in normal configuration files while ensuring Compose mounts +# completed files with no unresolved deployment placeholders. +# Arguments: source configuration, destination file, destination mode. +render_environment_config_template() { + local src="$1" + local dst="$2" + local file_mode="$3" + local token variable + local -a replacements=() + + [ -f "$src" ] || { + echo "Configuration source not found: $src" >&2 + return 1 + } + while IFS= read -r token; do + [ -n "$token" ] || continue + variable="${token#\$\{}" + variable="${variable%\}}" + if ! [[ -v "$variable" ]]; then + echo "Required template variable $variable is not set for $src" >&2 + return 1 + fi + replacements+=("$token" "${!variable}") + done < <(grep -oE '\$\{[A-Z][A-Z0-9_]*\}' "$src" | sort -u || true) + + render_config_template "$src" "$dst" "$file_mode" "${replacements[@]}" +} + # Quote one value for literal use in a Compose environment file. Single-quoted # dotenv values are not interpolated; embedded apostrophes are escaped. compose_environment_quote() { @@ -742,7 +772,10 @@ service_exists() { } # Resolve a service to a container name/ID, supporting application legacy names -# first and standard Compose service labels as the portable fallback. +# first. Otherwise inspect every container returned by the selected Compose +# project and match its service label. Listing the project first avoids both a +# cross-project "app" collision and the unsupported `ps -q SERVICE` syntax in +# podman-compose 1.0.x. resolve_service_container() { local service_name="$1" local service_container="" @@ -756,7 +789,22 @@ resolve_service_container() { if [ -n "$container_name" ] \ && engine_exec inspect -f '{{.Id}}' "$container_name" >/dev/null 2>&1; then service_container="$container_name" - else + elif [ -n "${compose_file:-}" ]; then + local candidate candidate_service + while IFS= read -r candidate; do + [ -n "$candidate" ] || continue + candidate_service="$(engine_exec inspect -f \ + '{{ index .Config.Labels "com.docker.compose.service" }}' \ + "$candidate" 2>/dev/null || true)" + if [ "$candidate_service" = "$service_name" ]; then + service_container="$candidate" + break + fi + done < <(compose_with_env_exec -f "$compose_file" ps -q 2>/dev/null || true) + fi + # Retain the label query as a compatibility fallback for callers that do + # not have a Compose file in scope (including older application wrappers). + if [ -z "$service_container" ]; then service_container="$(engine_exec ps -a \ --filter "label=com.docker.compose.service=${service_name}" \ --format '{{.ID}}' | head -n 1)" diff --git a/deployment/lib/container/django.sh b/deployment/lib/container/django.sh index d2f4577b..0723d6ff 100644 --- a/deployment/lib/container/django.sh +++ b/deployment/lib/container/django.sh @@ -30,6 +30,7 @@ render_django_settings_file() { local install_conf_path="$3" local secret_line="" local template_secret_line="" + local django_debug="" if [ -f "$settings_path" ]; then secret_line="$(grep -E "^SECRET_KEY[[:space:]]*=" "$settings_path" | tail -n 1 || true)" @@ -43,21 +44,31 @@ render_django_settings_file() { echo "Django template '$template_path' has no SECRET_KEY assignment." >&2 return 1 fi + django_debug="$(config_value_or_default DJANGO_DEBUG "$install_conf_path" false)" + case "${django_debug,,}" in + true|1|yes|on) django_debug=True ;; + false|0|no|off) django_debug=False ;; + *) + echo "DJANGO_DEBUG must be a boolean value." >&2 + return 1 + ;; + esac render_config_template "$template_path" "$settings_path" 0664 \ "$template_secret_line" "$secret_line" \ djangouser "$(read_install_conf_first "$install_conf_path" DB_USER)" \ - djangopass "$(read_install_conf_first "$install_conf_path" DB_PASS DB_PASSWORD)" \ - djangohost "$(read_install_conf_first "$install_conf_path" DB_SERVER_IP DB_HOST)" \ + djangopass "$(read_install_conf_value DB_PASSWORD "$install_conf_path")" \ + djangohost "$(read_install_conf_value DB_HOST "$install_conf_path")" \ djangoport "$(read_install_conf_value DB_PORT "$install_conf_path")" \ djangodbname "$(read_install_conf_value DB_NAME "$install_conf_path")" \ - emailhostserver "$(read_install_conf_first "$install_conf_path" EMAIL_HOST_SERVER EMAIL_HOST)" \ + emailhostserver "$(read_install_conf_value EMAIL_HOST "$install_conf_path")" \ emailport "$(read_install_conf_value EMAIL_PORT "$install_conf_path")" \ emailhostuser "$(read_install_conf_value EMAIL_HOST_USER "$install_conf_path")" \ emailhostpassword "$(read_install_conf_value EMAIL_HOST_PASSWORD "$install_conf_path")" \ emailhosttls "$(read_install_conf_value EMAIL_USE_TLS "$install_conf_path")" \ + djangodebug "$django_debug" \ djangoallowedhosts "$(read_install_conf_value DJANGO_ALLOWED_HOSTS "$install_conf_path")" \ - localserverip "$(read_install_conf_value LOCAL_SERVER_IP "$install_conf_path")" \ - localhost "$(read_install_conf_first "$install_conf_path" DNS_URL DJANGO_ALLOWED_HOSTS)" + djangocsrftrustedorigins "$(read_install_conf_value DJANGO_CSRF_TRUSTED_ORIGINS "$install_conf_path")" \ + dbconnmaxage "$(config_value_or_default DB_CONN_MAX_AGE "$install_conf_path" 0)" } # Ensure the production settings bind source exists and reflects the selected @@ -67,7 +78,6 @@ prepare_django_settings_bind_mount() { local template_path="$1" local settings_path="$2" local install_conf_path="$3" - local configured_db_host="" [ "${mode:-production}" = "production" ] || return 0 if [ -d "$settings_path" ]; then @@ -76,12 +86,10 @@ prepare_django_settings_bind_mount() { fi mkdir -p "$(dirname "$settings_path")" - configured_db_host="$(read_install_conf_first "$install_conf_path" DB_SERVER_IP DB_HOST)" - if [ ! -f "$settings_path" ] \ - || grep -Eq "SECRET_KEY[[:space:]]*=[[:space:]]*SECRET|emailhosttls|djangouser|djangopass|djangohost|djangodbname" "$settings_path" \ - || ! grep -Fq -- "\"HOST\": \"$configured_db_host\"," "$settings_path"; then - render_django_settings_file "$template_path" "$settings_path" "$install_conf_path" - fi + # Always rerender so application-template and deployment-setting changes + # reach upgrades. render_django_settings_file preserves the existing + # non-placeholder SECRET_KEY, and render_config_template installs atomically. + render_django_settings_file "$template_path" "$settings_path" "$install_conf_path" chmod_with_podman_fallback 0664 "$settings_path" } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index d0fa5c1f..f45b57bd 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,47 +1,48 @@ services: # BEGIN BU-ISCIII SERVICE: app (django) app: - image: ${APP_IMAGE:-relecov-iskylims:local} + image: "${APP_IMAGE:-relecov-iskylims:local}" build: context: "." dockerfile: "Dockerfile" args: GIT_REVISION: ${GIT_REVISION:-current} + # Fixed internal path used only while install.sh reads the build secret. INSTALL_CONF: "conf/.runtime_install_settings.txt" USE_INSTALL_CONF_SECRET: "true" RENDER_DJANGO_SETTINGS: "false" - APP_UID: ${APP_APP_UID:-1212} - APP_GID: ${APP_APP_GID:-1212} - APP_PORT: ${APP_APP_PORT:-8000} - APP_REPO_PATH: ${APP_REPO_PATH:-/srv/iskylims} - APP_INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} + APP_UID: ${APP_APP_UID:?APP_APP_UID is required} + APP_GID: ${APP_APP_GID:?APP_APP_GID is required} + APP_PORT: ${APP_APP_PORT:?APP_APP_PORT is required} + APP_REPO_PATH: ${APP_REPO_PATH:?APP_REPO_PATH is required} + APP_INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} restart: unless-stopped - user: "${APP_APP_UID:-1212}:${APP_APP_GID:-1212}" + user: "${APP_APP_UID:?APP_APP_UID is required}:${APP_APP_GID:?APP_APP_GID is required}" environment: APP_MODE: prod - INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} - APP_INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} - APP_PORT: ${APP_APP_PORT:-8000} + INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} + APP_INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} + APP_PORT: ${APP_APP_PORT:?APP_APP_PORT is required} PROJECT_MODULE: "iskylims" DB_HOST: ${APP_DB_HOST:?APP_DB_HOST is required} - DB_PORT: ${APP_DB_PORT:-3306} + DB_PORT: ${APP_DB_PORT:?APP_DB_PORT is required} DB_NAME: ${APP_DB_NAME:?APP_DB_NAME is required} DB_USER: ${APP_DB_USER:?APP_DB_USER is required} DB_PASSWORD: ${APP_DB_PASSWORD:?APP_DB_PASSWORD is required} DJANGO_SECRET_KEY: ${APP_DJANGO_SECRET_KEY:?APP_DJANGO_SECRET_KEY is required} DJANGO_ALLOWED_HOSTS: ${APP_DJANGO_ALLOWED_HOSTS:?APP_DJANGO_ALLOWED_HOSTS is required} volumes: - - ${APP_HOST_DATA_PATH:?APP_HOST_DATA_PATH is required}/documents:${APP_INSTALL_PATH:-/opt/iskylims}/documents:Z - - ${APP_HOST_LOG_PATH:?APP_HOST_LOG_PATH is required}:${APP_INSTALL_PATH:-/opt/iskylims}/logs:Z - - ${APP_DJANGO_SETTINGS_PATH:?APP_DJANGO_SETTINGS_PATH is required}:${APP_INSTALL_PATH:-/opt/iskylims}/iskylims/settings.py:Z - - app_static:${APP_INSTALL_PATH:-/opt/iskylims}/static:z + - ${APP_HOST_DATA_PATH:?APP_HOST_DATA_PATH is required}/documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:Z + - ${APP_HOST_LOG_PATH:?APP_HOST_LOG_PATH is required}:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/logs:Z + - ${APP_DJANGO_SETTINGS_PATH:?APP_DJANGO_SETTINGS_PATH is required}:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/iskylims/settings.py:Z + - app_static:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/static:z healthcheck: test: ["CMD", "python", "-c", "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ['APP_PORT'] + '/health/', timeout=3)"] interval: 10s timeout: 5s retries: 12 ports: - - "127.0.0.1:${APP_APP_PORT:-8000}:${APP_APP_PORT:-8000}" + - "127.0.0.1:${APP_APP_PORT:?APP_APP_PORT is required}:${APP_APP_PORT:?APP_APP_PORT is required}" networks: [deployment_net] # END BU-ISCIII SERVICE: app # BEGIN BU-ISCIII ADDON: apache @@ -51,17 +52,10 @@ services: depends_on: app: condition: service_healthy - environment: - # REQUIRED reverse-proxy contract consumed by the generated Apache conf. - APACHE_FORWARDED_PROTO: ${APACHE_FORWARDED_PROTO:-https} - APACHE_FORWARDED_PORT: ${APACHE_FORWARDED_PORT:-443} - APACHE_LIMIT_REQUEST_BODY: ${APACHE_LIMIT_REQUEST_BODY:-52428800} - # REQUIRED application ports used by the generated proxy configuration. - APP_APP_PORT: ${APP_APP_PORT:-8000} ports: # Public binding can be restricted to loopback when another host proxy # terminates TLS before forwarding traffic to this container. - - "${APACHE_BIND_HOST:-0.0.0.0}:${APACHE_PORT:-80}:8080" + - "${APACHE_BIND_HOST:?APACHE_BIND_HOST is required}:${APACHE_PORT:?APACHE_PORT is required}:8080" expose: - "8080" healthcheck: @@ -71,19 +65,19 @@ services: retries: 30 start_period: 30s volumes: - # REQUIRED generated read-only configuration bind sources. + # REQUIRED runtime-rendered read-only configuration bind sources. - ./deployment/apache/00-logs.conf:/etc/httpd/conf.d/00-logs.conf:ro,z - ./deployment/apache/01-reverse-proxy.conf:/etc/httpd/conf.d/01-reverse-proxy.conf:ro,z - ./deployment/apache/02-server-status.conf:/etc/httpd/conf.d/02-server-status.conf:ro,z # REQUIRED persistent host log bind. Back it up/rotate per host policy. - - ${APACHE_LOG_PATH:-/var/log/local/iskylims/apache}:/var/log/httpd:z + - ${APACHE_LOG_PATH:?APACHE_LOG_PATH is required}:/var/log/httpd:z # REQUIRED when container and host timezone must agree in log records. - /etc/localtime:/etc/localtime:ro - /usr/share/zoneinfo:/usr/share/zoneinfo:ro # GENERATED MULTI-APP MOUNTS: one read-only static/documents pair for # every Django service. React assets remain inside the React image. - - app_static:/var/www/app/static:ro,z - - ${APP_HOST_DATA_PATH:?APP_HOST_DATA_PATH is required}/documents:/var/www/app/documents:ro,Z + - app_static:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/static:ro,z + - ${APP_HOST_DATA_PATH:?APP_HOST_DATA_PATH is required}/documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:ro,Z # OPTIONAL APP MOUNTS belong in ADDONS.apache.MOUNTS. Examples: # - reports_data:/var/www/reports:ro,z # - /srv/public-downloads:/var/www/downloads:ro,Z diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 8904e5df..8901e27b 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,48 +1,49 @@ services: # BEGIN BU-ISCIII SERVICE: app (django) app: - image: ${APP_IMAGE:-relecov-iskylims:local} + image: "${APP_IMAGE:-relecov-iskylims:local}" build: context: "." dockerfile: "Dockerfile" args: GIT_REVISION: ${GIT_REVISION:-current} + # Safe committed test settings are available inside the build context. INSTALL_CONF: "conf/docker_test_settings.txt" USE_INSTALL_CONF_SECRET: "false" RENDER_DJANGO_SETTINGS: "true" - APP_UID: ${APP_APP_UID:-1212} - APP_GID: ${APP_APP_GID:-1212} - APP_PORT: ${APP_APP_PORT:-8000} - APP_REPO_PATH: ${APP_REPO_PATH:-/srv/iskylims} - APP_INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} + APP_UID: ${APP_APP_UID:?APP_APP_UID is required} + APP_GID: ${APP_APP_GID:?APP_APP_GID is required} + APP_PORT: ${APP_APP_PORT:?APP_APP_PORT is required} + APP_REPO_PATH: ${APP_REPO_PATH:?APP_REPO_PATH is required} + APP_INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} restart: unless-stopped - user: "${APP_APP_UID:-1212}:${APP_APP_GID:-1212}" + user: "${APP_APP_UID:?APP_APP_UID is required}:${APP_APP_GID:?APP_APP_GID is required}" environment: APP_MODE: dev - INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} - APP_INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} - APP_PORT: ${APP_APP_PORT:-8000} + INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} + APP_INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} + APP_PORT: ${APP_APP_PORT:?APP_APP_PORT is required} PROJECT_MODULE: "iskylims" DB_HOST: app_db - DB_PORT: ${APP_DB_PORT:-3306} - DB_NAME: ${APP_DB_NAME:-iskylims} - DB_USER: ${APP_DB_USER:-django} - DB_PASSWORD: ${APP_DB_PASSWORD:-djangopass} - DJANGO_SECRET_KEY: ${APP_DJANGO_SECRET_KEY:-test-only-change-me} - DJANGO_ALLOWED_HOSTS: ${APP_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0} + DB_PORT: ${APP_DB_PORT:?APP_DB_PORT is required} + DB_NAME: ${APP_DB_NAME:?APP_DB_NAME is required} + DB_USER: ${APP_DB_USER:?APP_DB_USER is required} + DB_PASSWORD: ${APP_DB_PASSWORD:?APP_DB_PASSWORD is required} + DJANGO_SECRET_KEY: ${APP_DJANGO_SECRET_KEY:?APP_DJANGO_SECRET_KEY is required} + DJANGO_ALLOWED_HOSTS: ${APP_DJANGO_ALLOWED_HOSTS:?APP_DJANGO_ALLOWED_HOSTS is required} depends_on: app_db: condition: service_healthy volumes: - - app_test_documents:${APP_INSTALL_PATH:-/opt/iskylims}/documents:z - - app_test_static:${APP_INSTALL_PATH:-/opt/iskylims}/static:z + - app_test_documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:z + - app_test_static:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/static:z healthcheck: test: ["CMD", "python", "-c", "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ['APP_PORT'] + '/health/', timeout=3)"] interval: 10s timeout: 5s retries: 12 ports: - - "127.0.0.1:${APP_APP_PORT:-8000}:${APP_APP_PORT:-8000}" + - "127.0.0.1:${APP_APP_PORT:?APP_APP_PORT is required}:${APP_APP_PORT:?APP_APP_PORT is required}" networks: [deployment_net] # END BU-ISCIII SERVICE: app # BEGIN BU-ISCIII ADDON: apache @@ -52,15 +53,8 @@ services: depends_on: app: condition: service_healthy - environment: - # Keep proxy behavior equivalent to production while using local HTTP. - APACHE_FORWARDED_PROTO: ${APACHE_FORWARDED_PROTO:-http} - APACHE_FORWARDED_PORT: ${APACHE_FORWARDED_PORT:-8088} - APACHE_LIMIT_REQUEST_BODY: ${APACHE_LIMIT_REQUEST_BODY:-52428800} - # REQUIRED application ports used by the generated proxy configuration. - APP_APP_PORT: ${APP_APP_PORT:-8000} ports: - - "127.0.0.1:${APACHE_PORT:-8088}:8080" + - "${APACHE_BIND_HOST:?APACHE_BIND_HOST is required}:${APACHE_PORT:?APACHE_PORT is required}:8080" healthcheck: test: ["CMD-SHELL", "bash -ec '&2; exit 1; } -compose_run config --quiet - container_id="$(compose_run ps -q app)" +compose_run config >/dev/null + container_id="$(resolve_service_container app)" [ -n "$container_id" ] || fail "Service app has no container" - compose_run exec -T app bash -lc 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check && ! python manage.py showmigrations --plan | grep -F '"'"'[ ]'"'"'' + ensure_service_running app "$container_id" >/dev/null + engine_exec exec "$container_id" bash -lc 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check && ! python manage.py showmigrations --plan | grep -F '"'"'[ ]'"'"'' echo "PASS: app Django checks and migrations" check_url() { local service="$1" url="$2" @@ -36,5 +46,12 @@ check_url() { || { echo "FAIL: $service health endpoint: $url" >&2; return 1; } echo "PASS: $service health endpoint" } - check_url app "http://127.0.0.1:${APP_APP_PORT:-8000}/health/" -echo "iSkyLIMS deployment smoke test passed." +for service in "${install_services[@]}"; do + prefix="${service^^}" + prefix="${prefix//-/_}" + port_variable="${prefix}_APP_PORT" + port="${!port_variable:-}" + [ -n "$port" ] || fail "$port_variable is required in the rendered service settings" + check_url "$service" "http://127.0.0.1:${port}/health/" +done +echo "Example Application deployment smoke test passed." From 9862fd23212be1ae64f95376158e621e4431e41d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 10 Aug 2026 13:13:42 +0200 Subject: [PATCH 05/55] chore: update deployment scaffold configuration --- .bu-isciii-deployment/state.json | 43 ++++++----- README.md | 2 +- conf/INSTALL_SETTINGS.md | 22 +++++- conf/docker_production_settings.txt | 110 ++++++++++++++++++++++++---- conf/docker_test_settings.txt | 79 ++++++++++++++++---- deployment/project.json | 5 +- 6 files changed, 206 insertions(+), 55 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 08f2467b..421358f9 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -2,9 +2,8 @@ "config": { "ADDONS": { "apache": { - "VIRTUAL_HOSTS": { - "localhost": "app" - } + "CONFIG_SERVICE": "app", + "MOUNTS": [] } }, "APP_NAME": "iSkyLIMS", @@ -27,24 +26,28 @@ "TIMEZONE": "Europe/Madrid" }, "files": { - ".dockerignore": "461fb3f0597cc936bc0e6f9a4b50bf5e5a06fb03b12293725f57850ec1184eed", - ".gitignore": "8b93d5b7cd43c8c6cfeb8b1a9b3b0a1ef70c02b08c33859940a5f2ef86b19631", - "Dockerfile": "70c53323ca533bfc62e56a5fb3f116155757e9dafa5e33d9556257e80d70abb5", + ".dockerignore": "49a5b2223fb2a4f4201c19a833c60ba91f3e979632b6ae45fd4aca4cf22abb32", + ".gitignore": "5f08b2eb5e81fa7a1f4d5bc73df8ba6741e4a3f78925890c3fd1018f2188545b", + "Dockerfile": "805c9c06010f3f77d168071abef30df02cda65cb85740d5f6a87bee5a3c1efc5", "LEAME.md": "56279a4c2102a7ed42e34afe7c85055f6279fd9e67ee0df8a5d31c81c7ff9fb5", - "README.md": "7eeaaef44d091288de6285a306d0684b5b0e7aaab724f4a8fd9d2e48915a0d03", - "conf/INSTALL_SETTINGS.md": "9bd0fd353311652474a5b15feff79746e84732f193a7a469694a67fea71cb405", - "conf/docker_production_settings.txt": "39696a9cf4986529eb054b8f104b9956ebba9e5fd09601809706826e61e57c12", - "conf/docker_test_settings.txt": "af8db8079d135be9b95272c5d54821bdf5ad312ffd9e3047e96a391be4bd2c02", - "conf/template_settings.py": "5e286696097d51c662b6b79fbd9599bb4efa3c04f2f30b0a076686ac6583b8db", - "container_install.sh": "8f3104560fd39131bc51444ed9a72fdd0514bef177ab24f16e209211d27d68c3", - "deployment/apache/00-logs.conf": "f94afa73a965ce3c8b25796a9891bb52603f02155f75eb71bba41e9f54873438", - "deployment/apache/01-reverse-proxy.conf": "1cb982a49e14af9369c2d08f806102433eeb0a0ad8097f9dd6704043be18a797", - "deployment/apache/02-server-status.conf": "2dabf0ebe1b5a69c05cba8fc31490a15d82c89556fd22c4f85219bf752321613", - "docker-compose.prod.yml": "4e9f2a53099f626659302bcf313ff895cffdf0fa7555e0fd0ffb80441f928ab4", - "docker-compose.test.yml": "5a9c543d95b05e67205591c4ec0d6b6ab82d2dded01ddcaa2357c76c040ece8d", - "install.sh": "8ed305fd4f2c251bc02db4775ec47fc0e81b809bd5186b5e0ff33c7e99517bf5", - "scripts/container_start.sh": "7d1e7c015c5f95d25ee762493bf051b7c132d4cc10b91bf63f3fe1c5ed443d94", - "scripts/smoke_test.sh": "0a3c21b64674f9bc2178205b2a112d8e9f4a3da5603f33b97c3b20575eaff57b" + "README.md": "2adeeefd761f55a2e23c6a78ee05a70ee1badb56bb4ffbd198631480dfcfb04b", + "conf/INSTALL_SETTINGS.md": "edde8933e93c82460c430bdfb2ef95a0413f977a30d5128a93f280673b364cd2", + "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", + "conf/apache/01-reverse-proxy.conf": "5b8262212bef24ac9606861270dc6b6de63206e8d2b567e2cb09da33e84f683d", + "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", + "conf/docker_production_settings.txt": "e2c77474effacb8d9a460116c0467bf626777d96534386157c1c273e1f747f48", + "conf/docker_test_settings.txt": "b097040f09530d1c00f066fb99ad9d63b2c5e84ab0e6cad6b7cdacdd8e28542c", + "conf/template_settings.py": "590369c8b3db2e18b35deb04b2a4abf05e60f65d41ba0983b3b4af8ef20e5039", + "container_install.sh": "e2c2a45d5b9ab5c5a7756620cd941fec35f5561eea8840a509f3aeb422714d8b", + "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", + "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", + "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", + "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", + "docker-compose.prod.yml": "fccf8f382a57a4333a061710009021dad43ec3298c4ef487e82e3dc1f2eb6244", + "docker-compose.test.yml": "4e528d06fd480271e78990a296a341263b71b01b80601a7f74b805955e57f539", + "install.sh": "4ca3fe301fe7fefa5882d982dde97bb81a34396c054fe64660fc21fddb63f84b", + "scripts/container_start.sh": "5e7c07c03fd1b3b468dc8b2c4eaf6d9f63fb881cbad18f45c942dc39756a96da", + "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, "source": "BU-ISCIII deployment standards", "standard_version": "0.1.0" diff --git a/README.md b/README.md index 64110c43..2e62f76e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Profile behavior: Selected add-ons: -- Apache generates either DNS virtual hosts from `ADDONS.apache.VIRTUAL_HOSTS` or prefix routes from `ADDONS.apache.ROUTES`. +- Apache source configuration lives under `conf/apache/`; customize its virtual hosts and routes there. The installer renders final bind sources under `deployment/apache/`. All selected services and add-ons are assembled into one `docker-compose.prod.yml` and one `docker-compose.test.yml`. diff --git a/conf/INSTALL_SETTINGS.md b/conf/INSTALL_SETTINGS.md index 6901eead..1075635d 100644 --- a/conf/INSTALL_SETTINGS.md +++ b/conf/INSTALL_SETTINGS.md @@ -59,4 +59,24 @@ provider, proxy, worker, or frontend. ### Apache -`APACHE_LOG_PATH`, `APACHE_BIND_HOST`, `APACHE_PORT`, `APACHE_FORWARDED_PROTO`, `APACHE_FORWARDED_PORT`, and `APACHE_LIMIT_REQUEST_BODY` configure the generated proxy. They are operational values, not Django settings. +`APACHE_LOG_PATH`, `APACHE_BIND_HOST`, `APACHE_PORT`, +`APACHE_FORWARDED_PROTO`, `APACHE_FORWARDED_PORT`, and +`APACHE_LIMIT_REQUEST_BODY` configure the rendered proxy. They are +operational values, not Django or React application settings. + +`APACHE_SERVER_NAME` is the host name handled by the baseline VirtualHost. +`APACHE_UPSTREAM_SERVICE` defaults to `ADDONS.apache.CONFIG_SERVICE`, while +`APACHE_UPSTREAM_PORT` defaults to that service's `APP_PORT`. +`APACHE_PROXY_TIMEOUT` defaults to its `GUNICORN_TIMEOUT` (or 120 seconds), and +`APACHE_LOG_STEM` defaults to a filename-safe form of `APACHE_SERVER_NAME`. +Leave those four derived values empty unless the proxy route needs an override. + +`SERVER_STATUS_SERVER_NAME`, `SERVER_STATUS_ALIASES`, and +`SERVER_STATUS_ALLOW_FROM` configure the restricted Apache status endpoint. +Keep its allow-list limited to trusted diagnostic hosts. + +Edit the source files under `conf/apache/` to define the application's virtual +hosts, routes, and aliases. During installation they are rendered with the +protected deployment environment into `deployment/apache/`; only those final +files are bind-mounted. `APACHE_LOG_PATH` is the writable persistent host log +source. diff --git a/conf/docker_production_settings.txt b/conf/docker_production_settings.txt index 7c8a37c0..75240520 100644 --- a/conf/docker_production_settings.txt +++ b/conf/docker_production_settings.txt @@ -3,56 +3,136 @@ # The same file drives the ephemeral image build secret, host settings render, # Compose environment and temporary bootstrap copy. -### Application layout and runtime identity -REPO_PATH='/srv/iskylims' -INSTALL_PATH='/opt/iskylims' +### Application layout and Django project +# Source repository staged in the image and final application installation. +REPO_PATH='/srv/relecov-iskylims' +INSTALL_PATH='/opt/relecov-iskylims' +# Python package containing settings.py, urls.py and wsgi.py. This is generated +# from project.json and normally should not be changed in this file. PROJECT_MODULE='iskylims' +# Python executable used to create the application virtual environment. PYTHON_BIN_PATH='python3.11' + +### Application-specific installation modules +# Optional space-separated Django applications consumed by installers that +# support dependency, migration and fake-initial module lists. Leave empty when +# the application install.sh does not implement the corresponding behavior. +# iSkyLIMS example: REQUIRED_MODULES='core drylab wetlab django_utils' +REQUIRED_MODULES='core drylab wetlab django_utils' +MIGRATION_MODULES='core drylab wetlab django_utils' +FAKEINITIAL_MODULES='core drylab wetlab django_utils' + +### Container runtime identity +# UID/GID must own every writable bind mount and named volume. APP_UID='1212' APP_GID='1212' -APP_PORT='8000' +# Production services should normally use a non-login shell. +APP_SHELL='/sbin/nologin' +# Internal application port used by Gunicorn and the reverse proxy. +APP_PORT='8001' ### Persistent host sources -# Documents live below HOST_DATA_PATH; application logs use HOST_LOG_PATH. -HOST_DATA_PATH='/srv/containers/bind/iskylims' -HOST_LOG_PATH='/var/log/local/iskylims/apps' -# Host bind source rendered by container_install.sh before Compose startup. -DJANGO_SETTINGS_PATH='/srv/containers/bind/iskylims/settings/iskylims/settings.py' +# Documents are stored below HOST_DATA_PATH; application logs use +# HOST_LOG_PATH. Both paths must exist or be creatable by container_install.sh. +HOST_DATA_PATH='/srv/containers/bind/relecov-iskylims' +HOST_LOG_PATH='/var/log/local/relecov-iskylims/apps' +# Host bind source rendered before Compose starts. It must be a file path, not +# a directory; the parent directory is created and protected by the installer. +DJANGO_SETTINGS_PATH='/srv/containers/bind/relecov-iskylims/settings/iskylims/settings.py' ### External production database +# Use a host name or IP reachable from inside the application container. DB_HOST='CHANGE_ME' DB_PORT='3306' DB_NAME='CHANGE_ME' DB_USER='CHANGE_ME' DB_PASSWORD='CHANGE_ME' +# install.sh derives its legacy aliases from DB_HOST and DB_PASSWORD. ### Django security and public URLs +# Production debug must remain disabled. DJANGO_DEBUG='false' +# Preserve this value across upgrades; changing it invalidates signed data. DJANGO_SECRET_KEY='CHANGE_ME' +# Comma-separated host names accepted by Django, without URL schemes. DJANGO_ALLOWED_HOSTS='CHANGE_ME' +# Comma-separated complete HTTPS origins, including their schemes. DJANGO_CSRF_TRUSTED_ORIGINS='https://CHANGE_ME' +# Persistent database connection lifetime in seconds. +DB_CONN_MAX_AGE='60' + +### Application access compatibility +# Legacy application templates may still consume these values. Use the server +# address/host name expected in ALLOWED_HOSTS and the public DNS name used by +# the reverse proxy. Replace them while the legacy renderer remains in use. +LOCAL_SERVER_IP='CHANGE_ME' +DNS_URL='CHANGE_ME' ### Email +# SMTP endpoint reachable from the application container. EMAIL_HOST='CHANGE_ME' -EMAIL_PORT='587' +EMAIL_PORT='25' EMAIL_HOST_USER='CHANGE_ME' EMAIL_HOST_PASSWORD='CHANGE_ME' -EMAIL_USE_TLS='true' +# Python boolean spelling is retained because legacy settings rendering writes +# this value directly into settings.py. +EMAIL_USE_TLS='False' +# Compatibility alias used by the current RELECOV/iSkyLIMS install.sh. +EMAIL_HOST_SERVER='CHANGE_ME' ### Gunicorn and startup tuning WEB_CONCURRENCY='2' GUNICORN_THREADS='2' -GUNICORN_TIMEOUT='120' +GUNICORN_TIMEOUT='300' GUNICORN_KEEPALIVE='5' +# Maximum time container_start.sh waits for staged application files. APP_START_WAIT_TIMEOUT_SECONDS='100' +### Application log policy compatibility +# regular_folder stores logs below INSTALL_PATH. symbolic_link links that +# directory to LOG_PATH, which must already exist and be writable. +LOG_TYPE='regular_folder' +LOG_PATH='' + ### Selected infrastructure add-ons # Apache add-on # Used only when ADDONS.apache selects this application as CONFIG_SERVICE. -APACHE_LOG_PATH='/var/log/local/iskylims/apache' + +# Repository-owned sources live under /conf/apache. The installer +# renders them to /deployment/apache and mounts those final files; +# APACHE_CONF_PATH is not used by the standard lifecycle. Keep it empty unless +# legacy application code explicitly consumes a different host directory. +# Example legacy override: APACHE_CONF_PATH='/srv/containers/bind/apache-conf' +APACHE_CONF_PATH='' + +# Persistent host directory mounted as the Apache log destination. +APACHE_LOG_PATH='/var/log/local/relecov-iskylims/apache' + +# Public host interface and port. Use 127.0.0.1 when another host reverse proxy +# is the only component that should reach this Apache container. APACHE_BIND_HOST='0.0.0.0' -APACHE_PORT='80' +APACHE_PORT='8080' + +# Default VirtualHost rendered from conf/apache/01-reverse-proxy.conf. +# Customize that repository-owned file directly for additional DNS names, +# services, static aliases, or path-based routes. Empty upstream values are +# derived from ADDONS.apache.CONFIG_SERVICE by container_install.sh. +APACHE_SERVER_NAME='CHANGE_ME_DNS_NAME' +APACHE_UPSTREAM_SERVICE='' +APACHE_UPSTREAM_PORT='' +APACHE_PROXY_TIMEOUT='' +APACHE_LOG_STEM='' + +# Server-status stays restricted by default. Use a local-only diagnostic name; +# aliases and allow-list accept Apache-compatible values. +SERVER_STATUS_SERVER_NAME='localhost' +SERVER_STATUS_ALIASES='127.0.0.1 localhost' +SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' + +# Forwarded headers received by Django behind the production proxy. APACHE_FORWARDED_PROTO='https' APACHE_FORWARDED_PORT='443' -# 50 MiB; increase only when an application endpoint requires it. + +# 50 MiB. The reverse-proxy template applies this value to Apache and, when +# loaded, ModSecurity. Increase it only for endpoints that require larger data. APACHE_LIMIT_REQUEST_BODY='52428800' diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index eaa27b0a..322734ac 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -1,18 +1,28 @@ # Safe local/test values. Never use these credentials in production. -### Application layout and runtime identity -REPO_PATH='/srv/iskylims' -INSTALL_PATH='/opt/iskylims' + +### Application layout and Django project +REPO_PATH='/srv/relecov-iskylims' +INSTALL_PATH='/opt/relecov-iskylims' PROJECT_MODULE='iskylims' PYTHON_BIN_PATH='python3.11' + +### Application-specific installation modules +# Optional lists used only when install.sh implements these operations. +REQUIRED_MODULES='' +MIGRATION_MODULES='' +FAKEINITIAL_MODULES='' + +### Container runtime identity APP_UID='1212' APP_GID='1212' -APP_PORT='8000' +APP_SHELL='/bin/bash' +APP_PORT='8001' ### Disposable persistence paths -HOST_DATA_PATH='/tmp/iskylims/data' -HOST_LOG_PATH='/tmp/iskylims/logs' +HOST_DATA_PATH='/tmp/relecov-iskylims/data' +HOST_LOG_PATH='/tmp/relecov-iskylims/logs' # Test images render settings during build and do not mount this host path. -DJANGO_SETTINGS_PATH='/tmp/iskylims/data/settings/iskylims/settings.py' +DJANGO_SETTINGS_PATH='/tmp/relecov-iskylims/data/settings/iskylims/settings.py' ### Compose test database DB_HOST='db' @@ -21,25 +31,64 @@ DB_NAME='iskylims' DB_USER='django' DB_PASSWORD='djangopass' DB_ROOT_PASSWORD='root' +# install.sh derives its legacy aliases from DB_HOST and DB_PASSWORD. ### Django and local email defaults -DJANGO_DEBUG='true' +DJANGO_DEBUG='false' DJANGO_SECRET_KEY='test-only-change-me' DJANGO_ALLOWED_HOSTS='localhost,127.0.0.1,0.0.0.0' -EMAIL_HOST='localhost' -EMAIL_PORT='1025' -EMAIL_HOST_USER='' +DJANGO_CSRF_TRUSTED_ORIGINS='http://localhost:8001,http://127.0.0.1:8001' +DB_CONN_MAX_AGE='60' +LOCAL_SERVER_IP='*' +DNS_URL='*' + +### Local email defaults +EMAIL_HOST='host.docker.internal' +EMAIL_PORT='25' +EMAIL_HOST_USER='bioinformatica@isciii.es' EMAIL_HOST_PASSWORD='' -EMAIL_USE_TLS='false' +EMAIL_USE_TLS='False' +EMAIL_HOST_SERVER='host.docker.internal' + +### Gunicorn and startup tuning +WEB_CONCURRENCY='2' +GUNICORN_THREADS='2' +GUNICORN_TIMEOUT='300' GUNICORN_KEEPALIVE='5' APP_START_WAIT_TIMEOUT_SECONDS='100' +### Application log policy compatibility +# Set LOG_PATH when LOG_TYPE='symbolic_link'. +LOG_TYPE='regular_folder' +LOG_PATH='' + ### Selected infrastructure add-ons # Apache add-on # Used only when ADDONS.apache selects this application as CONFIG_SERVICE. -APACHE_LOG_PATH='/tmp/iskylims/apache-logs' + +# Sources under /conf/apache are rendered into deployment/apache. +# This compatibility value is unused unless application code consumes it. +APACHE_CONF_PATH='' + +# Disposable test logs and a loopback-only public endpoint. +APACHE_LOG_PATH='/tmp/relecov-iskylims/apache-logs' APACHE_BIND_HOST='127.0.0.1' -APACHE_PORT='8088' +APACHE_PORT='8081' + +# Default local VirtualHost. Empty upstream values are derived from the +# add-on's CONFIG_SERVICE by container_install.sh. +APACHE_SERVER_NAME='localhost' +APACHE_UPSTREAM_SERVICE='' +APACHE_UPSTREAM_PORT='' +APACHE_PROXY_TIMEOUT='' +APACHE_LOG_STEM='' + +# Local server-status defaults. +SERVER_STATUS_SERVER_NAME='localhost' +SERVER_STATUS_ALIASES='127.0.0.1 localhost' +SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' + +# Test proxy headers and request-body limit. APACHE_FORWARDED_PROTO='http' -APACHE_FORWARDED_PORT='8088' +APACHE_FORWARDED_PORT='8081' APACHE_LIMIT_REQUEST_BODY='52428800' diff --git a/deployment/project.json b/deployment/project.json index 132dce00..d7d53d72 100644 --- a/deployment/project.json +++ b/deployment/project.json @@ -19,9 +19,8 @@ }, "ADDONS": { "apache": { - "VIRTUAL_HOSTS": { - "localhost": "app" - } + "CONFIG_SERVICE": "app", + "MOUNTS": [] } } } From 501b8a16a9d46e30b7af23d68ccd4bacbb4531ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 10 Aug 2026 13:48:07 +0200 Subject: [PATCH 06/55] feat: align iSkyLIMS with deployment lifecycle --- .bu-isciii-deployment/state.json | 19 +- Dockerfile | 6 +- conf/INSTALL_SETTINGS.md | 7 + conf/apache/01-reverse-proxy.conf | 2 +- conf/docker_production_settings.txt | 45 +- conf/docker_test_settings.txt | 32 +- conf/template_install_settings.txt | 6 + container_install.sh | 2 +- deployment/project.json | 5 +- install.sh | 1643 ++++++++------------------- scripts/container_start.sh | 4 +- scripts/smoke_test.sh | 2 +- 12 files changed, 543 insertions(+), 1230 deletions(-) mode change 100644 => 100755 install.sh diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 421358f9..7ce81a62 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -2,12 +2,11 @@ "config": { "ADDONS": { "apache": { - "CONFIG_SERVICE": "app", - "MOUNTS": [] + "CONFIG_SERVICE": "app" } }, "APP_NAME": "iSkyLIMS", - "APP_SLUG": "iskylims", + "APP_SLUG": "relecov-iskylims", "DEFAULT_BRANCH": "main", "DESCRIPTION": "Laboratory information management system for wet-lab and dry-lab sequencing workflows.", "PYTHON_VERSION": "3.11", @@ -28,15 +27,15 @@ "files": { ".dockerignore": "49a5b2223fb2a4f4201c19a833c60ba91f3e979632b6ae45fd4aca4cf22abb32", ".gitignore": "5f08b2eb5e81fa7a1f4d5bc73df8ba6741e4a3f78925890c3fd1018f2188545b", - "Dockerfile": "805c9c06010f3f77d168071abef30df02cda65cb85740d5f6a87bee5a3c1efc5", + "Dockerfile": "1a3f8ccccfd84f0a59efda8446f53954fade8b200738a630420564eb8c6e7d5f", "LEAME.md": "56279a4c2102a7ed42e34afe7c85055f6279fd9e67ee0df8a5d31c81c7ff9fb5", "README.md": "2adeeefd761f55a2e23c6a78ee05a70ee1badb56bb4ffbd198631480dfcfb04b", - "conf/INSTALL_SETTINGS.md": "edde8933e93c82460c430bdfb2ef95a0413f977a30d5128a93f280673b364cd2", + "conf/INSTALL_SETTINGS.md": "bea1f3d22896ad7d8b13c3afc829c1d90f6453771b4c5a90a44a95b0084dc4d2", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", - "conf/apache/01-reverse-proxy.conf": "5b8262212bef24ac9606861270dc6b6de63206e8d2b567e2cb09da33e84f683d", + "conf/apache/01-reverse-proxy.conf": "7d22c5d707efe6190d799fd75d08114dd67ef9077548cd34d5df1f174bd45677", "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", - "conf/docker_production_settings.txt": "e2c77474effacb8d9a460116c0467bf626777d96534386157c1c273e1f747f48", - "conf/docker_test_settings.txt": "b097040f09530d1c00f066fb99ad9d63b2c5e84ab0e6cad6b7cdacdd8e28542c", + "conf/docker_production_settings.txt": "7676e176102c7cec75b5a07897eed4210178965da7b3fdc0f2b33a5feac6baab", + "conf/docker_test_settings.txt": "401665b86ab23a7eb4c35d75e7187794b628aee5adcebb5f0fa64aefae5f9f08", "conf/template_settings.py": "590369c8b3db2e18b35deb04b2a4abf05e60f65d41ba0983b3b4af8ef20e5039", "container_install.sh": "e2c2a45d5b9ab5c5a7756620cd941fec35f5561eea8840a509f3aeb422714d8b", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", @@ -45,8 +44,8 @@ "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", "docker-compose.prod.yml": "fccf8f382a57a4333a061710009021dad43ec3298c4ef487e82e3dc1f2eb6244", "docker-compose.test.yml": "4e528d06fd480271e78990a296a341263b71b01b80601a7f74b805955e57f539", - "install.sh": "4ca3fe301fe7fefa5882d982dde97bb81a34396c054fe64660fc21fddb63f84b", - "scripts/container_start.sh": "5e7c07c03fd1b3b468dc8b2c4eaf6d9f63fb881cbad18f45c942dc39756a96da", + "install.sh": "6d68766e042e068e2aad8a3cf69e5bbee0542d36c587bc12bd8d64d80a64404a", + "scripts/container_start.sh": "53c2989579848ef1a821d3be84df862ac1cb7aa157ad85b641023a6ea15e06e0", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, "source": "BU-ISCIII deployment standards", diff --git a/Dockerfile b/Dockerfile index 284c2d52..32e7cf58 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,8 @@ FROM registry.access.redhat.com/ubi9/ubi-minimal ARG APP_UID=1212 ARG APP_GID=1212 ARG APP_PORT=8001 -ARG APP_REPO_PATH=/srv/iskylims -ARG APP_INSTALL_PATH=/opt/iskylims +ARG APP_REPO_PATH=/srv/relecov-iskylims +ARG APP_INSTALL_PATH=/opt/relecov-iskylims ARG GIT_REVISION=current ARG INSTALL_CONF=conf/docker_test_settings.txt ARG USE_INSTALL_CONF_SECRET=false @@ -22,7 +22,7 @@ ENV APP_REPO_PATH=${APP_REPO_PATH} \ RUN microdnf -y update && \ microdnf -y install python3.11 python3.11-pip \ - python3.11-devel tar gcc git rsync wget mariadb-connector-c-devel shadow-utils && \ + tar gcc git rsync wget && \ microdnf clean all ARG SUPERCRONIC_VERSION=v0.2.38 diff --git a/conf/INSTALL_SETTINGS.md b/conf/INSTALL_SETTINGS.md index 1075635d..7e59cfb6 100644 --- a/conf/INSTALL_SETTINGS.md +++ b/conf/INSTALL_SETTINGS.md @@ -49,6 +49,13 @@ Email settings are required when the application sends operational or account messages. Document whether failed email blocks the user workflow and add an SMTP test to production acceptance. +## Initial administrator + +`CREATE_INITIAL_SUPERUSER=true` creates the first Django administrator only +during `--bootstrap install`. Set `DJANGO_SUPERUSER_USERNAME`, optional +`DJANGO_SUPERUSER_EMAIL`, and secret `DJANGO_SUPERUSER_PASSWORD` in the +protected settings file. Bootstrap retries never reset an existing account. + ## Project-specific settings Add every application setting here before declaring installation complete. diff --git a/conf/apache/01-reverse-proxy.conf b/conf/apache/01-reverse-proxy.conf index 2a73af98..4fa9083c 100644 --- a/conf/apache/01-reverse-proxy.conf +++ b/conf/apache/01-reverse-proxy.conf @@ -1,4 +1,4 @@ -# Apache reverse proxy source configuration for iskylims. +# Apache reverse proxy source configuration for relecov-iskylims. # # This file is copied to conf/apache/01-reverse-proxy.conf by the scaffold. # Customize its VirtualHost and route blocks in the application repository. diff --git a/conf/docker_production_settings.txt b/conf/docker_production_settings.txt index 75240520..bd1f69d3 100644 --- a/conf/docker_production_settings.txt +++ b/conf/docker_production_settings.txt @@ -17,8 +17,8 @@ PYTHON_BIN_PATH='python3.11' # Optional space-separated Django applications consumed by installers that # support dependency, migration and fake-initial module lists. Leave empty when # the application install.sh does not implement the corresponding behavior. -# iSkyLIMS example: REQUIRED_MODULES='core drylab wetlab django_utils' -REQUIRED_MODULES='core drylab wetlab django_utils' +# iSkyLIMS example: REQUIRED_MODULES='core drylab wetlab clinic django_utils' +REQUIRED_MODULES='core drylab wetlab clinic django_utils' MIGRATION_MODULES='core drylab wetlab django_utils' FAKEINITIAL_MODULES='core drylab wetlab django_utils' @@ -47,7 +47,6 @@ DB_PORT='3306' DB_NAME='CHANGE_ME' DB_USER='CHANGE_ME' DB_PASSWORD='CHANGE_ME' -# install.sh derives its legacy aliases from DB_HOST and DB_PASSWORD. ### Django security and public URLs # Production debug must remain disabled. @@ -61,24 +60,23 @@ DJANGO_CSRF_TRUSTED_ORIGINS='https://CHANGE_ME' # Persistent database connection lifetime in seconds. DB_CONN_MAX_AGE='60' -### Application access compatibility -# Legacy application templates may still consume these values. Use the server -# address/host name expected in ALLOWED_HOSTS and the public DNS name used by -# the reverse proxy. Replace them while the legacy renderer remains in use. -LOCAL_SERVER_IP='CHANGE_ME' -DNS_URL='CHANGE_ME' - ### Email # SMTP endpoint reachable from the application container. EMAIL_HOST='CHANGE_ME' EMAIL_PORT='25' EMAIL_HOST_USER='CHANGE_ME' EMAIL_HOST_PASSWORD='CHANGE_ME' -# Python boolean spelling is retained because legacy settings rendering writes -# this value directly into settings.py. +# Python boolean spelling is used because settings rendering writes this value +# directly into settings.py. EMAIL_USE_TLS='False' -# Compatibility alias used by the current RELECOV/iSkyLIMS install.sh. -EMAIL_HOST_SERVER='CHANGE_ME' + +### Initial administrator (bootstrap install only) +# Set to true only when a fresh installation should create its first Django +# administrator. Keep the protected production settings file mode at 0600. +CREATE_INITIAL_SUPERUSER='true' +DJANGO_SUPERUSER_USERNAME='admin' +DJANGO_SUPERUSER_EMAIL='' +DJANGO_SUPERUSER_PASSWORD='CHANGE_ME' ### Gunicorn and startup tuning WEB_CONCURRENCY='2' @@ -113,14 +111,25 @@ APACHE_LOG_PATH='/var/log/local/relecov-iskylims/apache' APACHE_BIND_HOST='0.0.0.0' APACHE_PORT='8080' -# Default VirtualHost rendered from conf/apache/01-reverse-proxy.conf. -# Customize that repository-owned file directly for additional DNS names, -# services, static aliases, or path-based routes. Empty upstream values are -# derived from ADDONS.apache.CONFIG_SERVICE by container_install.sh. +# Public host name handled by the default VirtualHost. Use localhost for a +# workstation deployment or the externally visible DNS name in production. APACHE_SERVER_NAME='CHANGE_ME_DNS_NAME' + +# Compose service receiving proxied requests. Leave empty to use the service +# selected by ADDONS.apache.CONFIG_SERVICE (normally app for a single service). APACHE_UPSTREAM_SERVICE='' + +# Internal application port used by ProxyPass. Leave empty to use APP_PORT from +# the selected service's production settings. APACHE_UPSTREAM_PORT='' + +# Maximum proxy request duration in seconds. Leave empty to use the selected +# service's GUNICORN_TIMEOUT, falling back to 120 seconds when it is undefined. APACHE_PROXY_TIMEOUT='' + +# Prefix for the generated -apache.access.log and +# -apache.error.log files. Leave empty to derive it from +# APACHE_SERVER_NAME, with URL schemes, paths and ports removed. APACHE_LOG_STEM='' # Server-status stays restricted by default. Use a local-only diagnostic name; diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index 322734ac..62cd25c5 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -8,9 +8,9 @@ PYTHON_BIN_PATH='python3.11' ### Application-specific installation modules # Optional lists used only when install.sh implements these operations. -REQUIRED_MODULES='' -MIGRATION_MODULES='' -FAKEINITIAL_MODULES='' +REQUIRED_MODULES='core drylab wetlab clinic django_utils' +MIGRATION_MODULES='core drylab wetlab django_utils' +FAKEINITIAL_MODULES='core drylab wetlab django_utils' ### Container runtime identity APP_UID='1212' @@ -31,7 +31,6 @@ DB_NAME='iskylims' DB_USER='django' DB_PASSWORD='djangopass' DB_ROOT_PASSWORD='root' -# install.sh derives its legacy aliases from DB_HOST and DB_PASSWORD. ### Django and local email defaults DJANGO_DEBUG='false' @@ -39,8 +38,6 @@ DJANGO_SECRET_KEY='test-only-change-me' DJANGO_ALLOWED_HOSTS='localhost,127.0.0.1,0.0.0.0' DJANGO_CSRF_TRUSTED_ORIGINS='http://localhost:8001,http://127.0.0.1:8001' DB_CONN_MAX_AGE='60' -LOCAL_SERVER_IP='*' -DNS_URL='*' ### Local email defaults EMAIL_HOST='host.docker.internal' @@ -48,7 +45,12 @@ EMAIL_PORT='25' EMAIL_HOST_USER='bioinformatica@isciii.es' EMAIL_HOST_PASSWORD='' EMAIL_USE_TLS='False' -EMAIL_HOST_SERVER='host.docker.internal' + +### Initial administrator (bootstrap install only) +CREATE_INITIAL_SUPERUSER='true' +DJANGO_SUPERUSER_USERNAME='admin' +DJANGO_SUPERUSER_EMAIL='admin@example.test' +DJANGO_SUPERUSER_PASSWORD='test-only-admin-password' ### Gunicorn and startup tuning WEB_CONCURRENCY='2' @@ -75,12 +77,24 @@ APACHE_LOG_PATH='/tmp/relecov-iskylims/apache-logs' APACHE_BIND_HOST='127.0.0.1' APACHE_PORT='8081' -# Default local VirtualHost. Empty upstream values are derived from the -# add-on's CONFIG_SERVICE by container_install.sh. +# Host name handled by the local test VirtualHost. APACHE_SERVER_NAME='localhost' + +# Compose service receiving proxied requests. Leave empty to use the service +# selected by ADDONS.apache.CONFIG_SERVICE (normally app for a single service). APACHE_UPSTREAM_SERVICE='' + +# Internal application port used by ProxyPass. Leave empty to use APP_PORT from +# the selected service's test settings. APACHE_UPSTREAM_PORT='' + +# Maximum proxy request duration in seconds. Leave empty to use the selected +# service's GUNICORN_TIMEOUT, falling back to 120 seconds when it is undefined. APACHE_PROXY_TIMEOUT='' + +# Prefix for the generated -apache.access.log and +# -apache.error.log files. Leave empty to derive it from +# APACHE_SERVER_NAME, with URL schemes, paths and ports removed. APACHE_LOG_STEM='' # Local server-status defaults. diff --git a/conf/template_install_settings.txt b/conf/template_install_settings.txt index bd99c0d7..dbc0a72a 100644 --- a/conf/template_install_settings.txt +++ b/conf/template_install_settings.txt @@ -59,6 +59,12 @@ EMAIL_HOST_USER='bioinformatica@isciii.es' EMAIL_HOST_PASSWORD='' EMAIL_USE_TLS='False' +### Initial administrator (bootstrap install only) +CREATE_INITIAL_SUPERUSER='true' +DJANGO_SUPERUSER_USERNAME='admin' +DJANGO_SUPERUSER_EMAIL='' +DJANGO_SUPERUSER_PASSWORD='' + ### Settings required for accessing iSkyLIMS # example: 172.0.0.1 LOCAL_SERVER_IP='' diff --git a/container_install.sh b/container_install.sh index 170da360..583919ca 100755 --- a/container_install.sh +++ b/container_install.sh @@ -8,7 +8,7 @@ source "$script_dir/deployment/lib/container/common.sh" source "$script_dir/deployment/lib/container/django.sh" APP_VERSION="0.1.0" -APPLICATION_NAME="Example Application" +APPLICATION_NAME="iSkyLIMS" # ============================================================================ # GENERATED SERVICE/ADD-ON CUSTOMIZATION diff --git a/deployment/project.json b/deployment/project.json index d7d53d72..a0023c48 100644 --- a/deployment/project.json +++ b/deployment/project.json @@ -1,6 +1,6 @@ { "APP_NAME": "iSkyLIMS", - "APP_SLUG": "iskylims", + "APP_SLUG": "relecov-iskylims", "DESCRIPTION": "Laboratory information management system for wet-lab and dry-lab sequencing workflows.", "REPOSITORY_URL": "https://github.com/BU-ISCIII/iSkyLIMS.git", "DEFAULT_BRANCH": "main", @@ -19,8 +19,7 @@ }, "ADDONS": { "apache": { - "CONFIG_SERVICE": "app", - "MOUNTS": [] + "CONFIG_SERVICE": "app" } } } diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index 28d265ba..ffe7d351 --- a/install.sh +++ b/install.sh @@ -1,1222 +1,501 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail install_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$install_script_dir" # shellcheck disable=SC1091 source "$install_script_dir/deployment/lib/container/common.sh" # shellcheck disable=SC1091 source "$install_script_dir/deployment/lib/container/django.sh" -# All repository-relative inputs (conf, application modules and requirements) -# belong to the source tree containing this script. Container bootstrap invokes -# this file by absolute path while its working directory is INSTALL_PATH, so -# normalize the working directory before resolving any of those inputs. -cd "$install_script_dir" - APP_VERSION="3.1.1" +ACTION="install" +OPERATION_SCOPE="full" +WORKFLOW="standard" +GIT_REVISION="current" +INSTALL_CONF="./install_settings.txt" +LOAD_TABLES="false" +SKIP_TABLES="false" +SKIP_APACHE_RESTART="false" +SCRIPT_BEFORE=() +SCRIPT_AFTER=() +RENDER_SETTINGS="auto" +SETTINGS_OUTPUT="" +INITIAL_GIT_REF="" -# usage: prints the command line help and usage examples. usage() { -cat << EOF -This script install and upgrade the iskylims app. - -usage : $0 --upgrade --git_revision --conf - Optional input data: - --install | Install iskylims full/dep/app - --upgrade | Upgrade iskylims full/dep/app - --stage | Stage app files only (install/upgrade) without DB work. Internal/container use. - --bootstrap | Run DB/bootstrap steps only (install/upgrade) against an existing staged app. Internal/container use. - --render-settings | Render settings.py while staging (enabled automatically for non-container installs). - --settings-output | Override the rendered settings.py destination. - --git_revision | Git revision name to run (branch, tag, commit SHA, or 'current' to use copied local sources as-is) - --conf | Select custom configuration file. Default: ./install_settings.txt - --tables | Load the first inital tables (from conf folder) - --skip_tables | Skip loading initial tables (even during install) - --script | Run a migration script after migrations. - --script_before | Run a migration script before migrations. - --script_after | Run a migration script after migrations (same as --script). - --ren_app | Rename apps required for the upgrade migration to 3.0.0 - --docker | Deprecated. Use --skip_apache_restart to avoid Apache checks/restart. - + cat <<'EOF' +Install, stage, or bootstrap iSkyLIMS. + +Usage: ./install.sh [options] + + --install full|dep|app Install dependencies, application, or both. + --upgrade full|dep|app Upgrade dependencies, application, or both. + --stage install|upgrade Stage an immutable image; never touch the DB. + --bootstrap install|upgrade Bootstrap an already staged application. + --git_revision Branch, tag, commit, or current (default). + --conf Normalized installation settings file. + --render-settings Render Django settings during staging. + --settings-output Override the rendered settings destination. + --tables Load conf/first_install_tables.json. + --skip_tables Never load the initial fixture. + --script_before Repeatable pre-migrate django-extensions hook. + --script_after Repeatable post-migrate hook. + --script Alias for --script_after. + --docker Deprecated alias for --skip_apache_restart. + --skip_apache_restart Do not restart a host Apache service. + --help + --version Examples: - Install iskylims only dep - sudo $0 --install dep - - Install only iSkyLIMS app - $0 --install app - - Upgrade using develop code - $0 --upgrade full --git_revision develop - - Upgrade running migration script and update initial tables - $0 --upgrade full --script --tables - - Stage and render application files for a non-sensitive test image - $0 --stage install --git_revision main --conf conf/docker_test_settings.txt --render-settings - - Bootstrap database/static using an already staged container image - $0 --bootstrap upgrade --git_revision main --conf conf/docker_production_settings.txt - - Make adjustments for apps renaming in upgrade 2.3.0 to 2.3.1 - $0 --upgrade full --ren_app --script --tables - - Upgrade running pre/post migration scripts: - $0 --upgrade app --script_before --script_after + ./install.sh --install full --conf conf/docker_test_settings.txt --tables + ./install.sh --upgrade app --git_revision v2.0.0 --script_before prepare_v2 + ./install.sh --stage install --conf conf/docker_test_settings.txt --render-settings + ./install.sh --bootstrap upgrade --conf /tmp/runtime_install_settings.txt EOF } -# log: write timestamped log entries to stdout. -_log_compose_entry() { - local level="$1"; shift - local message="$*" - local timestamp - timestamp="$(date '+%Y-%m-%d %H:%M:%S')" - printf "%s [%s] %s" "$timestamp" "$level" "$message" -} - -log() { - local level="$1"; shift - local message="$*" - local entry - entry="$(_log_compose_entry "$level" "$message")" - printf "%s\n" "$entry" -} - -# db_check: verifies connectivity to the configured MySQL instance using mysqladmin/mysqlshow. -db_check(){ - log "INFO" "Checking database connectivity against $DB_SERVER_IP:$DB_PORT" - local mysqladmin_bin - local mysqlshow_bin - mysqladmin_bin="$(command -v mysqladmin || command -v mariadb-admin || true)" - mysqlshow_bin="$(command -v mysqlshow || command -v mariadb-show || true)" - - if [ -z "$mysqladmin_bin" ] || [ -z "$mysqlshow_bin" ]; then - log "ERROR" "mysql client tools not found (mysqladmin/mysqlshow or mariadb-admin/mariadb-show)." - exit 1 - fi - - MYSQL_PWD="$DB_PASSWORD" "$mysqladmin_bin" \ - --host="$DB_HOST" --user="$DB_USER" --port="$DB_PORT" \ - processlist > /dev/null - - if ! [ $? -eq 0 ]; then - log "ERROR" "Unable to connect to database. Check if your database is running and accessible" - exit 1 - fi - RESULT="$(MYSQL_PWD="$DB_PASSWORD" "$mysqlshow_bin" \ - --user="$DB_USER" --host="$DB_HOST" --port="$DB_PORT" \ - | grep -o "$DB_NAME")" - - if ! [ "$RESULT" == "$DB_NAME" ] ; then - log "ERROR" "iskylims database is not defined yet" - log "ERROR" "Create iskylims database on your mysql server and run again the installation script" - exit 1 - fi -} - -# apache_check: ensures apache/httpd service is running depending on distribution. -apache_check(){ - if [[ $linux_distribution == "Ubuntu" ]]; then - if ! pidof apache2 > /dev/null ; then - log "WARN" "Apache Server is down... Trying to restart Apache" - systemctl restart apache2.service - sleep 10 - if pidof apache2 > /dev/null ; then - log "INFO" "Apache Server is up" - else - log "ERROR" "Unable to start Apache" - log "ERROR" "Solve the issue with Apache server and run again the installation script" - exit 1 - fi - fi - elif [[ $linux_distribution == "CentOs" || $linux_distribution == "RedHatEnterprise" ]]; then - if ! pidof httpd > /dev/null ; then - log "WARN" "Apache Server is down... Trying to restart Apache" - systemctl restart httpd - sleep 10 - if pidof httpd > /dev/null ; then - log "INFO" "Apache Server is up" - else - log "ERROR" "Unable to start Apache" - log "ERROR" "Solve the issue with Apache server and run again the installation script" - exit 1 - fi - fi - fi -} - -# python_check: confirm required Python version is available in PYTHON_BIN_PATH. -python_check(){ - python_version=$(su -c $PYTHON_BIN_PATH --version $user 2>&1) - if [[ $python_version == "" ]]; then - log "ERROR" "Python3 is not found in your system" - log "ERROR" "Solve the issue with Python and run again the installation script" - exit 1 - fi - p_version=$(echo $python_version | cut -d"." -f2) - if (( $p_version < 7 )); then - log "ERROR" "Application requires at least version 3.7.x of Python3" - log "ERROR" "Solve the issue with python and run again the installation script" - exit 1 - fi -} - -# root_check: enforce running privileged sections as root. -root_check(){ - if [[ $EUID -ne 0 ]]; then - log "ERROR" "Exiting installation. This script must be run as root" - exit 1 - fi -} - -# update_settings_and_urls: install URL configuration and optionally render settings. -# Image staging skips production rendering unless --render-settings is explicit; -# production container_install.sh renders the persistent host bind source. -update_settings_and_urls(){ - log "INFO" "Updating settings.py and urls.py with deployment values" - local project_dir="$INSTALL_PATH/$PROJECT_NAME" - cp conf/urls.py "$project_dir" - if [ "$render_settings" = true ]; then - render_django_settings_file \ - "$install_script_dir/conf/template_settings.py" \ - "${settings_output:-$project_dir/settings.py}" \ - "$conf" - else - log "INFO" "Skipping settings.py rendering for staged production image" - fi -} - -# restore_git_ref: reset repository to branch/tag/commit active before script ran. -restore_git_ref() { - [ -n "${initial_git_ref:-}" ] || return 0 - echo "Restoring to initial git reference: $initial_git_ref" - git -C "$install_script_dir" checkout "$initial_git_ref" --quiet -} - -# load_tables: wrapper to call Django loaddata with optional verbosity. -load_tables() { - # Function parameters - local data_file="${1:-conf/first_install_tables.json}" - local verbose="${2:-false}" - - # Check if the file exists - if [[ ! -f "$data_file" ]]; then - echo "Error: The data file '$data_file' does not exist." - return 1 - fi - - # Conditional message based on verbose mode - if [[ "$verbose" == true ]]; then - echo "Loading pre-filled tables from file: $data_file" - fi - - # Load pre-filled tables - python manage.py loaddata "$data_file" - if [[ $? -eq 0 ]]; then - echo "Tables loaded successfully from '$data_file'." - else - echo "Error loading tables from '$data_file'." - return 1 - fi - - if [[ "$verbose" == true ]]; then - echo "Table loading process completed." - fi -} - -# ensure_git_safe_directory: avoid Git "dubious ownership" failures in containerized installs. -ensure_git_safe_directory() { - local repo_dir - repo_dir="$(pwd -P)" - - if [ -d "$repo_dir/.git" ] || [ -f "$repo_dir/.git" ]; then - git config --global --add safe.directory "$repo_dir" >/dev/null 2>&1 || true - git config --system --add safe.directory "$repo_dir" >/dev/null 2>&1 || true - fi -} +die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } +info() { printf 'INFO: %s\n' "$*"; } +command_required() { command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"; } + +while (($#)); do + case "$1" in + --git_revision) GIT_REVISION="${2:-}"; shift 2 ;; + --conf) INSTALL_CONF="${2:-}"; shift 2 ;; + --render-settings) RENDER_SETTINGS="true"; shift ;; + --settings-output) SETTINGS_OUTPUT="${2:-}"; shift 2 ;; + --stage|--bootstrap) + WORKFLOW="${1#--}" + [[ "${2:-}" =~ ^(install|upgrade)$ ]] || die "$1 requires install or upgrade" + ACTION="$2"; OPERATION_SCOPE="app"; shift 2 + ;; + --install|--upgrade) + ACTION="${1#--}"; WORKFLOW="standard" + [[ "${2:-}" =~ ^(full|dep|app)$ ]] || die "$1 requires full, dep, or app" + OPERATION_SCOPE="$2"; shift 2 + ;; + --script_before) [[ -n "${2:-}" ]] || die "$1 requires a value"; SCRIPT_BEFORE+=("$2"); shift 2 ;; + --script_after|--script) [[ -n "${2:-}" ]] || die "$1 requires a value"; SCRIPT_AFTER+=("$2"); shift 2 ;; + --tables) LOAD_TABLES="true"; shift ;; + --skip_tables) SKIP_TABLES="true"; LOAD_TABLES="false"; shift ;; + --docker|--skip_apache_restart) SKIP_APACHE_RESTART="true"; shift ;; + --help) usage; exit 0 ;; + --version) echo "$APP_VERSION"; exit 0 ;; + *) die "Unknown option: $1" ;; + esac +done -# A staged container image intentionally excludes .git. Git state is therefore -# optional: source staging may select a revision in a real checkout, whereas -# runtime bootstrap operates only on the immutable files already in the image. -initial_git_ref="" -if git -C "$install_script_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - ensure_git_safe_directory - initial_git_ref="$(git -C "$install_script_dir" rev-parse --abbrev-ref HEAD \ - || git -C "$install_script_dir" rev-parse HEAD)" - trap restore_git_ref EXIT +# iSkyLIMS ships its reference catalog as part of a fresh installation. Keep +# upgrades opt-in so existing operator-managed data is never reloaded. +if [[ "$ACTION" == "install" && "$SKIP_TABLES" == "false" ]]; then + LOAD_TABLES="true" fi -#================================================================ -#SET TEMINAL COLORS -#================================================================ -YELLOW='\033[0;33m' -WHITE='\033[0;37m' -CYAN='\033[0;36m' -BLUE='\033[0;34m' -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' -ORANGE='\033[0;33m' - -# log_section: print a visually separated header in both console and log. -log_section() { - local message="$1" - log "INFO" "$message" - printf "\n\n%s\n" "${YELLOW}------------------${NC}" - printf "%b\n" "${YELLOW}${message}${NC}" - printf "%s\n\n" "${YELLOW}------------------${NC}" -} - -# log_info: convenience helper for blue info messages (console only). -log_info() { - printf "%b\n" "${BLUE}$(_log_compose_entry "INFO" "$1")${NC}" -} - -# log_warn: emit warning text in cyan for terminal visibility. -log_warn() { - printf "%b\n" "${CYAN}$(_log_compose_entry "WARN" "$1")${NC}" -} - -# log_error: emit error text in red for terminal visibility. -log_error() { - printf "%b\n" "${RED}$(_log_compose_entry "ERROR" "$1")${NC}" -} - -# abort_install: log an error and exit with optional status. -abort_install() { - log_error "$1" - exit "${2:-1}" -} +[[ "$WORKFLOW" != "stage" || ${#SCRIPT_BEFORE[@]} -eq 0 && ${#SCRIPT_AFTER[@]} -eq 0 ]] \ + || die "Migration scripts cannot run during the stage workflow" +[[ -f "$INSTALL_CONF" ]] || die "Configuration not found: $INSTALL_CONF" +if [[ "$INSTALL_CONF" != /* ]]; then + INSTALL_CONF="$(cd "$(dirname "$INSTALL_CONF")" && pwd)/$(basename "$INSTALL_CONF")" +fi +if [[ "$WORKFLOW" != "stage" ]] && grep -Eq "^[A-Z0-9_]+=.*CHANGE_ME" "$INSTALL_CONF"; then + die "Configuration still contains CHANGE_ME values" +fi +# shellcheck disable=SC1090 +source "$INSTALL_CONF" +: "${INSTALL_PATH:?INSTALL_PATH is required}" +: "${PROJECT_MODULE:?PROJECT_MODULE is required}" +: "${PYTHON_BIN_PATH:?PYTHON_BIN_PATH is required}" +: "${DB_HOST:?DB_HOST is required}" +: "${DB_PORT:?DB_PORT is required}" +: "${DB_NAME:?DB_NAME is required}" +: "${DB_USER:?DB_USER is required}" +: "${DB_PASSWORD:?DB_PASSWORD is required}" +REQUIRED_MODULES="${REQUIRED_MODULES:-}" +MIGRATION_MODULES="${MIGRATION_MODULES:-}" -chown_if_root() { - if [ "$EUID" -eq 0 ]; then - chown "$@" - else - log_warn "Skipping chown (requires root): chown $*" - fi -} +if [[ "$RENDER_SETTINGS" == "auto" ]]; then + [[ "$WORKFLOW" == "standard" ]] && RENDER_SETTINGS="true" || RENDER_SETTINGS="false" +fi -ensure_file_exists() { - local file_path="$1" - local friendly_name="${2:-$1}" - if [ ! -f "$file_path" ]; then - abort_install "Required file '$friendly_name' not found." - fi +remember_git_ref() { + git -C "$install_script_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0 + INITIAL_GIT_REF="$(git -C "$install_script_dir" symbolic-ref --quiet --short HEAD \ + || git -C "$install_script_dir" rev-parse HEAD)" } -# load_install_config: source the selected install_settings file. -load_install_config() { - ensure_file_exists "$conf" "$conf" - # shellcheck disable=SC1090 - . "$conf" +restore_git_ref() { + [[ -n "$INITIAL_GIT_REF" ]] || return 0 + git -C "$install_script_dir" checkout --quiet "$INITIAL_GIT_REF" || \ + printf 'WARNING: could not restore git revision %s\n' "$INITIAL_GIT_REF" >&2 } -# checkout_git_revision: ensure desired git revision exists and check it out safely. checkout_git_revision() { - if [[ "$git_branch" == "current" ]]; then - printf "${YELLOW}Using copied local working tree without git checkout.${NC}\n" - return 0 - fi - if [ -z "$initial_git_ref" ]; then - abort_install "Git revision $git_branch cannot be selected because the staged source contains no .git metadata." - fi - if git -C "$install_script_dir" rev-parse --verify "$git_branch" >/dev/null 2>&1; then - if [[ $git_branch != $initial_git_ref ]]; then - local local_changes - local_changes=$(git -C "$install_script_dir" status --porcelain) - if [[ -n $local_changes ]]; then - abort_install "Unable to switch to $git_branch. Commit or stash local changes first." - fi - printf "${YELLOW}Switching to revision %s.${NC}\n" "$git_branch" - git -C "$install_script_dir" checkout "$git_branch" --quiet - else - printf "${YELLOW}Using current revision: '%s'.${NC}\n" "$git_branch" - fi - else - abort_install "Git reference $git_branch is not defined in ${PWD}." - fi -} - -# check_requirements: run Python/DB/Apache/root validations before install/upgrade. -check_requirements() { - log_section "Checking main requirements" - python_check - log_info "Valid version of Python" - if [[ "$operation_scope" == "full" || "$operation_scope" == "app" ]]; then - db_check - log_info "Successful check for database" - if [ "$restart_apache" = true ]; then - apache_check - log_info "Successful check for apache" - fi - fi - - if [ "$install_type" == "full" ] || [ "$install_type" == "dep" ] || [ "$upgrade_type" == "full" ] || [ "$upgrade_type" == "dep" ]; then - log_warn "Checking requirement of root user when installation is full or dep" - root_check - log_info "Successful checking of root user" - fi -} - -check_stage_requirements() { - log_section "Checking requirements for staged app preparation" - python_check - log_info "Valid version of Python" -} - -check_bootstrap_requirements() { - log_section "Checking requirements for application bootstrap" - python_check - log_info "Valid version of Python" - db_check - log_info "Successful check for database" -} - -# rename_apps_if_needed: handles legacy app renaming and DB/migration adjustments when --ren_app is provided. -rename_apps_if_needed() { - if [ $ren_app != true ]; then - return 0 - fi - - rm -rf $INSTALL_PATH/django_utils/migrations/* - rm -rf $INSTALL_PATH/iSkyLIMS_core/migrations/* - rm -rf $INSTALL_PATH/iSkyLIMS_wetlab/migrations/* - rm -rf $INSTALL_PATH/iSkyLIMS_drylab/migrations/* - - cd $INSTALL_PATH - sed -i "s/ugettext/gettext/g" iSkyLIMS_wetlab/models.py - sed -i "s/ugettext/gettext/g" iSkyLIMS_core/forms.py - sed -i "s/ugettext/gettext/g" django_utils/forms.py - echo "activate the virtualenv" - source virtualenv/bin/activate - - echo "Create a fake initial" - python manage.py makemigrations $FAKEINITIAL_MODULES - python manage.py migrate --fake-initial - - if [ -d "$INSTALL_PATH/iSkyLIMS_core" ]; then - echo "Changing app dir names in $INSTALL_PATH..." - rm -rf $INSTALL_PATH/.git $INSTALL_PATH/.github $INSTALL_PATH/.gitignore \ - $INSTALL_PATH/.Rhistory $INSTALL_PATH/docker-compose.test.yml $INSTALL_PATH/docker_iskylims_install.sh \ - $INSTALL_PATH/Dockerfile $INSTALL_PATH/install.sh $INSTALL_PATH/install_settings.txt - mv $INSTALL_PATH/iSkyLIMS_core $INSTALL_PATH/core - mv $INSTALL_PATH/iSkyLIMS_wetlab $INSTALL_PATH/wetlab - mv $INSTALL_PATH/iSkyLIMS_drylab $INSTALL_PATH/drylab - mv $INSTALL_PATH/iSkyLIMS_clinic $INSTALL_PATH/clinic - echo "Done changing app dir names in $INSTALL_PATH..." - fi - if [ -d "iSkyLIMS" ]; then - mv iSkyLIMS/ iskylims/ - sed -i "s/iSkyLIMS/iskylims/g" $INSTALL_PATH/iskylims/wsgi.py - sed -i "s/iSkyLIMS/iskylims/g" $INSTALL_PATH/manage.py - fi - - echo "Modifying database names and constraints..." - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_content_type SET app_label = REPLACE(app_label , "iSkyLIMS_core", "core") WHERE app_label like ("iSkyLIMS_%");' - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_content_type SET app_label = REPLACE(app_label , "iSkyLIMS_wetlab", "wetlab") WHERE app_label like ("iSkyLIMS_%");' - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_content_type SET app_label = REPLACE(app_label , "iSkyLIMS_drylab", "drylab") WHERE app_label like ("iSkyLIMS_%");' - - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_migrations SET app = REPLACE(app , "iSkyLIMS_core", "core") WHERE app like ("iSkyLIMS_%");' - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_migrations SET app = REPLACE(app , "iSkyLIMS_wetlab", "wetlab") WHERE app like ("iSkyLIMS_%");' - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_migrations SET app = REPLACE(app , "iSkyLIMS_drylab", "drylab") WHERE app like ("iSkyLIMS_%");' - - echo "Renaming tables" - query_rename_table="SELECT CONCAT('RENAME TABLE ', TABLE_SCHEMA, '.', TABLE_NAME, \ - ' TO ', TABLE_SCHEMA, '.', REPLACE(TABLE_NAME, 'iSkyLIMS_', ''), ';') \ - AS query FROM information_schema.tables WHERE TABLE_SCHEMA = \"$DB_NAME\" AND TABLE_NAME LIKE 'iSkyLIMS_%';" - mysql -u $DB_USER -p$DB_PASS -h $DB_SERVER_IP -e "$query_rename_table" \ - | xargs -I % echo "mysql -u$DB_USER -p'$DB_PASS' -D $DB_NAME -h $DB_SERVER_IP -e \"% \" " | bash - - echo "Renaming index" - query_rename_unique_indexes="SELECT CONCAT('ALTER TABLE ', rcu.TABLE_SCHEMA, '.', rcu.TABLE_NAME, \ - ' RENAME INDEX ', rcu.CONSTRAINT_NAME, \ - ' TO ', REPLACE(rcu.CONSTRAINT_NAME, 'iSkyLIMS_', ''), ';') \ - AS query FROM information_schema.key_column_usage rcu \ - JOIN information_schema.table_constraints tc \ - ON tc.CONSTRAINT_NAME = rcu.CONSTRAINT_NAME WHERE rcu.TABLE_SCHEMA = \"$DB_NAME\" \ - AND rcu.CONSTRAINT_NAME LIKE 'iSkyLIMS_%' AND tc.CONSTRAINT_TYPE = 'UNIQUE' \ - GROUP BY rcu.TABLE_SCHEMA, rcu.TABLE_NAME, rcu.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE, \ - rcu.REFERENCED_TABLE_SCHEMA, rcu.REFERENCED_TABLE_NAME;" - mysql -u $DB_USER -p$DB_PASS -h $DB_SERVER_IP -e "$query_rename_unique_indexes" \ - | xargs -I % echo "mysql -u$DB_USER -p'$DB_PASS' -D $DB_NAME -h $DB_SERVER_IP -e \"% \" " | bash - - echo "Renaming constraints" - query_rename_constraints="SELECT CONCAT('ALTER TABLE ', rcu.TABLE_SCHEMA, '.', rcu.TABLE_NAME, \ - ' DROP FOREIGN KEY ' , rcu.CONSTRAINT_NAME, ';', \ - ' ALTER TABLE ', rcu.TABLE_SCHEMA, '.', rcu.TABLE_NAME, \ - ' ADD CONSTRAINT ', REPLACE(rcu.CONSTRAINT_NAME, 'iSkyLIMS_', ''), ' ', \ - tc.CONSTRAINT_TYPE, ' (', GROUP_CONCAT(rcu.COLUMN_NAME ORDER BY rcu.ORDINAL_POSITION SEPARATOR ', '), ')', \ - IF(tc.CONSTRAINT_TYPE = 'FOREIGN KEY', \ - CONCAT(' REFERENCES ', rcu.REFERENCED_TABLE_SCHEMA, '.', REPLACE(rcu.REFERENCED_TABLE_NAME, 'iSkyLIMS_', ''), ' (', \ - GROUP_CONCAT(rcu.REFERENCED_COLUMN_NAME ORDER BY rcu.ORDINAL_POSITION SEPARATOR ', '), ') ON DELETE ', rc.DELETE_RULE), \ - ''), ';') AS query \ - FROM information_schema.key_column_usage rcu \ - LEFT JOIN information_schema.table_constraints tc ON rcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME \ - LEFT JOIN information_schema.referential_constraints rc ON rcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME \ - WHERE rcu.TABLE_SCHEMA = '$DB_NAME' AND rcu.CONSTRAINT_NAME LIKE 'iSkyLIMS_%' \ - GROUP BY rcu.TABLE_SCHEMA, rcu.TABLE_NAME, rcu.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE, rcu.REFERENCED_TABLE_SCHEMA, rcu.REFERENCED_TABLE_NAME, rc.DELETE_RULE;" - mysql -u $DB_USER -p$DB_PASS -h $DB_SERVER_IP -e "$query_rename_constraints" | xargs -I % echo "mysql -u$DB_USER -p'$DB_PASS' -D $DB_NAME -h $DB_SERVER_IP -e \"% \" " | bash - - echo "Done modifying database names and constraints..." - - echo "Modifying names in migration files..." - sed -i 's/iSkyLIMS_core/core/g' */migrations/*.py - sed -i 's/iSkyLIMS_drylab/drylab/g' */migrations/*.py - sed -i 's/iSkyLIMS_wetlab/wetlab/g' */migrations/*.py - echo "Done modifying names in migration files..." - - echo "Copying custom migration files from conf." - cp $INSTALL_PATH/conf/0002_core_migration_v3.0.0.py $INSTALL_PATH/core/migrations/0002_migration_v3_0_0.py - cp $INSTALL_PATH/conf/0002_drylab_migration_v3.0.0.py $INSTALL_PATH/drylab/migrations/0002_migration_v3_0_0.py - cp $INSTALL_PATH/conf/0002_wetlab_migration_v3.0.0.py $INSTALL_PATH/wetlab/migrations/0002_migration_v3_0_0.py - cp $INSTALL_PATH/conf/0002_django_utils_migration_v3.0.0.py $INSTALL_PATH/django_utils/migrations/0002_migration_v3_0_0.py - - read -p "Do you want to proceed with the migrate command? (Y/N) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]] ; then - log "WARN" "Exiting without running migrate command." - exit 1 - fi - - echo "activate the virtualenv" - source virtualenv/bin/activate - echo "Running migrate..." - python manage.py migrate - echo "Done migrate command." - - cd - -} - -# install_system_packages: install InterOp and distro-specific OS packages required by iSkyLIMS. -install_system_packages() { - if [ "${SKIP_SYSTEM_PACKAGES:-}" = "1" ]; then - echo "Skipping system package installation (SKIP_SYSTEM_PACKAGES=1)" + [[ "$GIT_REVISION" != "current" ]] || return 0 + [[ -n "$INITIAL_GIT_REF" ]] || die "Cannot select $GIT_REVISION: source has no Git metadata" + git -C "$install_script_dir" rev-parse --verify "${GIT_REVISION}^{commit}" >/dev/null 2>&1 \ + || die "Git revision is not available locally: $GIT_REVISION" + [[ -z "$(git -C "$install_script_dir" status --porcelain)" ]] \ + || die "Commit or stash local changes before selecting $GIT_REVISION" + git -C "$install_script_dir" checkout --quiet "$GIT_REVISION" +} + +check_python() { + command_required "$PYTHON_BIN_PATH" + "$PYTHON_BIN_PATH" -c 'import sys; raise SystemExit(sys.version_info < (3, 10))' \ + || die "Python 3.10 or newer is required" +} + +check_required_modules() { + local module + for module in $REQUIRED_MODULES; do + [[ -e "$install_script_dir/$module" ]] || die "Required application module is missing: $module" + done + [[ -f "$install_script_dir/conf/urls.py" ]] || die "iSkyLIMS URL configuration is missing" +} + +check_database() { + # The MySQL client is used by RELECOV/iSkyLIMS; PyMySQL fallback supports + # minimal Patho Core-style containers without the client package. + if command -v mysql >/dev/null 2>&1; then + MYSQL_PWD="$DB_PASSWORD" mysql --host="$DB_HOST" --port="$DB_PORT" \ + --user="$DB_USER" --database="$DB_NAME" --execute='SELECT 1' >/dev/null \ + || die "Unable to connect to database $DB_NAME at $DB_HOST:$DB_PORT" return fi - - echo "Installing Interop" - if [ -d /opt/interop ]; then - echo "There is already an interop installation" - echo "Skipping Interop installation" - else - cd /opt - echo "Downloading interop software" - wget https://github.com/Illumina/interop/releases/download/v1.1.15/InterOp-1.1.15-Linux-GNU.tar.gz - tar -xf InterOp-1.1.15-Linux-GNU.tar.gz - ln -s InterOp-1.1.15-Linux-GNU interop - rm InterOp-1.1.15-Linux-GNU.tar.gz - echo "Interop is now installed" - cd - - fi - - if command -v lsb_release >/dev/null 2>&1; then - linux_distribution=$(lsb_release -i | cut -f 2-) - else - linux_distribution=$(awk -F= '/^ID=/{gsub(/"/,""); print $2}' /etc/os-release) - fi - - if [[ $linux_distribution == "Ubuntu" || $linux_distribution == "ubuntu" ]]; then - echo "Software installation for Ubuntu" - apt-get update && apt-get upgrade -y - apt-get install -y \ - apt-utils wget \ - libmysqlclient-dev \ - default-mysql-client \ - python3-venv \ - libpq-dev \ - python3-dev python3-pip python3-wheel \ - apache2-dev cifs-utils \ - gnuplot - - elif [[ $linux_distribution == "CentOS" || $linux_distribution == "RedHatEnterprise" || $linux_distribution == "centos" || $linux_distribution == "rhel" || $linux_distribution == "fedora" ]]; then - set -euo pipefail - - echo "Installing OS dependencies" - # Enable EPEL 9 for packages not provided by UBI repositories - echo "Installing EPEL repository" - - wget -O /tmp/epel-release.rpm \ - https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm - - rpm -Uvh /tmp/epel-release.rpm - rm -f /tmp/epel-release.rpm - - microdnf repolist - - cat >/etc/yum.repos.d/centos-stream-baseos.repo <<'EOF' -[centos-stream-baseos] -name=CentOS Stream 9 - BaseOS -baseurl=https://mirror.stream.centos.org/9-stream/BaseOS/$basearch/os/ -enabled=1 -gpgcheck=0 - -[centos-stream-appstream] -name=CentOS Stream 9 - AppStream -baseurl=https://mirror.stream.centos.org/9-stream/AppStream/$basearch/os/ -enabled=1 -gpgcheck=0 - -[centos-stream-crb] -name=CentOS Stream 9 - CRB -baseurl=https://mirror.stream.centos.org/9-stream/CRB/$basearch/os/ -enabled=1 -gpgcheck=0 -EOF - - microdnf install -y \ - gcc \ - gcc-c++ \ - make \ - tar \ - zlib-devel \ - bzip2-devel \ - openssl-devel \ - wget \ - httpd-devel \ - sqlite \ - sqlite-devel \ - mariadb \ - mariadb-connector-c-devel \ - libffi-devel \ - gnuplot \ - cifs-utils - - microdnf clean all - fi -} - -# run_django_deploy: execute makemigrations/migrate and optional fixture/superuser steps. -run_django_deploy() { - local mode="${1:-install}" - if [ "$run_script_before" = true ]; then - for val in "${migration_script_before[@]}"; do - if [[ $val = *","* ]]; then - parameters=(${val//,/ }) - echo "Running pre-migration script: ${parameters[0]}" - ./manage.py runscript ${parameters[0]} --script-args ${parameters[1]} - echo "Done pre-migration script: ${parameters[0]}" - else - echo "Running pre-migration script: $val" - ./manage.py runscript $val - echo "Done pre-migration script: $val" - fi - done - fi - - if [ "$mode" = "upgrade" ]; then - echo "Applying migrations in fake-initial mode" - python manage.py migrate --noinput --fake-initial - # Second pass ensures non-initial migrations are applied after fake-initial. - echo "Applying migrations" - python manage.py migrate --noinput - else - echo "Applying migrations" - python manage.py migrate --noinput - fi - - if [ "$tables" = true ]; then - echo "Loading pre-filled tables..." - load_tables "$prefilled_tables" true - echo "Done loading pre-filled tables..." - fi - - if [ "$run_script" = true ]; then - for val in "${migration_script[@]}"; do - if [[ $val = *","* ]]; then - parameters=(${val//,/ }) - echo "Running post-migration script: ${parameters[0]}" - ./manage.py runscript ${parameters[0]} --script-args ${parameters[1]} - echo "Done post-migration script: ${parameters[0]}" - else - echo "Running post-migration script: $val" - ./manage.py runscript $val - echo "Done post-migration script: $val" + "$INSTALL_PATH/virtualenv/bin/python" - "$DB_HOST" "$DB_PORT" "$DB_USER" "$DB_PASSWORD" "$DB_NAME" <<'PY' +import sys +import pymysql +connection = pymysql.connect(host=sys.argv[1], port=int(sys.argv[2]), + user=sys.argv[3], password=sys.argv[4], database=sys.argv[5]) +connection.close() +PY +} + +# ============================================================================ +# APPLICATION CUSTOMIZATION POINTS +# +# Keep generic lifecycle code outside this section. Each hook has a safe no-op +# default. Add project behavior here, document why it is required, and make it +# idempotent so a failed deployment can be retried safely. +# ============================================================================ + +install_application_system_packages() { + # iSkyLIMS owns these dependencies rather than its Dockerfile so the same + # lifecycle works in UBI containers and supported bare-metal deployments. + [[ "${SKIP_SYSTEM_PACKAGES:-0}" != "1" ]] || { + info "Skipping system package installation (SKIP_SYSTEM_PACKAGES=1)" + return 0 + } + [[ $(id -u) -eq 0 ]] || die "System dependency installation must run as root" + + local distribution="" + if [[ -r /etc/os-release ]]; then + # shellcheck disable=SC1091 + source /etc/os-release + distribution="${ID,,}" + fi + + case "$distribution" in + ubuntu|debian) + command_required apt-get + apt-get update + apt-get install -y --no-install-recommends \ + apt-utils wget tar gcc g++ make \ + libmysqlclient-dev default-mysql-client \ + python3-venv libpq-dev python3-dev python3-pip python3-wheel \ + apache2-dev cifs-utils gnuplot + ;; + rhel|centos|fedora|ubi) + command_required microdnf + command_required rpm + microdnf install -y wget tar + if ! rpm -q epel-release >/dev/null 2>&1; then + wget -O /tmp/epel-release.rpm \ + https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm + rpm -Uvh /tmp/epel-release.rpm + rm -f /tmp/epel-release.rpm fi - done - fi - - if [ "$mode" = "install" ]; then - echo "Creating super user " - python manage.py createsuperuser --username admin - fi -} - -refresh_static_files() { - echo "Deleting static files..." - if [ -d "$INSTALL_PATH/static" ]; then - if command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$INSTALL_PATH/static"; then - echo "Static directory is a mount point. Skipping delete." - else - rm -rf "$INSTALL_PATH/static" || echo "Skipping static removal (busy)." - fi - fi - echo "Running collect statics..." - python manage.py collectstatic --noinput - echo "Done collect statics" -} - -# sync_requirements_file: copy repository requirements into the target installation path. -sync_requirements_file() { - mkdir -p $INSTALL_PATH/conf - rsync -rlv conf/requirements.txt $INSTALL_PATH/conf/requirements.txt -} + printf '%s\n' \ + '[centos-stream-baseos]' \ + 'name=CentOS Stream 9 - BaseOS' \ + 'baseurl=https://mirror.stream.centos.org/9-stream/BaseOS/$basearch/os/' \ + 'enabled=1' 'gpgcheck=0' '' \ + '[centos-stream-appstream]' \ + 'name=CentOS Stream 9 - AppStream' \ + 'baseurl=https://mirror.stream.centos.org/9-stream/AppStream/$basearch/os/' \ + 'enabled=1' 'gpgcheck=0' '' \ + '[centos-stream-crb]' \ + 'name=CentOS Stream 9 - CRB' \ + 'baseurl=https://mirror.stream.centos.org/9-stream/CRB/$basearch/os/' \ + 'enabled=1' 'gpgcheck=0' \ + > /etc/yum.repos.d/centos-stream-baseos.repo + microdnf install -y \ + gcc gcc-c++ make tar zlib-devel bzip2-devel openssl-devel wget \ + python3.11-devel httpd-devel sqlite sqlite-devel mariadb \ + mariadb-connector-c-devel libffi-devel gnuplot cifs-utils \ + git rsync shadow-utils + microdnf clean all + ;; + *) + die "Unsupported Linux distribution for dependency installation: ${distribution:-unknown}" + ;; + esac -# setup_virtualenv: create or refresh the Python virtualenv depending on mode. -setup_virtualenv() { - local mode="$1" - cd $INSTALL_PATH - if [ "$mode" = "install" ]; then - if [ -d virtualenv ]; then - echo "There already is a virtualenv for iskylims in $INSTALL_PATH." - read -p "Do you want to remove current virtualenv and reinstall? (Y/N) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]] ; then - rm -rf $INSTALL_PATH/virtualenv - bash -c "$PYTHON_BIN_PATH -m venv virtualenv" - else - echo "virtualenv already defined. Skipping." - fi - else - bash -c "$PYTHON_BIN_PATH -m venv virtualenv" - fi - else - if [ -d virtualenv ]; then - read -p "Do you want to remove current virtualenv and reinstall? (Y/N) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]] ; then - rm -rf $INSTALL_PATH/virtualenv - bash -c "$PYTHON_BIN_PATH -m venv virtualenv" - fi + if [[ ! -e /opt/interop ]]; then + local archive="/tmp/InterOp-1.1.15-Linux-GNU.tar.gz" + wget -O "$archive" \ + https://github.com/Illumina/interop/releases/download/v1.1.15/InterOp-1.1.15-Linux-GNU.tar.gz + tar -xzf "$archive" -C /opt + ln -s /opt/InterOp-1.1.15-Linux-GNU /opt/interop + rm -f "$archive" + fi +} + +prepare_application_directories() { + # Argument: final INSTALL_PATH. Create application-specific persistent + # directories here. Generic logs/documents/static/cron/tmp already exist. + # Example: + # mkdir -p "$1/documents/genomic_files" "$1/logs/audit" + local install_path="$1" + if [[ "${LOG_TYPE:-regular_folder}" == "symbolic_link" ]]; then + [[ -n "${LOG_PATH:-}" ]] || die "LOG_PATH is required when LOG_TYPE=symbolic_link" + [[ -d "$LOG_PATH" ]] || die "Configured log directory does not exist: $LOG_PATH" + if [[ -L "$install_path/logs" ]]; then + [[ "$(readlink -f "$install_path/logs")" == "$(readlink -f "$LOG_PATH")" ]] || + die "$install_path/logs points to a different log directory" else - read -p "There is no virtualenv. Do you want to create a new one? (Y/N) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]] ; then - bash -c "$PYTHON_BIN_PATH -m venv virtualenv" - else - echo "Exiting..." - exit 0 - fi + rmdir "$install_path/logs" 2>/dev/null || + die "$install_path/logs must be empty before it can become a symbolic link" + ln -s "$LOG_PATH" "$install_path/logs" fi fi - cd - -} - -# prepare_documents_structure: ensure document directories and templates exist with correct permissions. -prepare_documents_structure() { - echo "Created documents structure" - mkdir -p $INSTALL_PATH/documents/wetlab - mkdir -p $INSTALL_PATH/documents/wetlab/tmp - mkdir -p $INSTALL_PATH/documents/wetlab/sample_sheet - mkdir -p $INSTALL_PATH/documents/wetlab/images_plot - mkdir -p $INSTALL_PATH/documents/wetlab/templates - mkdir -p $INSTALL_PATH/documents/wetlab/sample_sheets_lib_prep - mkdir -p $INSTALL_PATH/documents/drylab - mkdir -p $INSTALL_PATH/documents/drylab/service_files - - chown_if_root -R "$user:$apache_group" "$INSTALL_PATH/documents" - chmod 775 $INSTALL_PATH/documents - - cp $INSTALL_PATH/conf/*_template.csv $INSTALL_PATH/documents/wetlab/templates/ - cp $INSTALL_PATH/conf/samples_template.xlsx $INSTALL_PATH/documents/wetlab/templates/ - - mkdir -p $INSTALL_PATH/documents/wetlab/collection_index_kits/ - cp $INSTALL_PATH/conf/collection_index_kits/*.txt $INSTALL_PATH/documents/wetlab/collection_index_kits/ - - cp $INSTALL_PATH/conf/template_logging_config.ini $INSTALL_PATH/wetlab/logging_config.ini - sed -i "s|INSTALL_PATH|${INSTALL_PATH}|g" $INSTALL_PATH/wetlab/logging_config.ini -} - -# install_python_requirements: activate the venv and install required Python packages. -install_python_requirements() { - cd $INSTALL_PATH - echo "activate the virtualenv" - source virtualenv/bin/activate - echo "Installing required python packages" - python -m pip install --upgrade pip - python -m pip install wheel + mkdir -p \ + "$install_path/documents/wetlab/tmp" \ + "$install_path/documents/wetlab/sample_sheet" \ + "$install_path/documents/wetlab/images_plot" \ + "$install_path/documents/wetlab/templates" \ + "$install_path/documents/wetlab/sample_sheets_lib_prep" \ + "$install_path/documents/wetlab/collection_index_kits" \ + "$install_path/documents/drylab/service_files" +} + +stage_application_custom_files() { + # Arguments: source directory, final INSTALL_PATH, action (install|upgrade). + # Copy files that intentionally live outside the normal rsync set. + # Example: [[ -f "$1/conf/routing.py" ]] && + # install -m 0644 "$1/conf/routing.py" "$2/$PROJECT_MODULE/routing.py" + local source_dir="$1" install_path="$2" + local template + + [[ -f "$install_path/manage.py" ]] || + "$install_path/virtualenv/bin/python" -m django startproject "$PROJECT_MODULE" "$install_path" + install -m 0644 "$source_dir/conf/urls.py" "$install_path/$PROJECT_MODULE/urls.py" + + for template in "$source_dir"/conf/*_template.csv "$source_dir"/conf/samples_template.xlsx; do + [[ -e "$template" ]] || continue + install -m 0644 "$template" "$install_path/documents/wetlab/templates/" + done + for template in "$source_dir"/conf/collection_index_kits/*.txt; do + [[ -e "$template" ]] || continue + install -m 0644 "$template" "$install_path/documents/wetlab/collection_index_kits/" + done + if [[ -f "$source_dir/conf/template_logging_config.ini" ]]; then + sed "s|INSTALL_PATH|$install_path|g" "$source_dir/conf/template_logging_config.ini" \ + > "$install_path/wetlab/logging_config.ini" + fi +} + +write_application_runtime_env() { + # Argument: final INSTALL_PATH. Use this only when the application reads a + # runtime .env in addition to Django settings. Never hard-code credentials. + # Patho Core-style example: + # umask 077 + # printf 'OIDC_ISSUER=%s\n' "${OIDC_ISSUER:?required}" > "$1/.env" + # for key in $(compgen -A variable KEYCLOAK_ | sort); do + # printf '%s=%s\n' "$key" "${!key}" >> "$1/.env" + # done + : +} + +validate_application_runtime() { + # Called after DB connectivity and before migrations. Validate optional + # identity-provider or feature configuration here. + # Example: require KEYCLOAK_ISSUER only when legacy auth is disabled: + # [[ "${ENABLE_LEGACY_AUTH:-true}" == true || -n "${KEYCLOAK_ISSUER:-}" ]] || + # die "KEYCLOAK_ISSUER is required when legacy auth is disabled" + : +} + +before_django_migrate() { + # Arguments: action and space-separated MIGRATION_MODULES. This is for + # application migration preparation, not the user-selected runscript hooks. + # Example: [[ "$1" == install && -n "$2" ]] && python manage.py makemigrations $2 + # iSkyLIMS commits its migrations. Generating migrations during deployment + # would make container images and upgrades non-reproducible. + : +} + +after_django_migrate() { + # The initial administrator belongs only to a fresh runtime bootstrap. A + # repeated bootstrap leaves an existing account and password unchanged. + [[ "$WORKFLOW" == "bootstrap" && "$ACTION" == "install" ]] || return 0 + [[ "${CREATE_INITIAL_SUPERUSER:-false}" == "true" ]] || return 0 + : "${DJANGO_SUPERUSER_USERNAME:?DJANGO_SUPERUSER_USERNAME is required}" + : "${DJANGO_SUPERUSER_PASSWORD:?DJANGO_SUPERUSER_PASSWORD is required}" + + DJANGO_SUPERUSER_USERNAME="$DJANGO_SUPERUSER_USERNAME" \ + DJANGO_SUPERUSER_EMAIL="${DJANGO_SUPERUSER_EMAIL:-}" \ + DJANGO_SUPERUSER_PASSWORD="$DJANGO_SUPERUSER_PASSWORD" \ + python manage.py shell <<'PY' +import os + +from django.contrib.auth import get_user_model + +user_model = get_user_model() +username = os.environ["DJANGO_SUPERUSER_USERNAME"] +email = os.environ.get("DJANGO_SUPERUSER_EMAIL", "") +password = os.environ["DJANGO_SUPERUSER_PASSWORD"] +lookup = {user_model.USERNAME_FIELD: username} +user, created = user_model._default_manager.get_or_create(**lookup) +if created: + if hasattr(user, "email"): + user.email = email + user.is_staff = True + user.is_superuser = True + user.set_password(password) + user.save() + print(f"Created initial superuser: {username}") +else: + print(f"Initial superuser already exists: {username}") +PY +} + +set_application_permissions() { + # Argument: final INSTALL_PATH. Direct/bare-metal installs can customize + # owner/group here; container orchestration owns container mount permissions. + # Example: chown -R "${APP_UID}:${APP_GID}" "$1/logs" "$1/documents" + local install_path="$1" + if [[ "$WORKFLOW" == "standard" && $(id -u) -eq 0 ]]; then + local owner="${SUDO_USER:-root}" apache_group="apache" + [[ -f /etc/debian_version ]] && apache_group="www-data" + getent group "$apache_group" >/dev/null 2>&1 || + die "Apache runtime group does not exist: $apache_group" + chown -R "$owner:$apache_group" \ + "$install_path/logs" "$install_path/documents" "$install_path/static" + fi + chmod -R u+rwX,g+rwX "$install_path/logs" "$install_path/documents" "$install_path/static" +} + +restart_application_server() { + # Called only for a direct standard workflow unless restart was skipped. + # Example: systemctl reload apache2 (or httpd on RHEL-family systems). + local service="httpd" + [[ -f /etc/debian_version ]] && service="apache2" + command -v systemctl >/dev/null 2>&1 || return 0 + systemctl restart "$service" +} + +# ========================= END APPLICATION CUSTOMIZATION ===================== + +stage_dependencies() { + checkout_git_revision + check_python + check_required_modules + install_application_system_packages + mkdir -p "$INSTALL_PATH" + [[ -d "$INSTALL_PATH/virtualenv" ]] \ + || "$PYTHON_BIN_PATH" -m venv "$INSTALL_PATH/virtualenv" + # shellcheck disable=SC1091 + source "$INSTALL_PATH/virtualenv/bin/activate" + python -m pip install --upgrade pip wheel + [[ -f conf/requirements.txt ]] || die "Missing conf/requirements.txt" python -m pip install -r conf/requirements.txt - cd - -} - -ensure_virtualenv_ready() { - if [ ! -d "$INSTALL_PATH/virtualenv" ]; then - log_warn "Virtualenv missing. INSTALL_PATH=$INSTALL_PATH" - ls -la "$INSTALL_PATH" || true - abort_install "Virtualenv not found at $INSTALL_PATH/virtualenv. Run --install dep first." - fi -} - -# restart_apache_service: restart Apache/HTTPD unless running inside Docker or explicitly skipped. -restart_apache_service() { - if command -v lsb_release >/dev/null 2>&1; then - linux_distribution=$(lsb_release -i | cut -f 2-) - else - linux_distribution=$(awk -F= '/^ID=/{gsub(/"/,""); print $2}' /etc/os-release) - fi - if [[ $linux_distribution == "Ubuntu" ]]; then - apache_daemon="apache2" - else - apache_daemon="httpd" - fi - if ! systemctl restart $apache_daemon; then - echo -e "${ORANGE}Apache server restart failed. trying with sudo${NC}" - sudo systemctl restart $apache_daemon - fi } -# run_dependency_stage: execute the dependency portion (system packages + venv + pip) for install or upgrade. -run_dependency_stage() { - local mode="$1" - - if [ "$mode" = "install" ]; then - log_section "Preparing dependency environment for installation" - if [ -d $INSTALL_PATH ]; then - echo "There already is an installation of iskylims in $INSTALL_PATH." - read -p "Do you want to remove current installation and reinstall? (Y/N) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]] ; then - echo "Exiting without running iSkyLIMS installation" - exit 1 - else - rm -rf $INSTALL_PATH - fi - fi - install_system_packages - mkdir -p $INSTALL_PATH - if command -v lsb_release >/dev/null 2>&1; then - linux_distribution=$(lsb_release -i | cut -f 2-) - else - linux_distribution=$(awk -F= '/^ID=/{gsub(/"/,""); print $2}' /etc/os-release) - fi - if [[ $linux_distribution == "Ubuntu" ]]; then - apache_group="www-data" - else - apache_group="apache" - fi - chown_if_root -R "$user:$apache_group" "$INSTALL_PATH" - chmod 775 $INSTALL_PATH - else - log_section "Preparing dependency environment for upgrade" - if [ ! -d $INSTALL_PATH ]; then - abort_install "Unable to start the upgrade. Folder $INSTALL_PATH does not exist." - fi - install_system_packages - fi - - sync_requirements_file - setup_virtualenv "$mode" - install_python_requirements -} - -# upgrade_application_files: sync code/config and run upgrade-specific tasks (renames, migrations). -upgrade_application_files() { - if [ ! -d $INSTALL_PATH ]; then - abort_install "Unable to start the upgrade. Folder $INSTALL_PATH does not exist." - fi - - log_section "Starting iSkyLIMS Upgrade version: ${APP_VERSION}" - - rename_apps_if_needed - - stage_upgrade_application_files - bootstrap_application_runtime "upgrade" - - log_section "Successfuly upgrade of iSKyLIMS version: ${APP_VERSION}" -} - -# stage_upgrade_application_files: sync code/config into INSTALL_PATH without DB work. -stage_upgrade_application_files() { - if [ ! -d $INSTALL_PATH ]; then - abort_install "Unable to start the upgrade. Folder $INSTALL_PATH does not exist." - fi - - echo "Copying files to installation folder" - rsync -rlv conf/ $INSTALL_PATH/conf/ - rsync -rlv --fuzzy --delay-updates --delete-delay \ - --exclude "logs" --exclude "documents" --exclude "__pycache__" \ - README.md LICENSE test conf deployment_health $REQUIRED_MODULES $INSTALL_PATH - - cd $INSTALL_PATH - ensure_virtualenv_ready - echo "activate the virtualenv" +stage_application_files() { + checkout_git_revision + [[ -d "$INSTALL_PATH/virtualenv" ]] \ + || die "virtualenv not found at $INSTALL_PATH; install dependencies first" + rsync -rl --delete \ + --exclude .git --exclude .env --exclude logs --exclude documents \ + --exclude static --exclude tmp --exclude virtualenv \ + ./ "$INSTALL_PATH/" + mkdir -p "$INSTALL_PATH/logs" "$INSTALL_PATH/documents" \ + "$INSTALL_PATH/static" "$INSTALL_PATH/cron" "$INSTALL_PATH/tmp" + prepare_application_directories "$INSTALL_PATH" + stage_application_custom_files "$install_script_dir" "$INSTALL_PATH" "$ACTION" + printf '%s\n' "$GIT_REVISION" > "$INSTALL_PATH/.deployed_revision" + if [[ "$RENDER_SETTINGS" == "true" ]]; then + local template="$install_script_dir/conf/template_settings.py" + local output="${SETTINGS_OUTPUT:-$INSTALL_PATH/$PROJECT_MODULE/settings.py}" + [[ -f "$template" ]] || die "Django settings template not found: $template" + render_django_settings_file "$template" "$output" "$INSTALL_CONF" + fi + write_application_runtime_env "$INSTALL_PATH" + set_application_permissions "$INSTALL_PATH" +} + +run_hook() { + local specification="$1" script_name="${1%%,*}" + local -a args=(manage.py runscript "$script_name") + [[ -n "$script_name" ]] || die "Empty migration script name" + [[ "$specification" != *,* ]] || args+=(--script-args "${specification#*,}") + python "${args[@]}" +} + +bootstrap_application() { + [[ -f "$INSTALL_PATH/manage.py" ]] || die "manage.py not found; run --stage first" + [[ -x "$INSTALL_PATH/virtualenv/bin/python" ]] || die "virtualenv not found; run --stage first" + cd "$INSTALL_PATH" + # shellcheck disable=SC1091 source virtualenv/bin/activate - - if [ ! -f "$INSTALL_PATH/manage.py" ]; then - echo "manage.py not found. Creating ${PROJECT_NAME} project" - "$INSTALL_PATH/virtualenv/bin/python" -m django startproject "$PROJECT_NAME" . - fi - - echo "Update settings and url file." - update_settings_and_urls - prepare_documents_structure - - cd - -} - -# install_application_files: deploy Django project files, update settings, and run initial migrations. -install_application_files() { - stage_install_application_files - bootstrap_application_runtime "install" - log_section "Successfuly iSkyLIMS Installation version: ${APP_VERSION}" - echo "Installation completed" -} - -# stage_install_application_files: copy app files into INSTALL_PATH without DB work. -stage_install_application_files() { - log_section "Starting iSkyLIMS install version: ${APP_VERSION}" - - user="${SUDO_USER:-$(id -un)}" - group="$(id -gn)" - - if command -v lsb_release >/dev/null 2>&1; then - linux_distribution=$(lsb_release -i | cut -f 2-) - else - linux_distribution=$(awk -F= '/^ID=/{gsub(/"/,""); print $2}' /etc/os-release) + check_database + validate_application_runtime + python manage.py check --deploy + local hook + for hook in "${SCRIPT_BEFORE[@]}"; do run_hook "$hook"; done + before_django_migrate "$ACTION" "$MIGRATION_MODULES" + if [[ "$ACTION" == "upgrade" ]]; then + python manage.py migrate --noinput --fake-initial fi - - if [[ $linux_distribution == "Ubuntu" || $linux_distribution == "ubuntu" ]]; then - apache_group="www-data" - else - apache_group="apache" + python manage.py migrate --noinput + if [[ "$LOAD_TABLES" == "true" && "$SKIP_TABLES" == "false" ]]; then + [[ -f conf/first_install_tables.json ]] \ + || die "Initial table fixture not found: conf/first_install_tables.json" + python manage.py loaddata conf/first_install_tables.json fi - - if [ "$install_type" == "full" ] || [ "$install_type" == "app" ]; then - - if [ $LOG_TYPE == "symbolic_link" ]; then - if [ -d $LOG_PATH ]; then - if [ -e "$INSTALL_PATH/logs" ]; then - echo "Log target $INSTALL_PATH/logs already exists. Leaving it unchanged." - else - echo "Creating symbolic link to log folder" - ln -s "$LOG_PATH" "$INSTALL_PATH/logs" - chmod 775 "$LOG_PATH" - fi - else - echo "Log folder path: $LOG_PATH does not exist. Fix it in the install_settings.txt and run again." - exit 1 - fi - else - if [ ! -d $INSTALL_PATH/logs ]; then - mkdir -p $INSTALL_PATH/logs - chown_if_root "$user:$apache_group" "$INSTALL_PATH/logs" - chmod 775 $INSTALL_PATH/logs - else - echo "Log folder path: $INSTALL_PATH/logs already exist." - fi + for hook in "${SCRIPT_AFTER[@]}"; do run_hook "$hook"; done + after_django_migrate "$ACTION" + python manage.py collectstatic --noinput + local migration_log + migration_log="$(mktemp "${TMPDIR:-/tmp}/iskylims-migrations.XXXXXX.log")" + if ! python manage.py showmigrations --plan > "$migration_log" 2>&1 \ + || grep -Fq '[ ]' "$migration_log"; then + cat "$migration_log" >&2 + rm -f "$migration_log" + die "Django migration verification failed" + fi + rm -f "$migration_log" +} + +remember_git_ref +trap restore_git_ref EXIT + +case "$WORKFLOW" in + stage) stage_dependencies; stage_application_files ;; + bootstrap) bootstrap_application ;; + standard) + if [[ "$OPERATION_SCOPE" == "full" || "$OPERATION_SCOPE" == "dep" ]]; then + stage_dependencies fi + if [[ "$OPERATION_SCOPE" == "full" || "$OPERATION_SCOPE" == "app" ]]; then + stage_application_files + bootstrap_application + fi + if [[ "$SKIP_APACHE_RESTART" == "false" ]]; then restart_application_server; fi + ;; + *) die "Invalid workflow: $WORKFLOW" ;; +esac - rsync -rlv README.md LICENSE test conf deployment_health $REQUIRED_MODULES $INSTALL_PATH - - cd $INSTALL_PATH - - prepare_documents_structure - - ensure_virtualenv_ready - echo "activate the virtualenv" - source virtualenv/bin/activate - - echo "Creating ${PROJECT_NAME} project" - "$INSTALL_PATH/virtualenv/bin/python" -m django startproject "$PROJECT_NAME" . - - update_settings_and_urls - - cd - - fi -} - -# bootstrap_application_runtime: run DB/bootstrap tasks against an already staged INSTALL_PATH. -bootstrap_application_runtime() { - local mode="$1" - - if [ ! -d "$INSTALL_PATH" ]; then - abort_install "Unable to bootstrap application. Folder $INSTALL_PATH does not exist." - fi - - cd $INSTALL_PATH - ensure_virtualenv_ready - echo "activate the virtualenv" - source virtualenv/bin/activate - - if [ ! -f "$INSTALL_PATH/manage.py" ]; then - abort_install "manage.py not found at $INSTALL_PATH/manage.py. Stage application files first." - fi - - update_settings_and_urls - run_django_deploy "$mode" - refresh_static_files - - cd - -} - -# translate long options to short -reset=true -for arg in "$@" -do - if [ -n "$reset" ]; then - unset reset - set -- # this resets the "$@" array so we can rebuild it - fi - case "$arg" in - # OPTIONAL - --install) set -- "$@" -i ;; - --upgrade) set -- "$@" -u ;; - --stage) set -- "$@" -j ;; - --bootstrap) set -- "$@" -l ;; - --render-settings|--render_settings) set -- "$@" -q ;; - --settings-output|--settings_output) set -- "$@" -y ;; - --script) set -- "$@" -s ;; - --script_before) set -- "$@" -p ;; - --script_after) set -- "$@" -o ;; - --script_prev) set -- "$@" -p ;; - --tables) set -- "$@" -t ;; - --skip_tables) set -- "$@" -b ;; - --git_revision) set -- "$@" -g ;; - --conf) set -- "$@" -c ;; - --ren_app) set -- "$@" -r ;; - --docker) set -- "$@" -k ;; - --skip_apache_restart) set -- "$@" -a ;; - - # ADITIONAL - --help) set -- "$@" -h ;; - --version) set -- "$@" -v ;; - # PASSING VALUE IN PARAMETER - *) set -- "$@" "$arg" ;; - esac -done - -# SETTING DEFAULT VALUES -ren_app=false -tables=false -git_branch=${initial_git_ref:-current} -conf="./install_settings.txt" -install=true -install_type="full" -upgrade=false -upgrade_type="full" -workflow="standard" -workflow_mode="" -docker=false -prefilled_tables="conf/first_install_tables.json" -restart_apache=true -run_script=false -run_script_before=false -migration_script=() -migration_script_before=() -skip_tables=false -render_settings="auto" -settings_output="" - -# PARSE VARIABLE ARGUMENTS WITH getops -options=":c:s:i:u:j:l:r:g:tdbkvhaqo:p:y:" -while getopts $options opt; do - case $opt in - i ) - workflow="standard" - install=true - upgrade=false - if [[ "$OPTARG" == "full" || "$OPTARG" == "dep" || "$OPTARG" == "app" ]]; then - install_type=$OPTARG - upgrade_type=$OPTARG - else - echo "Install is not set to one valid option. Use: --install full/app/dep" - exit 1 - fi - ;; - u ) - workflow="standard" - install=false - upgrade=true - if [[ "$OPTARG" == "full" || "$OPTARG" == "dep" || "$OPTARG" == "app" ]]; then - upgrade_type=$OPTARG - install_type=$OPTARG - else - echo "Upgrade is not set to one valid option. Use: --upgrade full/app/dep" - exit 1 - fi - ;; - j ) - workflow="stage" - workflow_mode=$OPTARG - if [[ "$workflow_mode" != "install" && "$workflow_mode" != "upgrade" ]]; then - echo "Stage is not set to one valid option. Use: --stage install/upgrade" - exit 1 - fi - ;; - l ) - workflow="bootstrap" - workflow_mode=$OPTARG - if [[ "$workflow_mode" != "install" && "$workflow_mode" != "upgrade" ]]; then - echo "Bootstrap is not set to one valid option. Use: --bootstrap install/upgrade" - exit 1 - fi - ;; - s ) - run_script=true - migration_script+=("$OPTARG") - ;; - p ) - run_script_before=true - migration_script_before+=("$OPTARG") - ;; - o ) - run_script=true - migration_script+=("$OPTARG") - ;; - t ) - tables=true - ;; - b ) - tables=false - skip_tables=true - ;; - r ) - ren_app=true - ;; - g ) - git_branch=$OPTARG - ;; - c ) - conf=$OPTARG - ;; - k ) - docker=true - restart_apache=false - ;; - a ) - restart_apache=false - ;; - q ) - render_settings=true - ;; - y ) - settings_output=$OPTARG - ;; - h ) - usage - exit 1 - ;; - v ) - echo $APP_VERSION - exit 1 - ;; - \?) - echo "Invalid Option: -$OPTARG" 1>&2 - usage - exit 1 - ;; - : ) - echo "Option -$OPTARG requires an argument." >&2 - exit 1 - ;; - * ) - echo "Unimplemented option: -$OPTARG" >&2; - exit 1 - ;; - esac -done -shift $((OPTIND-1)) - -operation="install" -operation_scope="$install_type" -if [ $upgrade == true ]; then - operation="upgrade" - operation_scope="$upgrade_type" -fi -if [ "$workflow" != "standard" ]; then - operation="$workflow_mode" - operation_scope="app" -fi - -# Traditional installs keep their historical rendering behavior. Container -# image staging must opt in, which only the non-secret test build does. -if [ "$render_settings" = "auto" ]; then - if [ "$workflow" = "standard" ]; then render_settings=true; else render_settings=false; fi -fi - -# Default to loading initial tables on installs unless explicitly skipped. -if [ "$operation" = "install" ] && [ "$skip_tables" = false ] && [ "$tables" = false ]; then - tables=true -fi - -# Keep the configuration addressable after staging functions change directory. -if [[ "$conf" != /* ]]; then - conf="$(cd "$(dirname "$conf")" && pwd)/$(basename "$conf")" -fi -load_install_config - -# Keep one canonical database host and password in deployment configuration. -# Legacy iSkyLIMS functions still use DB_SERVER_IP and DB_PASS internally, so -# derive those aliases here instead of asking operators to duplicate secrets. -DB_HOST="${DB_HOST:-${DB_SERVER_IP:-}}" -DB_PASSWORD="${DB_PASSWORD:-${DB_PASS:-}}" -: "${DB_HOST:?DB_HOST is required}" -: "${DB_PASSWORD:?DB_PASSWORD is required}" -DB_SERVER_IP="$DB_HOST" -DB_PASS="$DB_PASSWORD" - -PROJECT_NAME="${PROJECT_NAME:-iskylims}" -# Bootstrap consumes the application staged during image build. It must not -# attempt to inspect or change a Git checkout, which is deliberately absent -# from the production image. -if [ "$workflow" != "bootstrap" ]; then - checkout_git_revision -fi -user=${SUDO_USER:-$USER} - -if [ "$workflow" = "stage" ]; then - check_stage_requirements - run_dependency_stage "$operation" - if [ "$workflow_mode" = "install" ]; then - stage_install_application_files - else - stage_upgrade_application_files - fi - exit 0 -fi - -if [ "$workflow" = "bootstrap" ]; then - check_bootstrap_requirements - if [ "$workflow_mode" = "upgrade" ]; then - rename_apps_if_needed - fi - bootstrap_application_runtime "$workflow_mode" - exit 0 -fi - -check_requirements - -if [[ "$operation_scope" == "full" || "$operation_scope" == "dep" ]]; then - run_dependency_stage "$operation" - if [ "$operation_scope" = "dep" ]; then - log_info "Dependency stage completed." - exit 0 - fi -fi - -if [[ "$operation_scope" == "full" || "$operation_scope" == "app" ]]; then - if [ "$operation" = "install" ]; then - install_application_files - else - upgrade_application_files - fi - if [ $restart_apache == true ]; then - restart_apache_service - fi - exit 0 -fi - -printf "\n\n%s" -printf "${RED}------------------${NC}\n" -printf "%s" -printf "${RED}Invalid installation parameters${NC}\n" -printf "%s" -printf "${RED}------------------${NC}\n\n" -echo "See the usage examples" -usage -exit 1 +info "$WORKFLOW $ACTION completed for iSkyLIMS at $INSTALL_PATH" diff --git a/scripts/container_start.sh b/scripts/container_start.sh index 4ef08e82..7dea027c 100755 --- a/scripts/container_start.sh +++ b/scripts/container_start.sh @@ -2,10 +2,10 @@ set -euo pipefail # Shared RELECOV Platform/iSkyLIMS runtime pattern, parameterized per project. -APP_DIR="${APP_INSTALL_PATH:-${INSTALL_PATH:-/opt/iskylims}}" +APP_DIR="${APP_INSTALL_PATH:-${INSTALL_PATH:-/opt/relecov-iskylims}}" CRON_DIR="${APP_DIR}/cron" TMP_DIR="${APP_DIR}/tmp" -CRON_FILE="${CRON_DIR}/iskylims" +CRON_FILE="${CRON_DIR}/relecov-iskylims" CRON_LOG="${TMP_DIR}/supercronic.log" CRON_DISABLED_FILE="${CRON_DIR}/disabled" APP_MODE="${APP_MODE:-prod}" diff --git a/scripts/smoke_test.sh b/scripts/smoke_test.sh index f833b76e..0a1fb5cb 100755 --- a/scripts/smoke_test.sh +++ b/scripts/smoke_test.sh @@ -54,4 +54,4 @@ for service in "${install_services[@]}"; do [ -n "$port" ] || fail "$port_variable is required in the rendered service settings" check_url "$service" "http://127.0.0.1:${port}/health/" done -echo "Example Application deployment smoke test passed." +echo "iSkyLIMS deployment smoke test passed." From 6f6d08eb5f0e3fb53c6e4c3b870e8b2797e2163d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 10 Aug 2026 14:46:56 +0200 Subject: [PATCH 07/55] fix(test): use scoped database service hostname --- conf/docker_test_settings.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index 62cd25c5..8837e7ed 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -25,7 +25,7 @@ HOST_LOG_PATH='/tmp/relecov-iskylims/logs' DJANGO_SETTINGS_PATH='/tmp/relecov-iskylims/data/settings/iskylims/settings.py' ### Compose test database -DB_HOST='db' +DB_HOST='app_db' DB_PORT='3306' DB_NAME='iskylims' DB_USER='django' From 7ceac9b7104c2a1ee5e1668da6f96c7b8a75b6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 10 Aug 2026 14:47:00 +0200 Subject: [PATCH 08/55] fix(deployment): avoid recreating persistent support services --- .bu-isciii-deployment/state.json | 4 ++-- container_install.sh | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 7ce81a62..32004b0e 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -35,9 +35,9 @@ "conf/apache/01-reverse-proxy.conf": "7d22c5d707efe6190d799fd75d08114dd67ef9077548cd34d5df1f174bd45677", "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", "conf/docker_production_settings.txt": "7676e176102c7cec75b5a07897eed4210178965da7b3fdc0f2b33a5feac6baab", - "conf/docker_test_settings.txt": "401665b86ab23a7eb4c35d75e7187794b628aee5adcebb5f0fa64aefae5f9f08", + "conf/docker_test_settings.txt": "a94f705105e73f8b6f727faaab0fae36bada291b262762abf37f78dd8002b8c0", "conf/template_settings.py": "590369c8b3db2e18b35deb04b2a4abf05e60f65d41ba0983b3b4af8ef20e5039", - "container_install.sh": "e2c2a45d5b9ab5c5a7756620cd941fec35f5561eea8840a509f3aeb422714d8b", + "container_install.sh": "78ea21a4d95c727bc931f67f639b0c9ea6d9cbcdffed9d435e54ef9152296fc7", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", diff --git a/container_install.sh b/container_install.sh index 583919ca..7af99193 100755 --- a/container_install.sh +++ b/container_install.sh @@ -393,11 +393,13 @@ for service_name in "${install_services[@]}"; do --tag "$(service_image_name "$service_name")" "$context" fi done -# 7. Start the complete topology from one Compose file. Recreate containers so -# a freshly built image is actually deployed; podman-compose 1.0.x may otherwise -# restart an existing named container that still references the previous image. -# Named volumes and bind-mounted persistent data are preserved. -deployment_compose -f "$compose_file" up -d --force-recreate +# 7. Force-recreate only the application services whose images were rebuilt. +# Recreating the complete topology needlessly stops persistent support services +# such as databases and can wedge older rootless Podman/runc combinations. A +# second normal convergence starts or updates add-ons without forcing healthy +# support services to restart. Named volumes and bind mounts are preserved. +deployment_compose -f "$compose_file" up -d --force-recreate "${install_services[@]}" +deployment_compose -f "$compose_file" up -d # 8. Wait for every application service readiness contract. for service_name in "${install_services[@]}"; do From fe0fb322a066ca968f7cf9a7d22b9ab09c891441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 10:50:37 +0200 Subject: [PATCH 09/55] feat(test): restore iSkyLIMS Samba demo workflow --- .bu-isciii-deployment/state.json | 22 ++++--- LEAME.md | 1 + README.md | 2 + conf/INSTALL_SETTINGS.md | 6 ++ conf/docker_test_settings.txt | 14 +++-- container_install.sh | 102 ++++++++++++++++++++++++++++--- deployment/project.json | 4 ++ docker-compose.test.yml | 12 +++- scripts/container_start.sh | 11 ++-- 9 files changed, 147 insertions(+), 27 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 32004b0e..6c8ba99a 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -3,6 +3,12 @@ "ADDONS": { "apache": { "CONFIG_SERVICE": "app" + }, + "samba": { + "CONFIG_SERVICE": "app", + "MODES": [ + "test" + ] } }, "APP_NAME": "iSkyLIMS", @@ -28,24 +34,24 @@ ".dockerignore": "49a5b2223fb2a4f4201c19a833c60ba91f3e979632b6ae45fd4aca4cf22abb32", ".gitignore": "5f08b2eb5e81fa7a1f4d5bc73df8ba6741e4a3f78925890c3fd1018f2188545b", "Dockerfile": "1a3f8ccccfd84f0a59efda8446f53954fade8b200738a630420564eb8c6e7d5f", - "LEAME.md": "56279a4c2102a7ed42e34afe7c85055f6279fd9e67ee0df8a5d31c81c7ff9fb5", - "README.md": "2adeeefd761f55a2e23c6a78ee05a70ee1badb56bb4ffbd198631480dfcfb04b", - "conf/INSTALL_SETTINGS.md": "bea1f3d22896ad7d8b13c3afc829c1d90f6453771b4c5a90a44a95b0084dc4d2", + "LEAME.md": "3ad8b8437b135446306682f0ae401fd6310e82f4d6816be37b8ac6241a399fae", + "README.md": "afe6fd080f81b93afab6a35bc147e48640a1425d2063571d1bda7b34fde13ad2", + "conf/INSTALL_SETTINGS.md": "c447b2aa506081b1eb551368f29a7eeb3da527018f4270f952b58bc276c28fe4", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "7d22c5d707efe6190d799fd75d08114dd67ef9077548cd34d5df1f174bd45677", "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", - "conf/docker_production_settings.txt": "7676e176102c7cec75b5a07897eed4210178965da7b3fdc0f2b33a5feac6baab", - "conf/docker_test_settings.txt": "a94f705105e73f8b6f727faaab0fae36bada291b262762abf37f78dd8002b8c0", + "conf/docker_production_settings.txt": "b26df69209a678020b430793f97cc96660d36a781dc623d3a8f47fa4f478d1c5", + "conf/docker_test_settings.txt": "925eff6f30ced4d39d3fc83f9edd1e2a71de05ff573d9d39cf07d19850ffb24a", "conf/template_settings.py": "590369c8b3db2e18b35deb04b2a4abf05e60f65d41ba0983b3b4af8ef20e5039", - "container_install.sh": "78ea21a4d95c727bc931f67f639b0c9ea6d9cbcdffed9d435e54ef9152296fc7", + "container_install.sh": "d67c9c36db42b793b2fcfb027d9dadeea91048450edc7c176df8b628b1ed32b3", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", "docker-compose.prod.yml": "fccf8f382a57a4333a061710009021dad43ec3298c4ef487e82e3dc1f2eb6244", - "docker-compose.test.yml": "4e528d06fd480271e78990a296a341263b71b01b80601a7f74b805955e57f539", + "docker-compose.test.yml": "ad5b5f540757859812160d03b4fb05c1b2ab208cb9fa6f359a3ffb12ec6e2973", "install.sh": "6d68766e042e068e2aad8a3cf69e5bbee0542d36c587bc12bd8d64d80a64404a", - "scripts/container_start.sh": "53c2989579848ef1a821d3be84df862ac1cb7aa157ad85b641023a6ea15e06e0", + "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, "source": "BU-ISCIII deployment standards", diff --git a/LEAME.md b/LEAME.md index db289c9d..5f2f2b2c 100644 --- a/LEAME.md +++ b/LEAME.md @@ -47,6 +47,7 @@ workflow. Record `compose ps`, image IDs and deployed revisions. | `app` database | External production database | Database backup before migration | | `app` documents | `HOST_DATA_PATH/documents` | Filesystem backup | | `app` static | `app_static` named volume | Replaceable through collectstatic | +| Samba test data | `samba_test_data` named volume | Disposable test/demo files | ## 7. Backup and restore diff --git a/README.md b/README.md index 2e62f76e..4912b0ea 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Profile behavior: Selected add-ons: - Apache source configuration lives under `conf/apache/`; customize its virtual hosts and routes there. The installer renders final bind sources under `deployment/apache/`. +- The Samba add-on provides disposable NGS demo storage only in `--test` mode. All selected services and add-ons are assembled into one `docker-compose.prod.yml` and one `docker-compose.test.yml`. @@ -74,6 +75,7 @@ immutable browser bundle and do not run Django bootstrap. | `app` database | External production database | Database backup before migration | | `app` documents | `HOST_DATA_PATH/documents` | Filesystem backup | | `app` static | `app_static` named volume | Replaceable through collectstatic | +| Samba test data | `samba_test_data` named volume | Disposable test/demo files | Run permission repair without building or migrating: diff --git a/conf/INSTALL_SETTINGS.md b/conf/INSTALL_SETTINGS.md index 7e59cfb6..8af561b7 100644 --- a/conf/INSTALL_SETTINGS.md +++ b/conf/INSTALL_SETTINGS.md @@ -87,3 +87,9 @@ hosts, routes, and aliases. During installation they are rendered with the protected deployment environment into `deployment/apache/`; only those final files are bind-mounted. `APACHE_LOG_PATH` is the writable persistent host log source. + + +### Samba test data + +`SAMBA_USER` and `SAMBA_PASSWORD` configure the disposable Samba service used +only by the test Compose profile. The add-on creates no production service. diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index 8837e7ed..3f19c8cb 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -33,10 +33,10 @@ DB_PASSWORD='djangopass' DB_ROOT_PASSWORD='root' ### Django and local email defaults -DJANGO_DEBUG='false' +DJANGO_DEBUG='true' DJANGO_SECRET_KEY='test-only-change-me' -DJANGO_ALLOWED_HOSTS='localhost,127.0.0.1,0.0.0.0' -DJANGO_CSRF_TRUSTED_ORIGINS='http://localhost:8001,http://127.0.0.1:8001' +DJANGO_ALLOWED_HOSTS='*' +DJANGO_CSRF_TRUSTED_ORIGINS='http://localhost:8001,http://127.0.0.1:8001,http://0.0.0.0:8001' DB_CONN_MAX_AGE='60' ### Local email defaults @@ -72,9 +72,9 @@ LOG_PATH='' # This compatibility value is unused unless application code consumes it. APACHE_CONF_PATH='' -# Disposable test logs and a loopback-only public endpoint. +# Disposable test logs and a public test endpoint. APACHE_LOG_PATH='/tmp/relecov-iskylims/apache-logs' -APACHE_BIND_HOST='127.0.0.1' +APACHE_BIND_HOST='0.0.0.0' APACHE_PORT='8081' # Host name handled by the local test VirtualHost. @@ -106,3 +106,7 @@ SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' APACHE_FORWARDED_PROTO='http' APACHE_FORWARDED_PORT='8081' APACHE_LIMIT_REQUEST_BODY='52428800' + +# Samba test/demo data add-on +SAMBA_USER='samba_user' +SAMBA_PASSWORD='sambapasswd' diff --git a/container_install.sh b/container_install.sh index 7af99193..c382fc57 100755 --- a/container_install.sh +++ b/container_install.sh @@ -114,6 +114,8 @@ prepare_compose_environment() { "APACHE_FORWARDED_PROTO|$(config_value_or_default APACHE_FORWARDED_PROTO "${install_conf_host_by_service[app]}" '')" "APACHE_FORWARDED_PORT|$(config_value_or_default APACHE_FORWARDED_PORT "${install_conf_host_by_service[app]}" '')" "APACHE_LIMIT_REQUEST_BODY|$(config_value_or_default APACHE_LIMIT_REQUEST_BODY "${install_conf_host_by_service[app]}" '')" + "SAMBA_USER|$(config_value_or_default SAMBA_USER "${install_conf_host_by_service[app]}" '')" + "SAMBA_PASSWORD|$(config_value_or_default SAMBA_PASSWORD "${install_conf_host_by_service[app]}" '')" ) compose_env_file="$script_dir/.env.${mode}.file" write_compose_environment_file "$compose_env_file" settings_sources deployment_values @@ -260,6 +262,75 @@ bootstrap_service() { esac } +# Applications with disposable fixtures or demo files customize this callback +# in their generated wrapper and set application_supports_test_data=true. Keep +# application-specific fixture names, users/groups, downloads, and data-service +# layout here so the complete test installation remains readable in one file. +application_supports_test_data=true +load_test_deployment_data() { + local app_container app_repo_path samba_container archive downloaded_archive + local admin_groups_code + + app_container="$(current_service_container app)" \ + || die "Unable to resolve the app container for test-data loading" + app_repo_path="$(service_repo_path app)" + + if [ "$skip_test_data" = false ]; then + echo "Loading iSkyLIMS test fixtures" + engine_exec exec -w "$app_repo_path" "$app_container" \ + python manage.py loaddata test/test_data.json + admin_groups_code=$(cat <<'PY' +from django.contrib.auth.models import Group, User + +admin = User.objects.get(username="admin") +admin.groups.add( + Group.objects.get(name="WetlabManager"), + Group.objects.get(name="ServiceManager"), +) +print("admin groups:", list(admin.groups.values_list("name", flat=True))) +PY +) + engine_exec exec -w "$app_repo_path" "$app_container" \ + python manage.py shell -c "$admin_groups_code" + else + echo "Skipping iSkyLIMS test fixtures as requested" + fi + + if [ "$skip_demo_data" = true ]; then + echo "Skipping Samba demo-data load as requested" + return 0 + fi + + samba_container="$(current_service_container samba)" \ + || die "The iSkyLIMS test-data workflow requires the Samba service" + archive="$demo_data" + downloaded_archive="" + if [ -z "$archive" ]; then + command -v wget >/dev/null 2>&1 \ + || die "wget is required to download iSkyLIMS demo data" + downloaded_archive="$(mktemp /tmp/iskylims_demo_data.XXXXXX.tar.gz)" + archive="$downloaded_archive" + wget -O "$archive" \ + https://zenodo.org/record/8091169/files/iskylims_demo_data.tar.gz + fi + [ -f "$archive" ] || die "Demo-data archive not found: $archive" + + echo "Copying and extracting iSkyLIMS demo data in Samba" + engine_exec cp "$archive" "$samba_container:/mnt/iskylims_demo_data.tar.gz" + engine_exec exec "$samba_container" \ + tar -xf /mnt/iskylims_demo_data.tar.gz -C /mnt + engine_exec exec "$samba_container" sh -lc ' + for root in /mnt/test_ngs_data /mnt/Runs; do + if [ -d "$root" ]; then + find "$root" -type d -exec chmod o+rx {} + + find "$root" -type f -exec chmod o+r {} + + fi + done + ' + engine_exec exec "$samba_container" rm /mnt/iskylims_demo_data.tar.gz + [ -z "$downloaded_archive" ] || rm -f "$downloaded_archive" +} + action="install"; mode="production"; engine="docker"; git_revision="current" install_conf=""; compose_file=""; compose_env_file="" install_conf_map_entries=(); migration_script_before=(); migration_script_after=() @@ -313,7 +384,17 @@ done # 2. Validate arguments before modifying deployment state. [[ "$action" =~ ^(install|upgrade|fix-permissions)$ ]] || die "Invalid action: $action" [[ "$engine" =~ ^(docker|podman)$ ]] || die "Invalid engine: $engine" -[ -z "$demo_data" ] || die "--demo_data requires an application-owned deployment hook" +if [ -n "$demo_data" ] && [ "$application_supports_test_data" != true ]; then + die "--demo_data is not implemented for $APPLICATION_NAME" +fi +if [ "$mode" = test ] && [ "$action" = install ] \ + && [ "$application_supports_test_data" = true ]; then + skip_demo_data="${skip_demo_data:-false}" + skip_test_data="${skip_test_data:-false}" +else + skip_demo_data=true + skip_test_data=true +fi # 3. Resolve one protected configuration source per configured component. cd "$script_dir" @@ -393,13 +474,10 @@ for service_name in "${install_services[@]}"; do --tag "$(service_image_name "$service_name")" "$context" fi done -# 7. Force-recreate only the application services whose images were rebuilt. -# Recreating the complete topology needlessly stops persistent support services -# such as databases and can wedge older rootless Podman/runc combinations. A -# second normal convergence starts or updates add-ons without forcing healthy -# support services to restart. Named volumes and bind mounts are preserved. -deployment_compose -f "$compose_file" up -d --force-recreate "${install_services[@]}" -deployment_compose -f "$compose_file" up -d +# 7. Recreate and start the complete topology from one Compose invocation so +# freshly built images and the current configuration are deployed consistently. +# Named volumes and bind-mounted persistent data are preserved. +deployment_compose -f "$compose_file" up -d --force-recreate # 8. Wait for every application service readiness contract. for service_name in "${install_services[@]}"; do @@ -426,7 +504,13 @@ for service_name in "${install_services[@]}"; do bootstrap_service "$service_name" "$container_id" "$action" || die "$service_name bootstrap failed" done -# 11. Execute the common smoke dispatcher with generated profile checks. +# 11. Load application-owned fixtures/demo files only for a fresh test install. +if [ "$mode" = test ] && [ "$action" = install ] \ + && [ "$application_supports_test_data" = true ]; then + load_test_deployment_data +fi + +# 12. Execute the common smoke dispatcher with generated profile checks. smoke_args=(--engine "$engine" --compose_file "$compose_file" --env_file "$compose_env_file") [ "$mode" = test ] && smoke_args+=(--test) bash "$script_dir/scripts/smoke_test.sh" "${smoke_args[@]}" diff --git a/deployment/project.json b/deployment/project.json index a0023c48..6ce39fc7 100644 --- a/deployment/project.json +++ b/deployment/project.json @@ -20,6 +20,10 @@ "ADDONS": { "apache": { "CONFIG_SERVICE": "app" + }, + "samba": { + "CONFIG_SERVICE": "app", + "MODES": ["test"] } } } diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 8901e27b..fce8e72d 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -43,7 +43,7 @@ services: timeout: 5s retries: 12 ports: - - "127.0.0.1:${APP_APP_PORT:?APP_APP_PORT is required}:${APP_APP_PORT:?APP_APP_PORT is required}" + - "0.0.0.0:${APP_APP_PORT:?APP_APP_PORT is required}:${APP_APP_PORT:?APP_APP_PORT is required}" networks: [deployment_net] # END BU-ISCIII SERVICE: app # BEGIN BU-ISCIII ADDON: apache @@ -74,6 +74,15 @@ services: # OPTIONAL APP MOUNTS belong in ADDONS.apache.MOUNTS. networks: [deployment_net] # END BU-ISCIII ADDON: apache + # BEGIN BU-ISCIII ADDON: samba (test only) + samba: + image: docker.io/dperson/samba:latest + restart: unless-stopped + command: '-S -s "ngs_data;/mnt/test_ngs_data;yes;yes;no;${SAMBA_USER:?SAMBA_USER is required};none;none;ngs data samba share" -u "${SAMBA_USER:?SAMBA_USER is required};${SAMBA_PASSWORD:?SAMBA_PASSWORD is required}" -p' + volumes: + - samba_test_data:/mnt:z + networks: [deployment_net] + # END BU-ISCIII ADDON: samba app_db: image: docker.io/library/mysql:8.0 environment: @@ -97,3 +106,4 @@ volumes: app_test_documents: app_test_static: apache_test_logs: + samba_test_data: diff --git a/scripts/container_start.sh b/scripts/container_start.sh index 7dea027c..b2b66df6 100755 --- a/scripts/container_start.sh +++ b/scripts/container_start.sh @@ -58,10 +58,6 @@ safe_chmod() { safe_chmod 700 "${CRON_DIR}" "${TMP_DIR}" -if [[ "$APP_MODE" == "dev" ]]; then - exec python "${APP_DIR}/manage.py" runserver "0.0.0.0:${APP_PORT}" -fi - if [[ -f "$CRON_DISABLED_FILE" ]]; then echo "Cron is disabled by ${CRON_DISABLED_FILE}. Skipping supercronic start." elif command -v supercronic >/dev/null 2>&1; then @@ -122,6 +118,13 @@ else echo "supercronic not found. Scheduled jobs are disabled." fi +if [[ "$APP_MODE" == "dev" ]]; then + # Disable Django's reloader so it cannot duplicate the cron worker that + # was started above. Test settings enable Django's static/media serving. + exec python "${APP_DIR}/manage.py" runserver \ + --noreload "0.0.0.0:${APP_PORT}" +fi + if [[ -n "${WEB_CONCURRENCY:-}" ]]; then GUNICORN_WORKERS="$WEB_CONCURRENCY" else From 14fe9ba940269c3d1630119fb80bc057b9021679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 10:52:03 +0200 Subject: [PATCH 10/55] fix(test): load fixtures from installed application --- container_install.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/container_install.sh b/container_install.sh index c382fc57..279de4bb 100755 --- a/container_install.sh +++ b/container_install.sh @@ -268,16 +268,16 @@ bootstrap_service() { # layout here so the complete test installation remains readable in one file. application_supports_test_data=true load_test_deployment_data() { - local app_container app_repo_path samba_container archive downloaded_archive + local app_container app_install_path samba_container archive downloaded_archive local admin_groups_code app_container="$(current_service_container app)" \ || die "Unable to resolve the app container for test-data loading" - app_repo_path="$(service_repo_path app)" + app_install_path="$(service_install_path app)" if [ "$skip_test_data" = false ]; then echo "Loading iSkyLIMS test fixtures" - engine_exec exec -w "$app_repo_path" "$app_container" \ + engine_exec exec -w "$app_install_path" "$app_container" \ python manage.py loaddata test/test_data.json admin_groups_code=$(cat <<'PY' from django.contrib.auth.models import Group, User @@ -290,7 +290,7 @@ admin.groups.add( print("admin groups:", list(admin.groups.values_list("name", flat=True))) PY ) - engine_exec exec -w "$app_repo_path" "$app_container" \ + engine_exec exec -w "$app_install_path" "$app_container" \ python manage.py shell -c "$admin_groups_code" else echo "Skipping iSkyLIMS test fixtures as requested" From 2b52cd5bc4db1300d8fb55fa90f687cb6e20a793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 10:57:14 +0200 Subject: [PATCH 11/55] fix(test): use application virtualenv for fixtures --- container_install.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/container_install.sh b/container_install.sh index 279de4bb..cfe8a296 100755 --- a/container_install.sh +++ b/container_install.sh @@ -278,7 +278,8 @@ load_test_deployment_data() { if [ "$skip_test_data" = false ]; then echo "Loading iSkyLIMS test fixtures" engine_exec exec -w "$app_install_path" "$app_container" \ - python manage.py loaddata test/test_data.json + "$app_install_path/virtualenv/bin/python" manage.py \ + loaddata test/test_data.json admin_groups_code=$(cat <<'PY' from django.contrib.auth.models import Group, User @@ -291,7 +292,8 @@ print("admin groups:", list(admin.groups.values_list("name", flat=True))) PY ) engine_exec exec -w "$app_install_path" "$app_container" \ - python manage.py shell -c "$admin_groups_code" + "$app_install_path/virtualenv/bin/python" manage.py \ + shell -c "$admin_groups_code" else echo "Skipping iSkyLIMS test fixtures as requested" fi From 8064dd91a6c9a03ad2aba9e10056cdca57c6a49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 11:06:14 +0200 Subject: [PATCH 12/55] fix(deployment): stage application static sources --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index ffe7d351..72d8d21d 100755 --- a/install.sh +++ b/install.sh @@ -418,8 +418,8 @@ stage_application_files() { [[ -d "$INSTALL_PATH/virtualenv" ]] \ || die "virtualenv not found at $INSTALL_PATH; install dependencies first" rsync -rl --delete \ - --exclude .git --exclude .env --exclude logs --exclude documents \ - --exclude static --exclude tmp --exclude virtualenv \ + --exclude .git --exclude .env --exclude /logs --exclude /documents \ + --exclude /static --exclude /tmp --exclude /virtualenv \ ./ "$INSTALL_PATH/" mkdir -p "$INSTALL_PATH/logs" "$INSTALL_PATH/documents" \ "$INSTALL_PATH/static" "$INSTALL_PATH/cron" "$INSTALL_PATH/tmp" From d0991aa8ec64c19feac8a15b568389a18254498d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 12:36:49 +0200 Subject: [PATCH 13/55] fix: replace protected bind files atomically --- deployment/lib/container/common.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/deployment/lib/container/common.sh b/deployment/lib/container/common.sh index c4de4b48..dae70b9d 100644 --- a/deployment/lib/container/common.sh +++ b/deployment/lib/container/common.sh @@ -227,10 +227,19 @@ print_container_repository_diagnostics() { copy_with_podman_fallback() { local src="$1" local dst="$2" - - if cp "$src" "$dst" 2>/dev/null; then + local tmp_dst="" + + # Replace the destination through its parent directory instead of opening + # an existing bind-mounted file in place. Containers may have changed that + # file's ownership to an unmapped/root identity, while the operator still + # owns the parent directory and is therefore allowed to replace it. The + # same-directory rename also prevents readers from seeing a partial file. + if tmp_dst="$(mktemp "${dst}.tmp.XXXXXX" 2>/dev/null)" \ + && cp "$src" "$tmp_dst" 2>/dev/null \ + && mv -f "$tmp_dst" "$dst" 2>/dev/null; then return 0 fi + [ -z "$tmp_dst" ] || rm -f "$tmp_dst" 2>/dev/null || true if [ "$engine" = "podman" ] && podman unshare cp "$src" "$dst"; then return 0 fi From d6cb44eeda8c035ca725269617c4b5c8e6bc947e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 12:37:06 +0200 Subject: [PATCH 14/55] feat: store Django documents in named volumes --- LEAME.md | 2 +- README.md | 2 +- conf/INSTALL_SETTINGS.md | 1 - conf/docker_production_settings.txt | 5 ++--- conf/docker_test_settings.txt | 1 - container_install.sh | 5 +---- docker-compose.prod.yml | 5 +++-- 7 files changed, 8 insertions(+), 13 deletions(-) diff --git a/LEAME.md b/LEAME.md index 5f2f2b2c..1519c75d 100644 --- a/LEAME.md +++ b/LEAME.md @@ -45,7 +45,7 @@ workflow. Record `compose ps`, image IDs and deployed revisions. | Asset | Production location | Recovery requirement | |---|---|---| | `app` database | External production database | Database backup before migration | -| `app` documents | `HOST_DATA_PATH/documents` | Filesystem backup | +| `app` documents | `app_documents` named volume | Volume backup | | `app` static | `app_static` named volume | Replaceable through collectstatic | | Samba test data | `samba_test_data` named volume | Disposable test/demo files | diff --git a/README.md b/README.md index 4912b0ea..e9212b9d 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ immutable browser bundle and do not run Django bootstrap. | Asset | Production location | Backup/rebuild policy | |---|---|---| | `app` database | External production database | Database backup before migration | -| `app` documents | `HOST_DATA_PATH/documents` | Filesystem backup | +| `app` documents | `app_documents` named volume | Volume backup | | `app` static | `app_static` named volume | Replaceable through collectstatic | | Samba test data | `samba_test_data` named volume | Disposable test/demo files | diff --git a/conf/INSTALL_SETTINGS.md b/conf/INSTALL_SETTINGS.md index 8af561b7..6d782acf 100644 --- a/conf/INSTALL_SETTINGS.md +++ b/conf/INSTALL_SETTINGS.md @@ -14,7 +14,6 @@ production secrets. Operators copy it to an ignored, permission-restricted file. | `PYTHON_BIN_PATH` | yes | no | Python used to create the virtual environment | | `APP_UID`, `APP_GID` | yes | no | Runtime identity and rootless volume ownership | | `APP_PORT` | yes | no | Internal Gunicorn and host-loopback port | -| `HOST_DATA_PATH` | production | no | Persistent documents root on the host | | `HOST_LOG_PATH` | production | no | Persistent application logs on the host | ## Database diff --git a/conf/docker_production_settings.txt b/conf/docker_production_settings.txt index bd1f69d3..13b4297b 100644 --- a/conf/docker_production_settings.txt +++ b/conf/docker_production_settings.txt @@ -32,9 +32,8 @@ APP_SHELL='/sbin/nologin' APP_PORT='8001' ### Persistent host sources -# Documents are stored below HOST_DATA_PATH; application logs use -# HOST_LOG_PATH. Both paths must exist or be creatable by container_install.sh. -HOST_DATA_PATH='/srv/containers/bind/relecov-iskylims' +# Documents use the Compose-managed app_documents named volume. +# Application logs remain on the host and must be creatable by the installer. HOST_LOG_PATH='/var/log/local/relecov-iskylims/apps' # Host bind source rendered before Compose starts. It must be a file path, not # a directory; the parent directory is created and protected by the installer. diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index 3f19c8cb..acb0c3f7 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -19,7 +19,6 @@ APP_SHELL='/bin/bash' APP_PORT='8001' ### Disposable persistence paths -HOST_DATA_PATH='/tmp/relecov-iskylims/data' HOST_LOG_PATH='/tmp/relecov-iskylims/logs' # Test images render settings during build and do not mount this host path. DJANGO_SETTINGS_PATH='/tmp/relecov-iskylims/data/settings/iskylims/settings.py' diff --git a/container_install.sh b/container_install.sh index cfe8a296..5ff0b6eb 100755 --- a/container_install.sh +++ b/container_install.sh @@ -185,16 +185,13 @@ prepare_application_host_sources() { # that add-on declares writable bind sources. prepare_host_bind_source_permissions() { [ "$mode" = production ] || return 0 - local data_path log_path settings_path uid gid - data_path="$(service_environment_value app HOST_DATA_PATH)" + local log_path settings_path uid gid log_path="$(service_environment_value app HOST_LOG_PATH)" settings_path="$(service_environment_value app DJANGO_SETTINGS_PATH)" - [ -n "$data_path" ] || { echo "HOST_DATA_PATH is required for app" >&2; return 1; } [ -n "$log_path" ] || { echo "HOST_LOG_PATH is required for app" >&2; return 1; } [ -n "$settings_path" ] || { echo "DJANGO_SETTINGS_PATH is required for app" >&2; return 1; } uid="$(service_uid app)"; gid="$(service_gid app)" local -a app_host_bind_permission_spec=( - "$data_path/documents|$uid:$gid|0775" "$log_path|$uid:$gid|0775" "$(dirname "$settings_path")|-|0755" "$settings_path|$uid:$gid|0664" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index f45b57bd..e80627bc 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -32,7 +32,7 @@ services: DJANGO_SECRET_KEY: ${APP_DJANGO_SECRET_KEY:?APP_DJANGO_SECRET_KEY is required} DJANGO_ALLOWED_HOSTS: ${APP_DJANGO_ALLOWED_HOSTS:?APP_DJANGO_ALLOWED_HOSTS is required} volumes: - - ${APP_HOST_DATA_PATH:?APP_HOST_DATA_PATH is required}/documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:Z + - app_documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:z - ${APP_HOST_LOG_PATH:?APP_HOST_LOG_PATH is required}:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/logs:Z - ${APP_DJANGO_SETTINGS_PATH:?APP_DJANGO_SETTINGS_PATH is required}:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/iskylims/settings.py:Z - app_static:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/static:z @@ -77,7 +77,7 @@ services: # GENERATED MULTI-APP MOUNTS: one read-only static/documents pair for # every Django service. React assets remain inside the React image. - app_static:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/static:ro,z - - ${APP_HOST_DATA_PATH:?APP_HOST_DATA_PATH is required}/documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:ro,Z + - app_documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:ro,z # OPTIONAL APP MOUNTS belong in ADDONS.apache.MOUNTS. Examples: # - reports_data:/var/www/reports:ro,z # - /srv/public-downloads:/var/www/downloads:ro,Z @@ -88,4 +88,5 @@ networks: deployment_net: volumes: + app_documents: app_static: From 788ed9c70a94c4bbf37411a0de3b2fc53fd6717a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 12:37:44 +0200 Subject: [PATCH 15/55] feat: map the container host gateway for Django --- .bu-isciii-deployment/state.json | 16 ++++++++-------- conf/INSTALL_SETTINGS.md | 4 +++- conf/docker_production_settings.txt | 2 ++ docker-compose.prod.yml | 5 +++++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 6c8ba99a..f08f1a76 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -34,23 +34,23 @@ ".dockerignore": "49a5b2223fb2a4f4201c19a833c60ba91f3e979632b6ae45fd4aca4cf22abb32", ".gitignore": "5f08b2eb5e81fa7a1f4d5bc73df8ba6741e4a3f78925890c3fd1018f2188545b", "Dockerfile": "1a3f8ccccfd84f0a59efda8446f53954fade8b200738a630420564eb8c6e7d5f", - "LEAME.md": "3ad8b8437b135446306682f0ae401fd6310e82f4d6816be37b8ac6241a399fae", - "README.md": "afe6fd080f81b93afab6a35bc147e48640a1425d2063571d1bda7b34fde13ad2", - "conf/INSTALL_SETTINGS.md": "c447b2aa506081b1eb551368f29a7eeb3da527018f4270f952b58bc276c28fe4", + "LEAME.md": "bc8fd9d904310157a7ac5749f5bd5526d13fc6bb5479046bffba8d7ec511f64c", + "README.md": "91e2323ba243ed7cf5932723c779498d762d8c2aad7b83f608d2137b0ccdee1e", + "conf/INSTALL_SETTINGS.md": "2d22baa2dc448f8f076a213ad9c9dc0ee2d555c5db72b586bb92759f4f105c57", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "7d22c5d707efe6190d799fd75d08114dd67ef9077548cd34d5df1f174bd45677", "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", - "conf/docker_production_settings.txt": "b26df69209a678020b430793f97cc96660d36a781dc623d3a8f47fa4f478d1c5", - "conf/docker_test_settings.txt": "925eff6f30ced4d39d3fc83f9edd1e2a71de05ff573d9d39cf07d19850ffb24a", + "conf/docker_production_settings.txt": "e7ac0087409a8acae7646e56e6c70d542c9d22b3b0dc4fc044e6e3c2c0a37e56", + "conf/docker_test_settings.txt": "96c59eda21936e181ae3e07911ec5edf7766fb1235c166827f84266e55ada57e", "conf/template_settings.py": "590369c8b3db2e18b35deb04b2a4abf05e60f65d41ba0983b3b4af8ef20e5039", - "container_install.sh": "d67c9c36db42b793b2fcfb027d9dadeea91048450edc7c176df8b628b1ed32b3", + "container_install.sh": "20e9aa271ba6661de079b4e078992c2390122e110e3d7e8854c4b341d58acf29", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", - "docker-compose.prod.yml": "fccf8f382a57a4333a061710009021dad43ec3298c4ef487e82e3dc1f2eb6244", + "docker-compose.prod.yml": "9feb8d66df5aee56822fbec2fa190ad151d17c3b838623211a41d42fb5480f3f", "docker-compose.test.yml": "ad5b5f540757859812160d03b4fb05c1b2ab208cb9fa6f359a3ffb12ec6e2973", - "install.sh": "6d68766e042e068e2aad8a3cf69e5bbee0542d36c587bc12bd8d64d80a64404a", + "install.sh": "f0dde4a5ebe610c2e23bb8fd3730c124f2d90ce4c7bd0a0ba55831cc720c999d", "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, diff --git a/conf/INSTALL_SETTINGS.md b/conf/INSTALL_SETTINGS.md index 6d782acf..c9f18f79 100644 --- a/conf/INSTALL_SETTINGS.md +++ b/conf/INSTALL_SETTINGS.md @@ -26,7 +26,9 @@ production secrets. Operators copy it to an ignored, permission-restricted file. | `DB_ROOT_PASSWORD` | test only | yes | Root password for disposable Compose MySQL | Production uses an external database. The production Compose file deliberately -contains no database service and publishes no database port. +contains no database service and publishes no database port. When that database +runs on the container host, use `host.docker.internal` with either Docker or +Podman; the production service maps it to the host gateway. ## Django and HTTP diff --git a/conf/docker_production_settings.txt b/conf/docker_production_settings.txt index 13b4297b..8127cd8f 100644 --- a/conf/docker_production_settings.txt +++ b/conf/docker_production_settings.txt @@ -41,6 +41,8 @@ DJANGO_SETTINGS_PATH='/srv/containers/bind/relecov-iskylims/settings/iskylims/se ### External production database # Use a host name or IP reachable from inside the application container. +# For a database running on the container host with either Docker or Podman, +# use host.docker.internal; the production Compose profile maps that alias. DB_HOST='CHANGE_ME' DB_PORT='3306' DB_NAME='CHANGE_ME' diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e80627bc..f5d345fc 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -43,6 +43,11 @@ services: retries: 12 ports: - "127.0.0.1:${APP_APP_PORT:?APP_APP_PORT is required}:${APP_APP_PORT:?APP_APP_PORT is required}" + # Docker Engine on Linux needs this explicit mapping. Podman also supports + # host-gateway and conventionally exposes host.docker.internal itself, so + # host-based dependencies can use one hostname with either engine. + extra_hosts: + - "host.docker.internal:host-gateway" networks: [deployment_net] # END BU-ISCIII SERVICE: app # BEGIN BU-ISCIII ADDON: apache From 5807f56b4aad6ce99e6269db4adb91653c4d6230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 12:42:46 +0200 Subject: [PATCH 16/55] fix: assign Apache log bind ownership --- .bu-isciii-deployment/state.json | 2 +- container_install.sh | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index f08f1a76..d566c601 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -43,7 +43,7 @@ "conf/docker_production_settings.txt": "e7ac0087409a8acae7646e56e6c70d542c9d22b3b0dc4fc044e6e3c2c0a37e56", "conf/docker_test_settings.txt": "96c59eda21936e181ae3e07911ec5edf7766fb1235c166827f84266e55ada57e", "conf/template_settings.py": "590369c8b3db2e18b35deb04b2a4abf05e60f65d41ba0983b3b4af8ef20e5039", - "container_install.sh": "20e9aa271ba6661de079b4e078992c2390122e110e3d7e8854c4b341d58acf29", + "container_install.sh": "fed1ad8fc78b04e98808c2785f8bd3123c687efc7598f877a1b575fd8641f285", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", diff --git a/container_install.sh b/container_install.sh index 5ff0b6eb..dc11a731 100755 --- a/container_install.sh +++ b/container_install.sh @@ -205,7 +205,10 @@ prepare_host_bind_source_permissions() { "$script_dir/deployment/apache/00-logs.conf|-|0644" "$script_dir/deployment/apache/01-reverse-proxy.conf|-|0644" "$script_dir/deployment/apache/02-server-status.conf|-|0644" - "$apache_log_path|-|0775" + # registry.access.redhat.com/ubi9/httpd-24 runs as UID 1001 with GID 0. + # The shared helper applies these IDs directly for Docker and through + # podman unshare when the bind source belongs to a rootless userns. + "$apache_log_path|1001:0|0775" ) apply_host_permission_spec "${apache_host_bind_permission_spec[@]}" } From 85a9c493fca68ff28a3238bc1d60f11779e318e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 13:01:25 +0200 Subject: [PATCH 17/55] fix: prepare Docker bind ownership without sudo --- deployment/lib/container/common.sh | 54 ++++++++++++++++++++++++++---- deployment/lib/container/django.sh | 2 +- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/deployment/lib/container/common.sh b/deployment/lib/container/common.sh index dae70b9d..eb4aa5d7 100644 --- a/deployment/lib/container/common.sh +++ b/deployment/lib/container/common.sh @@ -247,8 +247,32 @@ copy_with_podman_fallback() { return 1 } -# Change host-path modes, using Podman unshare for rootless mapped paths. -chmod_with_podman_fallback() { +# Run one ownership/mode operation through rootful Docker. Every bind source is +# resolved first and the host root is always rejected. +docker_host_path_operation() { + local operation="$1" + local value="$2" + shift 2 + local path resolved_path + + [[ "$operation" =~ ^(chown|chmod)$ ]] || return 2 + for path in "$@"; do + resolved_path="$(readlink -f -- "$path" 2>/dev/null || true)" + if [ -z "$resolved_path" ] || [ "$resolved_path" = / ]; then + echo "Refusing Docker $operation fallback for unsafe path: $path" >&2 + return 1 + fi + engine_exec run --rm --user 0 \ + --volume "$resolved_path:/target:z" \ + --entrypoint "/usr/bin/$operation" \ + registry.access.redhat.com/ubi9/ubi-minimal:latest \ + -R "$value" /target || return 1 + done +} + +# Change host-path modes, entering the selected engine's ownership context when +# an ordinary host operation is not permitted. +chmod_with_engine_fallback() { local mode_value="$1" shift @@ -258,12 +282,21 @@ chmod_with_podman_fallback() { if [ "$engine" = "podman" ] && podman unshare chmod "$mode_value" "$@"; then return 0 fi + if [ "$engine" = "docker" ]; then + [[ "$mode_value" =~ ^[0-7]{3,4}$ ]] || { + echo "Docker mode fallback requires a numeric mode: $mode_value" >&2 + return 1 + } + docker_host_path_operation chmod "$mode_value" "$@" && return 0 + fi echo "Failed to chmod $mode_value: $*" >&2 return 1 } -# Change host-path ownership recursively, using Podman unshare when required. -chown_with_podman_fallback() { +# Change host-path ownership recursively. Rootless Podman can operate on mapped +# IDs through its user namespace. Rootful Docker can perform the same change +# through a tightly scoped bind mount without requiring host sudo access. +chown_with_engine_fallback() { local owner="$1" shift @@ -273,6 +306,13 @@ chown_with_podman_fallback() { if [ "$engine" = "podman" ] && podman unshare chown -R "$owner" "$@"; then return 0 fi + if [ "$engine" = "docker" ]; then + [[ "$owner" =~ ^[0-9]+(:[0-9]+)?$ ]] || { + echo "Docker ownership fallback requires a numeric UID or UID:GID: $owner" >&2 + return 1 + } + docker_host_path_operation chown "$owner" "$@" && return 0 + fi echo "Failed to chown $owner: $*" >&2 return 1 } @@ -292,10 +332,10 @@ apply_host_permission_spec() { fi [ -e "$path" ] || continue if [ "$owner" != "-" ]; then - chown_with_podman_fallback "$owner" "$path" || return 1 + chown_with_engine_fallback "$owner" "$path" || return 1 fi if [ "$mode" != "-" ]; then - chmod_with_podman_fallback "$mode" "$path" || return 1 + chmod_with_engine_fallback "$mode" "$path" || return 1 fi done } @@ -524,7 +564,7 @@ render_config_template() { sed -E -i "s|$escaped_token|$escaped_value|g" "$tmp_file" done if copy_with_podman_fallback "$tmp_file" "$dst" \ - && chmod_with_podman_fallback "$file_mode" "$dst"; then + && chmod_with_engine_fallback "$file_mode" "$dst"; then rm -f "$tmp_file" return 0 fi diff --git a/deployment/lib/container/django.sh b/deployment/lib/container/django.sh index 0723d6ff..56dbcd28 100644 --- a/deployment/lib/container/django.sh +++ b/deployment/lib/container/django.sh @@ -90,7 +90,7 @@ prepare_django_settings_bind_mount() { # reach upgrades. render_django_settings_file preserves the existing # non-placeholder SECRET_KEY, and render_config_template installs atomically. render_django_settings_file "$template_path" "$settings_path" "$install_conf_path" - chmod_with_podman_fallback 0664 "$settings_path" + chmod_with_engine_fallback 0664 "$settings_path" } # Apply ownership and mode to a Django settings file visible inside a running From b16a94166a4a0ac883b46ecf29bcab37eb39863f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 14:06:18 +0200 Subject: [PATCH 18/55] docs: restore deployment and application guidance --- .bu-isciii-deployment/state.json | 6 +- LEAME.md | 387 ++++++++++++++++--- README.md | 630 ++++++++++++++++++++++++++++--- conf/INSTALL_SETTINGS.md | 30 +- 4 files changed, 946 insertions(+), 107 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index d566c601..2c22e9c8 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -34,9 +34,9 @@ ".dockerignore": "49a5b2223fb2a4f4201c19a833c60ba91f3e979632b6ae45fd4aca4cf22abb32", ".gitignore": "5f08b2eb5e81fa7a1f4d5bc73df8ba6741e4a3f78925890c3fd1018f2188545b", "Dockerfile": "1a3f8ccccfd84f0a59efda8446f53954fade8b200738a630420564eb8c6e7d5f", - "LEAME.md": "bc8fd9d904310157a7ac5749f5bd5526d13fc6bb5479046bffba8d7ec511f64c", - "README.md": "91e2323ba243ed7cf5932723c779498d762d8c2aad7b83f608d2137b0ccdee1e", - "conf/INSTALL_SETTINGS.md": "2d22baa2dc448f8f076a213ad9c9dc0ee2d555c5db72b586bb92759f4f105c57", + "LEAME.md": "6c155fea14afbece0456341cd25289cc9fbdf4399a696b800ac6da125b660346", + "README.md": "27dff7de5a21baf1e30aec58117a987c5d5eeb6ebb64d0dcc7836e2bebfcda92", + "conf/INSTALL_SETTINGS.md": "59b7bb116b0870b2f7565abc7923aa91ec5d4ad73f3269417a3a2e5b6043b925", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "7d22c5d707efe6190d799fd75d08114dd67ef9077548cd34d5df1f174bd45677", "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", diff --git a/LEAME.md b/LEAME.md index 1519c75d..8fc24e9a 100644 --- a/LEAME.md +++ b/LEAME.md @@ -1,79 +1,370 @@ -# iSkyLIMS production installation and operations +# Actualizacion de iSkyLIMS con Podman rootless -## 1. Pre-installation checks +Esta guia es la lista de ejecucion para instalar, actualizar y recuperar el +despliegue de produccion. Los comandos generados son reutilizables; antes de la +aprobacion, el responsable de la aplicacion debe completar los campos marcados +`` con valores o referencias institucionales verificadas. -1. Record the reviewed revision for every service build context. -2. Verify host capacity, engine/Compose versions, DNS, TLS and network access. -3. Verify database and identity dependencies are reachable from containers. -4. Review the generated Compose model with the protected environment file. +## Indice -## 2. Host preparation +- [Requisitos](#requisitos) +- [Estructura de directorios en los servidores](#estructura-de-directorios-en-los-servidores) +- [Preparar directorios del host](#preparar-directorios-del-host) +- [Actualizar codigo](#actualizar-codigo) +- [Configurar los ajustes de produccion](#configurar-los-ajustes-de-produccion) +- [Backup antes de actualizar](#backup-antes-de-actualizar) +- [Ejecutar la actualizacion](#ejecutar-la-actualizacion) +- [Comprobaciones posteriores](#comprobaciones-posteriores) +- [Rollback](#rollback) +- [Reparar permisos](#reparar-permisos) +- [Operaciones utiles](#operaciones-utiles) +- [Notas de permisos](#notas-de-permisos) -Create only the documented database, document, log, configuration and proxy -bind sources. Apply the UID/GID, modes and SELinux labels declared by each -component's independent permission specification. +## Requisitos -## 3. Configuration preparation - -Prepare one mode-`0600` production settings file per application service. -Selected infrastructure add-ons contribute clearly marked sections to one -application's file; they do not introduce additional settings files. -Confirm that no file contains unresolved `CHANGE_ME` values and no `VITE_*` -setting contains a secret. - -## 4. First installation +- Podman rootless y un proveedor de Compose funcionales. +- El mismo usuario sin privilegios para el instalador y Podman. +- Revision aprobada: ``. +- DNS/TLS, base de datos, almacenamiento, correo e identidad: ``. +- Responsable operativo y contacto de escalado: ``. +- Objetivos RPO/RTO y ubicacion de backups: ``. ```bash -bash container_install.sh --action install --engine podman \ - --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt +podman info +podman compose version || podman-compose --version ``` -## 5. Verification +No ejecutar `container_install.sh` con `sudo`. Podman rootless, el proveedor de +Compose y el instalador deben usar siempre la misma cuenta. Los ejemplos usan +`podman compose`; si el host proporciona `podman-compose`, sustituir ese prefijo +completo. La libreria compartida detecta ambos proveedores automaticamente. + +## Estructura de directorios en los servidores + +Todos los despliegues usan esta estructura institucional. El nombre de la +aplicacion separa sus fuentes bind, logs y backups; Podman administra su propio +storage y no debe modificarse manualmente. + +```text +/opt/containers_apps/ +└── relecov-iskylims/ + ├── backup/ # Backups locales opcionales + └── relecov-iskylims/ # Clone Git y configuracion protegida -Run the generated smoke test against direct service endpoints and then verify -the public Apache/TLS endpoint, authentication redirects and one real read -workflow. Record `compose ps`, image IDs and deployed revisions. +/srv/containers/ +├── backup/ +│ └── relecov-iskylims/ # Backup central recomendado +├── bind/ +│ └── relecov-iskylims/ +│ └── settings/ # settings.py renderizado por servicio +├── shared/ # Datos compartidos entre aplicaciones +└── storage/ + └── / # Storage rootless gestionado por Podman -## 6. Upgrade +/var/log/local/ +└── relecov-iskylims/ + ├── apache/ + └── apps/ +``` -1. Review the supported source/target versions and configuration differences. -2. Back up all assets listed below from one consistent recovery point. -3. Run `container_install.sh --action upgrade` with the new revision. -4. Stop on build, readiness, bootstrap, migration or smoke-test failure. +Persistencia declarada por el despliegue: -| Asset | Production location | Recovery requirement | +| Activo | Ubicacion de produccion | Requisito de recuperacion | |---|---|---| | `app` database | External production database | Database backup before migration | | `app` documents | `app_documents` named volume | Volume backup | | `app` static | `app_static` named volume | Replaceable through collectstatic | +| `app` logs | `/var/log/local/relecov-iskylims/apps` host bind | Retain/rotate per institutional log policy | +| `app` rendered settings | `/srv/containers/bind/relecov-iskylims/settings/` host bind | Protected configuration backup | +| Apache logs | `/var/log/local/relecov-iskylims/apache` host bind | Retain/rotate per institutional log policy | +| Rendered Apache configuration | `deployment/apache/` in the deployment checkout | Rebuildable; preserve reviewed source configuration | | Samba test data | `samba_test_data` named volume | Disposable test/demo files | -## 7. Backup and restore +## Preparar directorios del host + +Crear la estructura comun antes de la primera instalacion. Sustituir +`` por la cuenta que ejecutara siempre Podman y el instalador. + +```bash +sudo mkdir -p /opt/containers_apps/relecov-iskylims +sudo mkdir -p /srv/containers/backup/relecov-iskylims +sudo mkdir -p /srv/containers/bind/relecov-iskylims/settings +sudo mkdir -p /var/log/local/relecov-iskylims/apps +sudo mkdir -p /var/log/local/relecov-iskylims/apache +sudo chown -R : \ + /opt/containers_apps/relecov-iskylims \ + /srv/containers/backup/relecov-iskylims \ + /srv/containers/bind/relecov-iskylims \ + /var/log/local/relecov-iskylims +``` + +Aplicar despues UID/GID internos, modos y etiquetas SELinux mediante el +instalador. No modificar el arbol `/srv/containers/storage/` manualmente. + +```bash +bash container_install.sh --action fix-permissions --engine podman \ + --install_conf_map app,/protected/app_production_settings.txt +``` + +## Actualizar codigo + +Para un checkout nuevo: + +```bash +cd /opt/containers_apps/relecov-iskylims +git clone https://github.com/BU-ISCIII/iSkyLIMS.git relecov-iskylims +cd relecov-iskylims +git checkout +``` + +En un checkout existente, verificar primero que no haya cambios locales y +cambiar a la revision entregada mediante el procedimiento Git de la institucion. +Registrar el commit exacto con `git rev-parse HEAD`. + +## Configurar los ajustes de produccion -Test database dumps and filesystem/volume backups before relying on them. -Restore databases, documents and identity state from the same recovery point, -then deploy the compatible application revisions and rerun all smoke tests. +Crear un fichero ignorado y con modo `0600` por servicio a partir de su +`conf/docker_production_settings.txt`. Resolver todos los `CHANGE_ME` y revisar +la matriz [`conf/INSTALL_SETTINGS.md`](conf/INSTALL_SETTINGS.md). No guardar +secretos en `.env.production.file`, Compose, Git ni argumentos de proceso. -## 8. Rollback +Valores que requieren decision del responsable de la aplicacion: -Use an image-only rollback only when schema compatibility is confirmed. -Otherwise stop writes, restore the complete recovery point, deploy the recorded -previous revisions and verify direct and public endpoints before reopening. +- hostnames publicos, TLS y proxy; +- base de datos y credenciales de minimo privilegio; +- rutas persistentes, UID/GID, SELinux y politica de backup; +- correo, identidad, almacenamiento y ajustes propios de la aplicacion; +- administrador inicial y transferencia segura de sus credenciales. -## 9. Permission repair +## Backup antes de actualizar + +Crear un directorio identificado y registrar el estado desplegado: ```bash +BACKUP_DIR="/srv/containers/backup/relecov-iskylims/$(date +%Y%m%d_%H%M%S)" +mkdir -p "$BACKUP_DIR" +git rev-parse HEAD > "$BACKUP_DIR/git-revision.txt" +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + images > "$BACKUP_DIR/images.txt" +cp .env.production.file "$BACKUP_DIR/" +cp "$BACKUP_DIR/" +chmod -R go-rwx "$BACKUP_DIR" +``` + +Exportar la base de datos externa desde un punto coherente: + +```bash +mysqldump --single-transaction --routines --triggers \ + --host= --port= --user= --password \ + > "$BACKUP_DIR/database.sql" +``` + +Localizar y exportar cada volumen no reconstruible declarado en la tabla: + +```bash +podman volume ls | grep 'relecov-iskylims' +podman volume export > "$BACKUP_DIR/documents.tar" +podman volume export > "$BACKUP_DIR/static.tar" +``` + +Exportar `documents` y `static` por cada servicio Django que los declare; +omitir esos comandos para perfiles sin dichos volumenes. Aunque `static` puede +regenerarse con `collectstatic`, conservarlo permite una restauracion exacta. + +Guardar tambien los bind mounts persistentes. Los logs se conservan segun su +politica de retencion; la configuracion protegida debe incluirse siempre. + +```bash +tar -C /srv/containers/bind -czf "$BACKUP_DIR/bind-mounts.tar.gz" relecov-iskylims +tar -C /var/log/local -czf "$BACKUP_DIR/logs.tar.gz" relecov-iskylims +sha256sum "$BACKUP_DIR"/* > "$BACKUP_DIR/SHA256SUMS" +``` + +No continuar hasta verificar los ficheros, espacio disponible y procedimiento +de restauracion. + +## Ejecutar la actualizacion + +Primera instalacion: + +```bash +bash container_install.sh --action install --engine podman \ + --git_revision \ + --install_conf_map app,/protected/app_production_settings.txt +``` + +Actualizacion: + +```bash +bash container_install.sh --action upgrade --engine podman \ + --git_revision \ + --install_conf_map app,/protected/app_production_settings.txt +``` + +Durante `--action upgrade`, `container_install.sh`: + +1. valida opciones, configuraciones protegidas y Compose antes de modificar el + despliegue; +2. genera `.env.production.file` y las configuraciones runtime protegidas; +3. prepara bind mounts, propietarios, modos y etiquetas SELinux; +4. construye las imagenes desde la revision aprobada; +5. recrea la topologia conservando volumenes y bind mounts persistentes; +6. espera readiness y repara los volumenes desde los contenedores en ejecucion; +7. ejecuta el bootstrap requerido por cada perfil —checks, migraciones, + scripts/fixtures y `collectstatic` para Django—; +8. ejecuta el smoke test y solo entonces declara completada la actualizacion. + +Seguir ademas la guia especifica de la version cuando exista. Detenerse ante +cualquier fallo de build, readiness, bootstrap, migracion o smoke test. + +## Comprobaciones posteriores + +```bash +podman compose --env-file .env.production.file -f docker-compose.prod.yml ps +podman compose --env-file .env.production.file -f docker-compose.prod.yml logs --tail 200 +bash scripts/smoke_test.sh --engine podman +``` + +Verificar tambien ``, autenticacion, correo, tareas +programadas y un flujo real de lectura. Registrar estado, imagenes, revision y +resultado de aceptacion. + +## Rollback + +Si el esquema y los formatos persistentes siguen siendo compatibles, desplegar +la revision anterior registrada y repetir las pruebas: + +```bash +bash container_install.sh --action upgrade --engine podman \ + --git_revision \ + --install_conf_map app,/protected/app_production_settings.txt +``` + +Si no son compatibles, detener escrituras y restaurar el punto completo: + +```bash +podman compose --env-file .env.production.file -f docker-compose.prod.yml down +mysql --host= --port= --user= --password \ + < "$BACKUP_DIR/database.sql" +podman volume import "$BACKUP_DIR/documents.tar" +podman volume import "$BACKUP_DIR/static.tar" +tar -C /srv/containers/bind -xzf "$BACKUP_DIR/bind-mounts.tar.gz" +cp "$BACKUP_DIR/" / +``` + +Restaurar el fichero de ajustes protegido, desplegar la revision anotada en +`git-revision.txt` y dejar que el instalador regenere `.env.production.file`. +Ejecutar `fix-permissions`, arrancar y validar antes de +reabrir el servicio. Los volumenes deben existir y estar vacios antes de +`podman volume import`; recrearlos con Compose cuando sea necesario. + +## Reparar permisos + +Ejecutar esta accion cuando: + +- se hayan creado o restaurado bind mounts o volumenes; +- se hayan recreado contenedores manualmente; +- hayan cambiado `APP_UID`, `APP_GID` o el usuario rootless; +- existan errores de escritura en logs, documentos, static o configuracion; +- SELinux rechace un bind mount revisado; +- Apache o la aplicacion fallen por propietarios/modos incorrectos. + +Primera fase, incluso con los contenedores detenidos: + +```bash +bash container_install.sh --action fix-permissions --engine podman \ + --install_conf_map app,/protected/app_production_settings.txt +``` + +Esta accion no construye imagenes, no migra la base de datos y no borra datos. +Con los contenedores detenidos repara los bind mounts accesibles desde el host. +Arrancar y repetirla para reparar tambien los volumenes montados: + +```bash +podman compose --env-file .env.production.file -f docker-compose.prod.yml up -d bash container_install.sh --action fix-permissions --engine podman \ --install_conf_map app,/protected/app_production_settings.txt ``` -This action MUST NOT build images, migrate databases or delete data. +## Operaciones utiles + +```bash +podman compose --env-file .env.production.file -f docker-compose.prod.yml ps +podman compose --env-file .env.production.file -f docker-compose.prod.yml logs --tail 200 +podman compose --env-file .env.production.file -f docker-compose.prod.yml up -d +podman compose --env-file .env.production.file -f docker-compose.prod.yml restart +podman compose --env-file .env.production.file -f docker-compose.prod.yml down +``` + +### Servicio Django `app` + +```bash +# Logs separados del servicio. +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + logs --tail 200 app + +# Entrar al contenedor. +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + exec app bash + +# Regenerar static sin ejecutar migraciones. +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + exec app bash -lc \ + 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py collectstatic --noinput' + +# Diagnostico previo a una recuperacion de bootstrap. +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + exec app bash -lc \ + 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check --deploy && python manage.py showmigrations --plan' +``` + +La recuperacion preferida es corregir la causa y repetir +`container_install.sh --action install|upgrade` con la misma revision y +configuracion protegida. Si el instalador no puede completarse y el responsable +autoriza un bootstrap manual despues del backup: + +```bash +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + exec app bash -lc \ + 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py migrate --noinput && python manage.py collectstatic --noinput' +``` + +Registrar este procedimiento excepcional y ejecutar despues el smoke test. + +### Servicio Apache + +```bash +# Logs separados de Apache y validacion de configuracion. +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + logs --tail 200 apache +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + exec apache httpd -t + +# Estado restringido; usar valores del fichero protegido. +APACHE_PORT='CHANGE_ME' +SERVER_STATUS_SERVER_NAME='localhost' +curl --fail --show-error \ + --header "Host: $SERVER_STATUS_SERVER_NAME" \ + "http://127.0.0.1:$APACHE_PORT/server-status?auto" +``` + +Para diagnosticos SELinux y ModSecurity, comprobar el bind de logs antes de +reiniciar: + +```bash +ls -ldZ /var/log/local/relecov-iskylims/apache +``` + +Si aparece `ModSecurity: Failed to open debug log file`, conservar el fichero +para diagnostico, ejecutar `fix-permissions` y reiniciar. Si hay que sustituir +el inode, moverlo primero a un backup en vez de borrarlo. -## 10. Incident troubleshooting +## Notas de permisos -1. Capture service state and logs. -2. Test direct application health. -3. Test internal DNS and dependent databases/identity services. -4. Test Apache routing and external TLS last. -5. Preserve evidence and backups before any destructive recovery action. +- Ejecutar siempre Podman y el instalador con el mismo usuario rootless. +- No usar `sudo container_install.sh` ni cambiar propietarios dentro del storage + de Podman. +- Mantener estables los UID/GID de runtime entre actualizaciones. +- Revisar etiquetas SELinux y propietarios de bind mounts mediante + `fix-permissions`. +- Preservar evidencias y backups antes de cualquier recuperacion destructiva. diff --git a/README.md b/README.md index e9212b9d..f5ad59b7 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,97 @@ # iSkyLIMS -Laboratory information management system for wet-lab and dry-lab sequencing workflows. +iSkyLIMS is a laboratory information management system for genomics facilities. +It tracks massive-sequencing work from library preparation and sequencing-run +registration through FASTQ generation, quality control, bioinformatics service +requests, analysis and delivery to researchers. + +## What is iSkyLIMS? + +iSkyLIMS connects wet-lab and dry-lab activities in one traceable workflow: + +- **WetLab** records projects, samples, library preparation, pools and + sequencing runs. It follows each run through registration, sample-sheet + delivery, data processing, statistics generation and completion, and exposes + reports by sample, project, run and investigation. +- **DryLab** manages bioinformatics service requests associated with sequencing + data, including requested analyses, status, files, resolution and delivery. +- **Clinic** and shared core modules provide the supporting application data, + permissions, configuration and APIs used by those workflows. + +![iSkyLIMS sequencing workflow](img/iskylims_scheme.png) + +## Infrastructure + +The supported deployment separates application runtime from institutional +services and persistent data: + +- The `app` service runs Django under Gunicorn as an unprivileged user. +- The `apache` service is the public container reverse proxy and serves the + shared static files. Production TLS may terminate there or at the + institution's upstream proxy, according to the reviewed deployment. +- Production uses an external MySQL or MariaDB database. The disposable test + stack creates its own MySQL service. +- Production sequencing storage is an externally managed Samba share. The test + stack provides a disposable Samba service and can load fixtures and demo NGS + data through the installer. +- Documents and collected static files use persistent named volumes. Django + settings, application logs and Apache logs use the standardized protected + host paths documented below. +- Scheduled Django jobs run through Supercronic inside the application + container from the project's `CRONJOBS` setting. + +The same lifecycle supports Docker and Podman for local testing and production, +plus the reviewed Django bare-metal procedure. For issues, use the +[iSkyLIMS issue tracker](https://github.com/BU-ISCIII/iSkyLIMS/issues). + +- [What is iSkyLIMS?](#what-is-iskylims) +- [Infrastructure](#infrastructure) +- [Get the code (required)](#get-the-code-required) +- [Choose your path](#choose-your-path) +- [Minimum requirements](#minimum-requirements) +- [Docker deployment](#docker-deployment) + - [Local test stack](#local-test-stack) + - [Production container](#production-container) + - [Manage containers after installation](#manage-containers-after-installation) + - [Upgrade docker deployment](#upgrade-docker-deployment) +- [Bare-metal deployment (Ubuntu/CentOS)](#bare-metal-deployment-ubuntucentos) +- [Common operations (Docker + bare-metal)](#common-operations-docker--bare-metal) +- [Final configuration steps](#final-configuration-steps) + - [Configure Samba](#configure-samba) + - [Verify email](#verify-email) + - [Run the iSkyLIMS configuration tests](#run-the-iskylims-configuration-tests) +- [Developer notes](#developer-notes) +- [Application documentation](#application-documentation) + +## Get the code (required) -## Architecture and services +```bash +git clone https://github.com/BU-ISCIII/iSkyLIMS.git relecov-iskylims +cd relecov-iskylims +``` + +For an orchestrated deployment, every external build context in the service +table must exist at the declared path relative to this checkout. + +## Choose your path + +| Capability | Supported | Owner or command | +|---|---:|---| +| Docker local test | Yes | `container_install.sh --test --engine docker` | +| Podman local test | Yes | `container_install.sh --test --engine podman` | +| Docker production | Yes | `container_install.sh --engine docker` | +| Podman production | Yes | `container_install.sh --engine podman` | +| Bare metal | Profile-specific | See [Bare-metal deployment](#bare-metal-deployment-ubuntucentos) | +| Upgrade | Yes | `--action upgrade` | +| Permission repair | Yes | `--action fix-permissions` | +| Backup and restore | Yes | Operator-owned; follow [LEAME.md](LEAME.md) | + +Services: | Service | Profile | Build context | Internal port | |---|---|---|---:| | `app` | `django` | `.` | settings: `APP_PORT` | -Profile behavior: - - Django services build with an ephemeral settings secret, render protected host settings, and run controlled migration/bootstrap steps. Selected add-ons: @@ -17,99 +99,541 @@ Selected add-ons: - Apache source configuration lives under `conf/apache/`; customize its virtual hosts and routes there. The installer renders final bind sources under `deployment/apache/`. - The Samba add-on provides disposable NGS demo storage only in `--test` mode. -All selected services and add-ons are assembled into one -`docker-compose.prod.yml` and one `docker-compose.test.yml`. +## Minimum requirements -## Requirements +- Git and access to every declared build context. +- Docker Engine with Compose v2, or Podman with a Compose provider. +- Enough disk and memory for image builds and persistent application data. +- A protected production settings file for every application service. +- Production DNS, TLS termination, database, storage, email, identity, backup, + and monitoring services required by the selected profiles. -- Docker with Compose v2, or Podman with a Compose provider. -- Git and access to every declared service build context. -- One protected production settings file for every application service. -- Production DNS, TLS termination, databases, storage and backup destinations - required by the selected profiles. +Copy each service's `conf/docker_production_settings.txt` to a protected, +ignored file, set mode `0600`, and replace every `CHANGE_ME` value. The exact +meaning and security classification of settings is in +[`conf/INSTALL_SETTINGS.md`](conf/INSTALL_SETTINGS.md). -## Configuration +## Docker deployment -Each application repository owns its configuration template. Copy it to a -protected, ignored file, set mode `0600`, and resolve every `CHANGE_ME` value. -Browser-visible `VITE_*` values MUST NOT contain secrets. -Apache and Keycloak settings appear as commented `APACHE_*` and `KEYCLOAK_*` -sections in the selected application's same settings file. +Both engines use the same lifecycle and Compose files. Do not invoke Compose +directly for the first install or an upgrade: the installer also renders +configuration, prepares permissions, waits for readiness, and runs bootstrap. -The installer generates one protected `.env.production.file` or -`.env.test.file`. Service prefixes prevent collisions between settings such as -`APP_PORT`, `DB_HOST`, `APP_UID` and `APP_GID`. +### Local test stack -## Test installation +Docker: ```bash bash container_install.sh --test --action install --engine docker \ --git_revision current ``` -## Production installation +Podman: ```bash -bash container_install.sh --action install --engine podman \ +bash container_install.sh --test --action install --engine podman \ + --git_revision current +``` + +Test settings and test services are disposable. Verify either deployment with: + +```bash +bash scripts/smoke_test.sh --test --engine docker +# or: bash scripts/smoke_test.sh --test --engine podman +``` + +Django test installation creates the disposable database declared by the test +Compose profile, waits for it, applies committed migrations, optionally loads +fixtures, runs selected data scripts, collects static files, and performs the +generated health checks. + +Migration/data scripts are repeatable `django-extensions` runscript names. Use +`--script_before` for preparation before migrations and `--script` (an alias of +`--script_after`) for a transformation after migrations: + +```bash +bash container_install.sh --test --action install --engine docker \ + --script_before prepare_test_data \ + --script migrate_optional_values +``` + +`--demo_data`, `--skip_demo_data`, and `--skip_test_data` are part of the +standard interface. A project that supplies fixtures or demo files must set +`application_supports_test_data=true` and implement `load_test_deployment_data` +in its wrapper; otherwise `--demo_data` is rejected explicitly. + +For an automatic first administrator, set `CREATE_INITIAL_SUPERUSER=true` and +the `DJANGO_SUPERUSER_*` values in the selected test settings before install. +An existing account is never reset. Open the loopback URL using `APP_PORT` from +the rendered test environment, or `APACHE_PORT` when the Apache add-on is used. + +The Samba add-on supplies disposable test storage only. Applications may load +fixtures and demo files into it through `load_test_deployment_data`; production +continues to use the externally managed storage configured by the application. + +### Production container + +Prepare the protected settings files and deploy a reviewed tag or commit. + +Docker: + +```bash +bash container_install.sh --action install --engine docker \ --git_revision \ --install_conf_map app,/protected/app_production_settings.txt ``` -## Upgrade - -Back up persistent state, review release/configuration changes, and run: +Podman: ```bash -bash container_install.sh --action upgrade --engine podman \ - --git_revision \ +bash container_install.sh --action install --engine podman \ + --git_revision \ --install_conf_map app,/protected/app_production_settings.txt ``` -Django services run controlled bootstrap/migrations. React services rebuild an -immutable browser bundle and do not run Django bootstrap. +The installer creates `.env.production.file`; use it for later direct Compose +operations. Production secrets stay in the protected settings files and are +not copied into image layers. -## Persistence and permissions +#### Persist logs/documents on the host | Asset | Production location | Backup/rebuild policy | |---|---|---| | `app` database | External production database | Database backup before migration | | `app` documents | `app_documents` named volume | Volume backup | | `app` static | `app_static` named volume | Replaceable through collectstatic | +| `app` logs | `/var/log/local/relecov-iskylims/apps` host bind | Retain/rotate per institutional log policy | +| `app` rendered settings | `/srv/containers/bind/relecov-iskylims/settings/` host bind | Protected configuration backup | +| Apache logs | `/var/log/local/relecov-iskylims/apache` host bind | Retain/rotate per institutional log policy | +| Rendered Apache configuration | `deployment/apache/` in the deployment checkout | Rebuildable; preserve reviewed source configuration | | Samba test data | `samba_test_data` named volume | Disposable test/demo files | -Run permission repair without building or migrating: +The standard fixes application binds below `/srv/containers/bind/relecov-iskylims` +and logs below `/var/log/local/relecov-iskylims`. The operator must still record the +backup owner, retention, actual engine volume names, and restore-test evidence +for every non-rebuildable asset. Never treat a container writable layer as +persistent storage. + +#### Reverse proxy and application server + +The selected profiles and add-ons define the internal application server and +proxy topology. Review public hostnames, TLS ownership, forwarded headers, +request limits, timeouts, health paths, and static/media routing together. + +#### Scheduled jobs + +The application developer must list every scheduler/worker, whether a failed +job blocks a workflow, and how operators inspect and retry it. Do not add an +untracked host cron job when the application profile owns scheduling. + +### Manage containers after installation + +Use the engine that performed the installation: + +```bash +docker compose --env-file .env.production.file -f docker-compose.prod.yml ps +docker compose --env-file .env.production.file -f docker-compose.prod.yml logs --tail 200 +docker compose --env-file .env.production.file -f docker-compose.prod.yml restart +``` + +```bash +podman compose --env-file .env.production.file -f docker-compose.prod.yml ps +podman compose --env-file .env.production.file -f docker-compose.prod.yml logs --tail 200 +podman compose --env-file .env.production.file -f docker-compose.prod.yml restart +``` + +### Upgrade docker deployment + +After taking a consistent backup and reading the version-specific upgrade +notes: + +```bash +bash container_install.sh --action upgrade --engine podman \ + --git_revision \ + --install_conf_map app,/protected/app_production_settings.txt +``` + +Replace `podman` with `docker` for a Docker-managed deployment. Stop on build, +readiness, bootstrap, migration, or smoke-test failure. See [LEAME.md](LEAME.md) +for the ordered production checklist and rollback decision. + +## Bare-metal deployment (Ubuntu/CentOS) + +### Install + +#### Clone the repository + +Use [Get the code (required)](#get-the-code-required) and check out the reviewed +revision. + +#### Prepare the database + +Provision the application database and least-privilege account outside the +installer. Confirm that the host can reach it before bootstrap. + +#### Configure install_settings.txt + +Start from `conf/docker_production_settings.txt`, but review all paths and +container-oriented defaults for the target host. Keep the resulting file +ignored and mode `0600`. + +#### Run install.sh + +The Django profile includes `install.sh` for application staging and bootstrap, +but system package, database, web-server, service-manager, TLS, and backup +provisioning remain host-specific. Bare-metal installation is supported only +after the application developer documents and tests those integrations. + +```bash +# Stage application files and dependencies. +bash install.sh --stage install --git_revision current \ + --conf conf/docker_production_settings.txt + +# Bootstrap the prepared runtime (settings, migrations and static files). +bash install.sh --bootstrap install \ + --conf conf/docker_production_settings.txt +``` + +For upgrades, take a backup and replace both `install` actions with `upgrade`. +Do not use container-oriented paths or defaults on a bare-metal host without an +application-specific review. + +For a host-managed Apache 2.4 deployment, adapt the reviewed virtual host from +`conf/apache/` to the distribution path. The generated add-on files target the +container image, so do not copy them blindly without checking module names, +paths, runtime user, TLS ownership, and log locations. + +Ubuntu/Debian baseline: + +```bash +sudo cp /etc/apache2/sites-available/relecov-iskylims.conf +sudo a2enmod proxy proxy_http headers +sudo a2ensite relecov-iskylims.conf +sudo apache2ctl configtest +sudo systemctl reload apache2 +``` + +CentOS/RHEL baseline: + +```bash +sudo cp /etc/httpd/conf.d/relecov-iskylims.conf +sudo httpd -t +sudo systemctl reload httpd +``` + +The reviewed virtual host must define the public `ServerName`, proxy to the +Django `APP_PORT`, serve the correct static/media paths, preserve forwarded +scheme/host headers, and use institutionally managed TLS and logs. + +### Upgrade bare-metal deployment + +Follow the same staged lifecycle with `upgrade` only after a consistent backup +and review of the version-specific guide. + +## Common operations (Docker + bare-metal) + +### Database creation, users and grants + +Production databases are externally managed unless the application documents a +different supported topology. Create a dedicated schema and least-privilege +account, verify connectivity from the application container, and keep DBA +commands and credentials outside this repository. + +Connect as an authorized database administrator without putting the password +on the command line: + +```bash +DB_HOST='CHANGE_ME' +DB_PORT='3306' +DB_ADMIN='CHANGE_ME' +DB_NAME='CHANGE_ME' +DB_USER='CHANGE_ME' +mysql --host="$DB_HOST" --port="$DB_PORT" --user="$DB_ADMIN" --password +``` + +Create the application database and account. Replace every angle-bracket value; +restrict the account host further than `%` when the network topology permits. + +```sql +CREATE DATABASE `` + CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER ''@'%' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON ``.* TO ''@'%'; +FLUSH PRIVILEGES; +``` + +Verify the same endpoint and least-privilege credentials configured for the +application: + +```bash +mysql --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" --password \ + --database="$DB_NAME" --execute='SELECT 1;' +``` + +### Backups + +Back up every non-rebuildable row in the persistence table from one consistent +recovery point before installation or upgrade. Record the revision, image IDs, +settings files, and backup identifiers. + +```bash +BACKUP_DIR="/srv/containers/backup/relecov-iskylims/$(date +%Y%m%d_%H%M%S)" +SETTINGS_FILE='/protected/app_production_settings.txt' +DOCUMENTS_VOLUME='CHANGE_ME' +DB_HOST='CHANGE_ME' +DB_PORT='3306' +DB_NAME='CHANGE_ME' +DB_USER='CHANGE_ME' +mkdir -p "$BACKUP_DIR" +git rev-parse HEAD > "$BACKUP_DIR/git-revision.txt" +cp .env.production.file "$SETTINGS_FILE" "$BACKUP_DIR/" +chmod -R go-rwx "$BACKUP_DIR" + +mysqldump --single-transaction --routines --triggers \ + --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" --password \ + "$DB_NAME" > "$BACKUP_DIR/database.sql" + +podman volume ls | grep 'relecov-iskylims' +podman volume export "$DOCUMENTS_VOLUME" > "$BACKUP_DIR/documents.tar" +tar -C /srv/containers/bind -czf "$BACKUP_DIR/bind-mounts.tar.gz" relecov-iskylims +sha256sum "$BACKUP_DIR"/* > "$BACKUP_DIR/SHA256SUMS" +``` + +For Docker, archive a named volume through a temporary container after ensuring +the application is not writing to it: + +```bash +docker run --rm \ + --volume "$DOCUMENTS_VOLUME":/data:ro \ + --volume "$BACKUP_DIR":/backup \ + alpine tar -C /data -cf /backup/documents.tar . +``` + +The full ordered backup checklist, including logs and image metadata, is in +[LEAME.md](LEAME.md). + +### Restore / rollback + +An image-only rollback is safe only when the previous application version +supports the current schema and persistent-file format. Otherwise stop writes, +restore the database and files from the same recovery point, deploy the recorded +compatible revision, and rerun all smoke tests. + +Compatible application-only rollback: + +```bash +bash container_install.sh --action upgrade --engine podman \ + --git_revision \ + --install_conf_map app,/protected/app_production_settings.txt +``` + +Full restore when schema or persistent-file formats are incompatible: ```bash +BACKUP_DIR='/srv/containers/backup/relecov-iskylims/CHANGE_ME' +DOCUMENTS_VOLUME='CHANGE_ME' +DB_HOST='CHANGE_ME' +DB_PORT='3306' +DB_NAME='CHANGE_ME' +DB_USER='CHANGE_ME' +podman compose --env-file .env.production.file -f docker-compose.prod.yml down +mysql --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" --password \ + "$DB_NAME" < "$BACKUP_DIR/database.sql" +podman volume import "$DOCUMENTS_VOLUME" "$BACKUP_DIR/documents.tar" +tar -C /srv/containers/bind -xzf "$BACKUP_DIR/bind-mounts.tar.gz" bash container_install.sh --action fix-permissions --engine podman \ --install_conf_map app,/protected/app_production_settings.txt ``` -Each application and add-on has independent host-bind and running-container -permission specifications in `container_install.sh`. +Then deploy the revision recorded in `git-revision.txt`, start the deployment, +and run the smoke test before reopening service. For Docker volume restoration, +reverse the temporary-container archive command by mounting the empty target +volume at `/data` and extracting `/backup/documents.tar` there. + +### What to do if something fails + +1. Preserve installer output, `compose ps`, image IDs, and service logs. +2. Test the direct application health endpoint and dependencies. +3. Test proxy routing, public DNS, and TLS after direct health succeeds. +4. Run permission repair for reviewed ownership or SELinux drift: + + ```bash + bash container_install.sh --action fix-permissions --engine podman \ + --install_conf_map app,/protected/app_production_settings.txt + ``` + +5. Do not fake migrations, delete volumes, or rebuild from an unrecorded + revision as a first response. + +### Service-specific operational commands + +#### Django service `app` + +```bash +# Logs and an interactive shell (replace podman with docker when applicable). +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + logs --tail 200 app +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + exec app bash + +# Rebuild static assets without running migrations. +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + exec app bash -lc \ + 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py collectstatic --noinput' + +# Inspect Django and migration state before deciding whether to recover. +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + exec app bash -lc \ + 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check --deploy && python manage.py showmigrations --plan' +``` + +For bootstrap recovery, fix the cause and rerun `container_install.sh` with the +same revision, protected configuration, and `--action install` or `upgrade`. +This safely recreates the temporary runtime configuration and repeats the +controlled migration/fixture/static lifecycle. Direct `manage.py migrate` is a +diagnostic last resort and must use the same backup and release procedure. + +#### Apache service + +```bash +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + logs --tail 200 apache +podman compose --env-file .env.production.file -f docker-compose.prod.yml \ + exec apache httpd -t + +APACHE_PORT='CHANGE_ME' +SERVER_STATUS_SERVER_NAME='localhost' +curl --fail --show-error \ + --header "Host: $SERVER_STATUS_SERVER_NAME" \ + "http://127.0.0.1:$APACHE_PORT/server-status?auto" +``` + +Keep `SERVER_STATUS_ALLOW_FROM` limited to trusted diagnostic hosts. If SELinux +is enabled, inspect the persistent log bind and confirm a container-compatible +label before restarting: + +```bash +ls -ldZ /var/log/local/relecov-iskylims/apache +``` -## Testing +An Apache failure containing `ModSecurity: Failed to open debug log file` often +means the existing `modsec_debug.log` inode has stale ownership or labeling. +Preserve it for diagnosis, run `fix-permissions`, and restart Apache. If it must +be replaced, move it to a timestamped backup instead of deleting evidence: ```bash -bash scripts/smoke_test.sh --engine podman \ - --compose_file docker-compose.prod.yml \ - --env_file .env.production.file +sudo mv /var/log/local/relecov-iskylims/apache/modsec_debug.log \ + /var/log/local/relecov-iskylims/apache/modsec_debug.log.blocked +bash container_install.sh --action fix-permissions --engine podman \ + --install_conf_map app,/protected/app_production_settings.txt +podman compose --env-file .env.production.file -f docker-compose.prod.yml restart apache ``` -The generated smoke dispatcher combines framework checks with direct HTTP -health checks. Add authenticated application workflows without removing the -generated baseline checks. +## Final configuration steps + +Sign in with the administrator account after the first installation. The +installer creates that account only when `CREATE_INITIAL_SUPERUSER=true` and +the protected `DJANGO_SUPERUSER_*` settings are provided. + +### Configure Samba + +iSkyLIMS stores the connection to the institutional sequencing-data share in +its application configuration. The Samba container included in the test stack +is disposable; production must use the reviewed external share. + +1. Open **Massive sequencing** from the iSkyLIMS home page. + + ![Open the WetLab module](img/got_to_wetlab.png) + +2. Go to **Configuration → Samba configuration**. +3. Enter the server/share, credentials and paths for the sequencing run + folders. Use the disposable `SAMBA_USER` and `SAMBA_PASSWORD` only in the + local test deployment. +4. Save the configuration and verify the connection before enabling scheduled + run discovery. + + ![iSkyLIMS Samba configuration form](img/samba_form.png) + +### Verify email -## Rollback and restore +1. Open **Massive sequencing → Configuration → Email configuration**. +2. Confirm the sender and recipients expected by the laboratory workflow. +3. Send a test message and verify delivery. Do not approve production while + application email fails, even when the SMTP connection itself succeeds. + +### Run the iSkyLIMS configuration tests + +After every clean installation, upgrade, restore or Samba/email change: + +1. Run the generated deployment smoke test: + + ```bash + bash scripts/smoke_test.sh --engine podman + ``` + + Replace `podman` with `docker` for a Docker-managed deployment. + +2. Sign in as an administrator and open + `/wetlab/configurationTest/`. For the institutional + deployment, the current endpoint is + [iSkyLIMS configuration test](https://iskylims.isciii.es/wetlab/configurationTest/). +3. Submit the configuration test and inspect every result tab. +4. Confirm the database and Samba checks succeed. +5. Run the available checks for each configured sequencing platform, including + MiSeq, NextSeq and NovaSeq where enabled. +6. Confirm a representative WetLab read workflow and record the result with the + deployed revision. + +## Developer notes + +### Shared container installer library + +`container_install.sh` sources the vendored files under +`deployment/lib/container/`. Do not edit those copies. Check or update them +from the standards repository with `scaffold.py check-lib` or `sync-lib`. + +### Schema migration workflow + +Django migrations MUST be generated, reviewed, tested, and committed with the +release. Installation and production upgrade run `migrate --noinput`; they +MUST NOT run `makemigrations` or silently manufacture schema history. + +For a legacy application entering the standard: + +1. Generate and commit baseline migrations from the last supported stable tag. +2. Generate and commit new migrations for later model changes. +3. Identify only legacy application labels whose existing tables match the + committed initial migrations exactly. +4. Document and run `migrate --fake-initial` once through a reviewed + application migration callback or version-specific upgrade step. The common + bootstrap does not apply `FAKEINITIAL_MODULES` automatically. +5. Put ordered data transformations in version-specific upgrade guides and run + them through `--script_before`, `--script_after`, or `--script`. +6. Verify `showmigrations --plan` has no unapplied entries after bootstrap. + +Never use `--fake` to conceal a failed or partially applied migration. Normal +new installations and subsequent upgrades use the committed migration graph +without `--fake-initial`. + +### Persistent host paths + +Keep source checkouts, protected configuration, bind mounts, engine-managed +volumes, logs, and backups separate. For rootless Podman, run the installer as +the same unprivileged account every time and use `fix-permissions` instead of +manually changing engine storage. + +### Verification of the installation + +```bash +bash scripts/smoke_test.sh --engine podman +``` -Record the deployed image/revision before every upgrade. An image-only rollback -is safe only when the previous application version supports the current schema. -Otherwise restore databases and persistent files from the same recovery point. -See [LEAME.md](LEAME.md) for the ordered production procedure. +Complete the application-specific Samba, email and sequencing checks in +[Final configuration steps](#final-configuration-steps) after the generated +baseline succeeds. -## Troubleshooting +## Application documentation -- Inspect `compose ps` and service logs before restarting anything. -- Use `fix-permissions` for reviewed ownership/mode drift. -- Test direct service health before debugging Apache or external TLS. -- Verify Keycloak issuer, public hostname, clients and redirect URIs together. -- Never fake Django migrations or delete persistent volumes as a first response. +- [User and administrator documentation](https://iskylims.readthedocs.io/en/latest/) +- [Version-specific upgrade guides](docs/upgrades/README.md) +- [Issue tracker](https://github.com/BU-ISCIII/iSkyLIMS/issues) diff --git a/conf/INSTALL_SETTINGS.md b/conf/INSTALL_SETTINGS.md index c9f18f79..e55db958 100644 --- a/conf/INSTALL_SETTINGS.md +++ b/conf/INSTALL_SETTINGS.md @@ -4,6 +4,12 @@ `docker_production_settings.txt` is a template and MUST NOT contain real production secrets. Operators copy it to an ignored, permission-restricted file. +Values already rendered from `project.json` (application name, module, Python +version and default paths) provide a runnable baseline. Operators customize +environment-dependent values. Application developers add domain settings to +both settings templates, this matrix, and `template_settings.py`; adding an +undocumented environment variable alone does not configure Django. + ## Application and filesystem | Variable | Required | Secret | Meaning | @@ -12,9 +18,14 @@ production secrets. Operators copy it to an ignored, permission-restricted file. | `INSTALL_PATH` | yes | no | Application runtime root inside the container | | `PROJECT_MODULE` | generated | no | Django package declared by `project.json`; do not customize independently | | `PYTHON_BIN_PATH` | yes | no | Python used to create the virtual environment | +| `REQUIRED_MODULES` | application | no | Import checks required before bootstrap | +| `MIGRATION_MODULES` | application | no | Modules whose committed migrations are applied | +| `FAKEINITIAL_MODULES` | upgrade only | no | Reviewed legacy modules allowed to use `--fake-initial` | | `APP_UID`, `APP_GID` | yes | no | Runtime identity and rootless volume ownership | +| `APP_SHELL` | yes | no | Runtime account shell; normally `/sbin/nologin` in production | | `APP_PORT` | yes | no | Internal Gunicorn and host-loopback port | | `HOST_LOG_PATH` | production | no | Persistent application logs on the host | +| `DJANGO_SETTINGS_PATH` | production | no | Protected rendered `settings.py` bind source on the host | ## Database @@ -38,17 +49,22 @@ Podman; the production service maps it to the host gateway. | `DJANGO_SECRET_KEY` | yes | yes | Stable Django signing key; preserve on upgrade | | `DJANGO_ALLOWED_HOSTS` | yes | no | Exact production hostnames | | `DJANGO_CSRF_TRUSTED_ORIGINS` | production | no | Exact HTTPS origins | +| `DB_CONN_MAX_AGE` | yes | no | Django persistent database connection lifetime | | `WEB_CONCURRENCY` | production | no | Gunicorn workers | | `GUNICORN_THREADS` | production | no | Threads per worker | | `GUNICORN_TIMEOUT` | production | no | Request timeout in seconds | | `GUNICORN_KEEPALIVE` | production | no | Keep-alive time in seconds | | `APP_START_WAIT_TIMEOUT_SECONDS` | yes | no | Maximum wait for staged application files | +| `LOG_TYPE`, `LOG_PATH` | application | no | Application logging backend and optional location | ## Email -Email settings are required when the application sends operational or account -messages. Document whether failed email blocks the user workflow and add an SMTP -test to production acceptance. +`EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_HOST_USER`, `EMAIL_HOST_PASSWORD`, and +`EMAIL_USE_TLS` configure SMTP. The password is secret; the remaining values +are operational unless the username is sensitive locally. Email settings are +required when the application sends operational or account messages. Document +whether failed email blocks the user workflow and add an SMTP test to +production acceptance. ## Initial administrator @@ -63,6 +79,14 @@ Add every application setting here before declaring installation complete. Classify secrets and identify cross-service values that must match an identity provider, proxy, worker, or frontend. +Developer review checklist: + +- map every project-specific token in `template_settings.py`; +- provide safe disposable test values and `CHANGE_ME` production placeholders; +- state validation rules, owner, restart/rebuild impact, and secret status; +- add acceptance checks for email, identity, storage, workers, and scheduled + jobs used by real workflows. + ## Selected infrastructure add-ons ### Apache From 415ef3fcb8433ecfb1658d1a9febc08fef9d1fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 17:38:43 +0200 Subject: [PATCH 19/55] fix: generate iSkyLIMS Django wrapper during staging --- install.sh | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/install.sh b/install.sh index 72d8d21d..60613bd2 100755 --- a/install.sh +++ b/install.sh @@ -105,6 +105,8 @@ fi source "$INSTALL_CONF" : "${INSTALL_PATH:?INSTALL_PATH is required}" : "${PROJECT_MODULE:?PROJECT_MODULE is required}" +[[ "$PROJECT_MODULE" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] \ + || die "PROJECT_MODULE must be a valid Python package name" : "${PYTHON_BIN_PATH:?PYTHON_BIN_PATH is required}" : "${DB_HOST:?DB_HOST is required}" : "${DB_PORT:?DB_PORT is required}" @@ -148,10 +150,11 @@ check_python() { check_required_modules() { local module + [[ -f "$install_script_dir/conf/urls.py" ]] \ + || die "Django URL configuration is missing: conf/urls.py" for module in $REQUIRED_MODULES; do [[ -e "$install_script_dir/$module" ]] || die "Required application module is missing: $module" done - [[ -f "$install_script_dir/conf/urls.py" ]] || die "iSkyLIMS URL configuration is missing" } check_database() { @@ -282,16 +285,11 @@ prepare_application_directories() { stage_application_custom_files() { # Arguments: source directory, final INSTALL_PATH, action (install|upgrade). - # Copy files that intentionally live outside the normal rsync set. - # Example: [[ -f "$1/conf/routing.py" ]] && - # install -m 0644 "$1/conf/routing.py" "$2/$PROJECT_MODULE/routing.py" + # Copy application-owned files that intentionally need extra processing; + # the standard already installs the Django URL and optional routing files. local source_dir="$1" install_path="$2" local template - [[ -f "$install_path/manage.py" ]] || - "$install_path/virtualenv/bin/python" -m django startproject "$PROJECT_MODULE" "$install_path" - install -m 0644 "$source_dir/conf/urls.py" "$install_path/$PROJECT_MODULE/urls.py" - for template in "$source_dir"/conf/*_template.csv "$source_dir"/conf/samples_template.xlsx; do [[ -e "$template" ]] || continue install -m 0644 "$template" "$install_path/documents/wetlab/templates/" @@ -417,13 +415,26 @@ stage_application_files() { checkout_git_revision [[ -d "$INSTALL_PATH/virtualenv" ]] \ || die "virtualenv not found at $INSTALL_PATH; install dependencies first" + # The Django wrapper is deployment-generated and must never be inherited + # from an ignored local source tree or a previous staged installation. + rm -rf "$INSTALL_PATH/$PROJECT_MODULE" + rm -f "$INSTALL_PATH/manage.py" rsync -rl --delete \ --exclude .git --exclude .env --exclude /logs --exclude /documents \ - --exclude /static --exclude /tmp --exclude /virtualenv \ + --exclude /static --exclude /cron --exclude /tmp --exclude /virtualenv \ + --exclude /manage.py --exclude "/$PROJECT_MODULE" \ ./ "$INSTALL_PATH/" mkdir -p "$INSTALL_PATH/logs" "$INSTALL_PATH/documents" \ "$INSTALL_PATH/static" "$INSTALL_PATH/cron" "$INSTALL_PATH/tmp" prepare_application_directories "$INSTALL_PATH" + "$INSTALL_PATH/virtualenv/bin/python" -m django startproject \ + "$PROJECT_MODULE" "$INSTALL_PATH" + install -m 0644 "$install_script_dir/conf/urls.py" \ + "$INSTALL_PATH/$PROJECT_MODULE/urls.py" + if [[ -f "$install_script_dir/conf/routing.py" ]]; then + install -m 0644 "$install_script_dir/conf/routing.py" \ + "$INSTALL_PATH/$PROJECT_MODULE/routing.py" + fi stage_application_custom_files "$install_script_dir" "$INSTALL_PATH" "$ACTION" printf '%s\n' "$GIT_REVISION" > "$INSTALL_PATH/.deployed_revision" if [[ "$RENDER_SETTINGS" == "true" ]]; then From 3423075ede6ad84b0004496534727e4b038178b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 17:55:12 +0200 Subject: [PATCH 20/55] refactor: remove fake-initial iSkyLIMS upgrades --- conf/docker_production_settings.txt | 3 +-- conf/docker_test_settings.txt | 1 - conf/template_install_settings.txt | 1 - install.sh | 3 --- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/conf/docker_production_settings.txt b/conf/docker_production_settings.txt index 8127cd8f..79e7a52a 100644 --- a/conf/docker_production_settings.txt +++ b/conf/docker_production_settings.txt @@ -15,12 +15,11 @@ PYTHON_BIN_PATH='python3.11' ### Application-specific installation modules # Optional space-separated Django applications consumed by installers that -# support dependency, migration and fake-initial module lists. Leave empty when +# support dependency and migration module lists. Leave empty when # the application install.sh does not implement the corresponding behavior. # iSkyLIMS example: REQUIRED_MODULES='core drylab wetlab clinic django_utils' REQUIRED_MODULES='core drylab wetlab clinic django_utils' MIGRATION_MODULES='core drylab wetlab django_utils' -FAKEINITIAL_MODULES='core drylab wetlab django_utils' ### Container runtime identity # UID/GID must own every writable bind mount and named volume. diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index acb0c3f7..5af23112 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -10,7 +10,6 @@ PYTHON_BIN_PATH='python3.11' # Optional lists used only when install.sh implements these operations. REQUIRED_MODULES='core drylab wetlab clinic django_utils' MIGRATION_MODULES='core drylab wetlab django_utils' -FAKEINITIAL_MODULES='core drylab wetlab django_utils' ### Container runtime identity APP_UID='1212' diff --git a/conf/template_install_settings.txt b/conf/template_install_settings.txt index dbc0a72a..f762fc32 100644 --- a/conf/template_install_settings.txt +++ b/conf/template_install_settings.txt @@ -2,7 +2,6 @@ INSTALL_PATH='/opt/iskylims' REQUIRED_MODULES='core drylab wetlab clinic django_utils' MIGRATION_MODULES='core drylab wetlab django_utils' -FAKEINITIAL_MODULES='django_utils iSkyLIMS_core iSkyLIMS_wetlab iSkyLIMS_drylab' ### Container runtime settings # Host directory for Apache bind-mounted configuration files. diff --git a/install.sh b/install.sh index 60613bd2..922aba38 100755 --- a/install.sh +++ b/install.sh @@ -467,9 +467,6 @@ bootstrap_application() { local hook for hook in "${SCRIPT_BEFORE[@]}"; do run_hook "$hook"; done before_django_migrate "$ACTION" "$MIGRATION_MODULES" - if [[ "$ACTION" == "upgrade" ]]; then - python manage.py migrate --noinput --fake-initial - fi python manage.py migrate --noinput if [[ "$LOAD_TABLES" == "true" && "$SKIP_TABLES" == "false" ]]; then [[ -f conf/first_install_tables.json ]] \ From 015a4b7bc5a2eceaa29be8215d373c6e6ed86e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 17:55:12 +0200 Subject: [PATCH 21/55] docs: require committed iSkyLIMS migrations --- README.md | 16 ++++++---------- conf/INSTALL_SETTINGS.md | 1 - 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f5ad59b7..f428e001 100644 --- a/README.md +++ b/README.md @@ -602,18 +602,14 @@ For a legacy application entering the standard: 1. Generate and commit baseline migrations from the last supported stable tag. 2. Generate and commit new migrations for later model changes. -3. Identify only legacy application labels whose existing tables match the - committed initial migrations exactly. -4. Document and run `migrate --fake-initial` once through a reviewed - application migration callback or version-specific upgrade step. The common - bootstrap does not apply `FAKEINITIAL_MODULES` automatically. -5. Put ordered data transformations in version-specific upgrade guides and run +3. Verify the committed migration history matches the supported production + database before deploying it. +4. Put ordered data transformations in version-specific upgrade guides and run them through `--script_before`, `--script_after`, or `--script`. -6. Verify `showmigrations --plan` has no unapplied entries after bootstrap. +5. Verify `showmigrations --plan` has no unapplied entries after bootstrap. -Never use `--fake` to conceal a failed or partially applied migration. Normal -new installations and subsequent upgrades use the committed migration graph -without `--fake-initial`. +Never use `--fake` to conceal a failed or partially applied migration. New +installations and upgrades use the committed migration graph. ### Persistent host paths diff --git a/conf/INSTALL_SETTINGS.md b/conf/INSTALL_SETTINGS.md index e55db958..b5f59d38 100644 --- a/conf/INSTALL_SETTINGS.md +++ b/conf/INSTALL_SETTINGS.md @@ -20,7 +20,6 @@ undocumented environment variable alone does not configure Django. | `PYTHON_BIN_PATH` | yes | no | Python used to create the virtual environment | | `REQUIRED_MODULES` | application | no | Import checks required before bootstrap | | `MIGRATION_MODULES` | application | no | Modules whose committed migrations are applied | -| `FAKEINITIAL_MODULES` | upgrade only | no | Reviewed legacy modules allowed to use `--fake-initial` | | `APP_UID`, `APP_GID` | yes | no | Runtime identity and rootless volume ownership | | `APP_SHELL` | yes | no | Runtime account shell; normally `/sbin/nologin` in production | | `APP_PORT` | yes | no | Internal Gunicorn and host-loopback port | From 97f451a76a56b82ab2c707b0c95ded322bdcce2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 18:01:39 +0200 Subject: [PATCH 22/55] feat: validate iSkyLIMS migration plan before migrate --- install.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 922aba38..9e025646 100755 --- a/install.sh +++ b/install.sh @@ -328,10 +328,17 @@ validate_application_runtime() { before_django_migrate() { # Arguments: action and space-separated MIGRATION_MODULES. This is for # application migration preparation, not the user-selected runscript hooks. - # Example: [[ "$1" == install && -n "$2" ]] && python manage.py makemigrations $2 # iSkyLIMS commits its migrations. Generating migrations during deployment # would make container images and upgrades non-reproducible. - : + local action="$1" migration_modules="$2" module + + echo "Validating Django migration plan for $action" + for module in $migration_modules; do + python manage.py showmigrations "$module" --plan >/dev/null \ + || die "Unable to inspect migrations for Django app: $module" + done + python manage.py migrate --plan --noinput >/dev/null \ + || die "Unable to calculate the Django migration plan" } after_django_migrate() { From 2d85b772ef1b996bfbfd786bcd8d69430992e39b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 18:20:29 +0200 Subject: [PATCH 23/55] refactor: separate iSkyLIMS add-on settings --- .bu-isciii-deployment/state.json | 18 +++++--- conf/apache/apache_production_settings.txt | 52 +++++++++++++++++++++ conf/apache/apache_test_settings.txt | 41 ++++++++++++++++ conf/docker_production_settings.txt | 54 ---------------------- conf/docker_test_settings.txt | 47 ------------------- conf/samba/samba_production_settings.txt | 2 + conf/samba/samba_test_settings.txt | 4 ++ container_install.sh | 33 ++++++------- 8 files changed, 123 insertions(+), 128 deletions(-) create mode 100644 conf/apache/apache_production_settings.txt create mode 100644 conf/apache/apache_test_settings.txt create mode 100644 conf/samba/samba_production_settings.txt create mode 100644 conf/samba/samba_test_settings.txt diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 2c22e9c8..92b99a0d 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -34,23 +34,27 @@ ".dockerignore": "49a5b2223fb2a4f4201c19a833c60ba91f3e979632b6ae45fd4aca4cf22abb32", ".gitignore": "5f08b2eb5e81fa7a1f4d5bc73df8ba6741e4a3f78925890c3fd1018f2188545b", "Dockerfile": "1a3f8ccccfd84f0a59efda8446f53954fade8b200738a630420564eb8c6e7d5f", - "LEAME.md": "6c155fea14afbece0456341cd25289cc9fbdf4399a696b800ac6da125b660346", - "README.md": "27dff7de5a21baf1e30aec58117a987c5d5eeb6ebb64d0dcc7836e2bebfcda92", - "conf/INSTALL_SETTINGS.md": "59b7bb116b0870b2f7565abc7923aa91ec5d4ad73f3269417a3a2e5b6043b925", + "LEAME.md": "694cd2f35b7d34094c3c6ec9ab4858033ffc549b5845b2b7cf25312bfe5a0db3", + "README.md": "fb7dc10bef4b3eac07eb2bddb9a3ede5d55462f5964536d4952a768abc350598", + "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "7d22c5d707efe6190d799fd75d08114dd67ef9077548cd34d5df1f174bd45677", "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", - "conf/docker_production_settings.txt": "e7ac0087409a8acae7646e56e6c70d542c9d22b3b0dc4fc044e6e3c2c0a37e56", - "conf/docker_test_settings.txt": "96c59eda21936e181ae3e07911ec5edf7766fb1235c166827f84266e55ada57e", + "conf/apache/apache_production_settings.txt": "a48153d241283e28d27f4eee1d4bd1f2daf0880c0b3c24cf2c27d4d7526eaff0", + "conf/apache/apache_test_settings.txt": "878b30eba2244891abadfac957a503be71fcc454549fd27afa83f4f2ea09371a", + "conf/docker_production_settings.txt": "bc697e64ba67fe81eb389adf93bf8a5ad5ecbe857516c08f6f5e2f2ee4d75bf6", + "conf/docker_test_settings.txt": "f7a64f7a9c3e06e3845d7b8b424c8b520a6b20f0b24bb6dd5ee8a15492cbcb5a", + "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", + "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", "conf/template_settings.py": "590369c8b3db2e18b35deb04b2a4abf05e60f65d41ba0983b3b4af8ef20e5039", - "container_install.sh": "fed1ad8fc78b04e98808c2785f8bd3123c687efc7598f877a1b575fd8641f285", + "container_install.sh": "62a65dcdcf9b8c58c2cdbbe072e3deb236098e734c20aaf1971e2cbde7ac3188", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", "docker-compose.prod.yml": "9feb8d66df5aee56822fbec2fa190ad151d17c3b838623211a41d42fb5480f3f", "docker-compose.test.yml": "ad5b5f540757859812160d03b4fb05c1b2ab208cb9fa6f359a3ffb12ec6e2973", - "install.sh": "f0dde4a5ebe610c2e23bb8fd3730c124f2d90ce4c7bd0a0ba55831cc720c999d", + "install.sh": "6258861f0fd5e2423d37f573dad725c87a29e6cdfd44c061c258c96094769760", "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, diff --git a/conf/apache/apache_production_settings.txt b/conf/apache/apache_production_settings.txt new file mode 100644 index 00000000..d267f537 --- /dev/null +++ b/conf/apache/apache_production_settings.txt @@ -0,0 +1,52 @@ +# Apache add-on +# Used only when ADDONS.apache selects this application as CONFIG_SERVICE. + +# Repository-owned sources live under /conf/apache. The installer +# renders them to /deployment/apache and mounts those final files; +# APACHE_CONF_PATH is not used by the standard lifecycle. Keep it empty unless +# legacy application code explicitly consumes a different host directory. +# Example legacy override: APACHE_CONF_PATH='/srv/containers/bind/apache-conf' +APACHE_CONF_PATH='' + +# Persistent host directory mounted as the Apache log destination. +APACHE_LOG_PATH='/var/log/local/relecov-iskylims/apache' + +# Public host interface and port. Use 127.0.0.1 when another host reverse proxy +# is the only component that should reach this Apache container. +APACHE_BIND_HOST='0.0.0.0' +APACHE_PORT='8080' + +# Public host name handled by the default VirtualHost. Use localhost for a +# workstation deployment or the externally visible DNS name in production. +APACHE_SERVER_NAME='CHANGE_ME_DNS_NAME' + +# Compose service receiving proxied requests. Leave empty to use the service +# selected by ADDONS.apache.CONFIG_SERVICE (normally app for a single service). +APACHE_UPSTREAM_SERVICE='' + +# Internal application port used by ProxyPass. Leave empty to use APP_PORT from +# the selected service's production settings. +APACHE_UPSTREAM_PORT='' + +# Maximum proxy request duration in seconds. Leave empty to use the selected +# service's GUNICORN_TIMEOUT, falling back to 120 seconds when it is undefined. +APACHE_PROXY_TIMEOUT='' + +# Prefix for the generated -apache.access.log and +# -apache.error.log files. Leave empty to derive it from +# APACHE_SERVER_NAME, with URL schemes, paths and ports removed. +APACHE_LOG_STEM='' + +# Server-status stays restricted by default. Use a local-only diagnostic name; +# aliases and allow-list accept Apache-compatible values. +SERVER_STATUS_SERVER_NAME='localhost' +SERVER_STATUS_ALIASES='127.0.0.1 localhost' +SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' + +# Forwarded headers received by Django behind the production proxy. +APACHE_FORWARDED_PROTO='https' +APACHE_FORWARDED_PORT='443' + +# 50 MiB. The reverse-proxy template applies this value to Apache and, when +# loaded, ModSecurity. Increase it only for endpoints that require larger data. +APACHE_LIMIT_REQUEST_BODY='52428800' diff --git a/conf/apache/apache_test_settings.txt b/conf/apache/apache_test_settings.txt new file mode 100644 index 00000000..615c3f6a --- /dev/null +++ b/conf/apache/apache_test_settings.txt @@ -0,0 +1,41 @@ +# Apache add-on +# Used only when ADDONS.apache selects this application as CONFIG_SERVICE. + +# Sources under /conf/apache are rendered into deployment/apache. +# This compatibility value is unused unless application code consumes it. +APACHE_CONF_PATH='' + +# Disposable test logs and a public test endpoint. +APACHE_LOG_PATH='/tmp/relecov-iskylims/apache-logs' +APACHE_BIND_HOST='0.0.0.0' +APACHE_PORT='8081' + +# Host name handled by the local test VirtualHost. +APACHE_SERVER_NAME='localhost' + +# Compose service receiving proxied requests. Leave empty to use the service +# selected by ADDONS.apache.CONFIG_SERVICE (normally app for a single service). +APACHE_UPSTREAM_SERVICE='' + +# Internal application port used by ProxyPass. Leave empty to use APP_PORT from +# the selected service's test settings. +APACHE_UPSTREAM_PORT='' + +# Maximum proxy request duration in seconds. Leave empty to use the selected +# service's GUNICORN_TIMEOUT, falling back to 120 seconds when it is undefined. +APACHE_PROXY_TIMEOUT='' + +# Prefix for the generated -apache.access.log and +# -apache.error.log files. Leave empty to derive it from +# APACHE_SERVER_NAME, with URL schemes, paths and ports removed. +APACHE_LOG_STEM='' + +# Local server-status defaults. +SERVER_STATUS_SERVER_NAME='localhost' +SERVER_STATUS_ALIASES='127.0.0.1 localhost' +SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' + +# Test proxy headers and request-body limit. +APACHE_FORWARDED_PROTO='http' +APACHE_FORWARDED_PORT='8081' +APACHE_LIMIT_REQUEST_BODY='52428800' diff --git a/conf/docker_production_settings.txt b/conf/docker_production_settings.txt index 79e7a52a..ddc057a5 100644 --- a/conf/docker_production_settings.txt +++ b/conf/docker_production_settings.txt @@ -91,57 +91,3 @@ APP_START_WAIT_TIMEOUT_SECONDS='100' # directory to LOG_PATH, which must already exist and be writable. LOG_TYPE='regular_folder' LOG_PATH='' - -### Selected infrastructure add-ons -# Apache add-on -# Used only when ADDONS.apache selects this application as CONFIG_SERVICE. - -# Repository-owned sources live under /conf/apache. The installer -# renders them to /deployment/apache and mounts those final files; -# APACHE_CONF_PATH is not used by the standard lifecycle. Keep it empty unless -# legacy application code explicitly consumes a different host directory. -# Example legacy override: APACHE_CONF_PATH='/srv/containers/bind/apache-conf' -APACHE_CONF_PATH='' - -# Persistent host directory mounted as the Apache log destination. -APACHE_LOG_PATH='/var/log/local/relecov-iskylims/apache' - -# Public host interface and port. Use 127.0.0.1 when another host reverse proxy -# is the only component that should reach this Apache container. -APACHE_BIND_HOST='0.0.0.0' -APACHE_PORT='8080' - -# Public host name handled by the default VirtualHost. Use localhost for a -# workstation deployment or the externally visible DNS name in production. -APACHE_SERVER_NAME='CHANGE_ME_DNS_NAME' - -# Compose service receiving proxied requests. Leave empty to use the service -# selected by ADDONS.apache.CONFIG_SERVICE (normally app for a single service). -APACHE_UPSTREAM_SERVICE='' - -# Internal application port used by ProxyPass. Leave empty to use APP_PORT from -# the selected service's production settings. -APACHE_UPSTREAM_PORT='' - -# Maximum proxy request duration in seconds. Leave empty to use the selected -# service's GUNICORN_TIMEOUT, falling back to 120 seconds when it is undefined. -APACHE_PROXY_TIMEOUT='' - -# Prefix for the generated -apache.access.log and -# -apache.error.log files. Leave empty to derive it from -# APACHE_SERVER_NAME, with URL schemes, paths and ports removed. -APACHE_LOG_STEM='' - -# Server-status stays restricted by default. Use a local-only diagnostic name; -# aliases and allow-list accept Apache-compatible values. -SERVER_STATUS_SERVER_NAME='localhost' -SERVER_STATUS_ALIASES='127.0.0.1 localhost' -SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' - -# Forwarded headers received by Django behind the production proxy. -APACHE_FORWARDED_PROTO='https' -APACHE_FORWARDED_PORT='443' - -# 50 MiB. The reverse-proxy template applies this value to Apache and, when -# loaded, ModSecurity. Increase it only for endpoints that require larger data. -APACHE_LIMIT_REQUEST_BODY='52428800' diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index 5af23112..4976417a 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -61,50 +61,3 @@ APP_START_WAIT_TIMEOUT_SECONDS='100' # Set LOG_PATH when LOG_TYPE='symbolic_link'. LOG_TYPE='regular_folder' LOG_PATH='' - -### Selected infrastructure add-ons -# Apache add-on -# Used only when ADDONS.apache selects this application as CONFIG_SERVICE. - -# Sources under /conf/apache are rendered into deployment/apache. -# This compatibility value is unused unless application code consumes it. -APACHE_CONF_PATH='' - -# Disposable test logs and a public test endpoint. -APACHE_LOG_PATH='/tmp/relecov-iskylims/apache-logs' -APACHE_BIND_HOST='0.0.0.0' -APACHE_PORT='8081' - -# Host name handled by the local test VirtualHost. -APACHE_SERVER_NAME='localhost' - -# Compose service receiving proxied requests. Leave empty to use the service -# selected by ADDONS.apache.CONFIG_SERVICE (normally app for a single service). -APACHE_UPSTREAM_SERVICE='' - -# Internal application port used by ProxyPass. Leave empty to use APP_PORT from -# the selected service's test settings. -APACHE_UPSTREAM_PORT='' - -# Maximum proxy request duration in seconds. Leave empty to use the selected -# service's GUNICORN_TIMEOUT, falling back to 120 seconds when it is undefined. -APACHE_PROXY_TIMEOUT='' - -# Prefix for the generated -apache.access.log and -# -apache.error.log files. Leave empty to derive it from -# APACHE_SERVER_NAME, with URL schemes, paths and ports removed. -APACHE_LOG_STEM='' - -# Local server-status defaults. -SERVER_STATUS_SERVER_NAME='localhost' -SERVER_STATUS_ALIASES='127.0.0.1 localhost' -SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' - -# Test proxy headers and request-body limit. -APACHE_FORWARDED_PROTO='http' -APACHE_FORWARDED_PORT='8081' -APACHE_LIMIT_REQUEST_BODY='52428800' - -# Samba test/demo data add-on -SAMBA_USER='samba_user' -SAMBA_PASSWORD='sambapasswd' diff --git a/conf/samba/samba_production_settings.txt b/conf/samba/samba_production_settings.txt new file mode 100644 index 00000000..179a870e --- /dev/null +++ b/conf/samba/samba_production_settings.txt @@ -0,0 +1,2 @@ + +# Samba demo storage is intentionally unavailable in production. diff --git a/conf/samba/samba_test_settings.txt b/conf/samba/samba_test_settings.txt new file mode 100644 index 00000000..0bc16384 --- /dev/null +++ b/conf/samba/samba_test_settings.txt @@ -0,0 +1,4 @@ + +# Samba test/demo data add-on +SAMBA_USER='samba_user' +SAMBA_PASSWORD='sambapasswd' diff --git a/container_install.sh b/container_install.sh index dc11a731..d05da234 100755 --- a/container_install.sh +++ b/container_install.sh @@ -16,12 +16,15 @@ APPLICATION_NAME="iSkyLIMS" # lifecycle mechanics below unchanged. # ============================================================================ install_services=(app) +addon_build_services=() permission_services=(app apache) -configured_services=(app) +configured_services=(app apache samba) default_service_install_conf() { case "$1" in app) [ "$mode" = test ] && echo conf/docker_test_settings.txt || echo conf/docker_production_settings.txt ;; + apache) [ "$mode" = test ] && echo conf/apache/apache_test_settings.txt || echo conf/apache/apache_production_settings.txt ;; + samba) [ "$mode" = test ] && echo conf/samba/samba_test_settings.txt || echo conf/samba/samba_production_settings.txt ;; *) return 1 ;; esac } @@ -95,27 +98,12 @@ service_gid() { prepare_compose_environment() { local -a settings_sources=( "APP|${install_conf_host_by_service[app]}" + "|${install_conf_host_by_service[apache]}" + "|${install_conf_host_by_service[samba]}" ) local -a deployment_values=( "GIT_REVISION|$git_revision" "APP_IMAGE|relecov-iskylims:local" - "APACHE_CONF_PATH|$(config_value_or_default APACHE_CONF_PATH "${install_conf_host_by_service[app]}" '')" - "APACHE_LOG_PATH|$(config_value_or_default APACHE_LOG_PATH "${install_conf_host_by_service[app]}" '')" - "APACHE_BIND_HOST|$(config_value_or_default APACHE_BIND_HOST "${install_conf_host_by_service[app]}" '')" - "APACHE_PORT|$(config_value_or_default APACHE_PORT "${install_conf_host_by_service[app]}" '')" - "APACHE_SERVER_NAME|$(config_value_or_default APACHE_SERVER_NAME "${install_conf_host_by_service[app]}" '')" - "APACHE_UPSTREAM_SERVICE|$(config_value_or_default APACHE_UPSTREAM_SERVICE "${install_conf_host_by_service[app]}" '')" - "APACHE_UPSTREAM_PORT|$(config_value_or_default APACHE_UPSTREAM_PORT "${install_conf_host_by_service[app]}" '')" - "APACHE_PROXY_TIMEOUT|$(config_value_or_default APACHE_PROXY_TIMEOUT "${install_conf_host_by_service[app]}" '')" - "APACHE_LOG_STEM|$(config_value_or_default APACHE_LOG_STEM "${install_conf_host_by_service[app]}" '')" - "SERVER_STATUS_SERVER_NAME|$(config_value_or_default SERVER_STATUS_SERVER_NAME "${install_conf_host_by_service[app]}" '')" - "SERVER_STATUS_ALIASES|$(config_value_or_default SERVER_STATUS_ALIASES "${install_conf_host_by_service[app]}" '')" - "SERVER_STATUS_ALLOW_FROM|$(config_value_or_default SERVER_STATUS_ALLOW_FROM "${install_conf_host_by_service[app]}" '')" - "APACHE_FORWARDED_PROTO|$(config_value_or_default APACHE_FORWARDED_PROTO "${install_conf_host_by_service[app]}" '')" - "APACHE_FORWARDED_PORT|$(config_value_or_default APACHE_FORWARDED_PORT "${install_conf_host_by_service[app]}" '')" - "APACHE_LIMIT_REQUEST_BODY|$(config_value_or_default APACHE_LIMIT_REQUEST_BODY "${install_conf_host_by_service[app]}" '')" - "SAMBA_USER|$(config_value_or_default SAMBA_USER "${install_conf_host_by_service[app]}" '')" - "SAMBA_PASSWORD|$(config_value_or_default SAMBA_PASSWORD "${install_conf_host_by_service[app]}" '')" ) compose_env_file="$script_dir/.env.${mode}.file" write_compose_environment_file "$compose_env_file" settings_sources deployment_values @@ -348,7 +336,7 @@ Options: --engine docker|podman --git_revision --install_conf First application service only. - --install_conf_map Repeat for every service override. + --install_conf_map Repeat for application and add-on overrides. --compose_file --script_before --script_after @@ -408,7 +396,7 @@ if [ -n "$install_conf" ]; then install_conf_host_by_service["${install_services for mapping in "${install_conf_map_entries[@]}"; do [[ "$mapping" == *,* ]] || die "Invalid --install_conf_map: $mapping" service_name="${mapping%%,*}"; path="${mapping#*,}" - array_contains "$service_name" "${configured_services[@]}" || die "Unknown mapped service: $service_name" + array_contains "$service_name" "${configured_services[@]}" || die "Unknown mapped component: $service_name" install_conf_host_by_service["$service_name"]="$path" done for service_name in "${configured_services[@]}"; do @@ -476,6 +464,11 @@ for service_name in "${install_services[@]}"; do --tag "$(service_image_name "$service_name")" "$context" fi done +# Build add-on images through Compose so their declared build arguments and +# add-on-owned Dockerfiles remain the single source of truth. +for service_name in "${addon_build_services[@]}"; do + deployment_compose -f "$compose_file" build --no-cache "$service_name" +done # 7. Recreate and start the complete topology from one Compose invocation so # freshly built images and the current configuration are deployed consistently. # Named volumes and bind-mounted persistent data are preserved. From 0bbc9905b199ba51dc0d60643d7bb8e6a3f43fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 18:20:29 +0200 Subject: [PATCH 24/55] docs: map protected iSkyLIMS add-on configuration --- LEAME.md | 12 ++++++------ README.md | 14 +++++++------- conf/INSTALL_SETTINGS.md | 5 +++++ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/LEAME.md b/LEAME.md index 8fc24e9a..b6f74cdc 100644 --- a/LEAME.md +++ b/LEAME.md @@ -103,7 +103,7 @@ instalador. No modificar el arbol `/srv/containers/storage/` manualmente. ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` ## Actualizar codigo @@ -190,7 +190,7 @@ Primera instalacion: ```bash bash container_install.sh --action install --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` Actualizacion: @@ -198,7 +198,7 @@ Actualizacion: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` Durante `--action upgrade`, `container_install.sh`: @@ -237,7 +237,7 @@ la revision anterior registrada y repetir las pruebas: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` Si no son compatibles, detener escrituras y restaurar el punto completo: @@ -273,7 +273,7 @@ Primera fase, incluso con los contenedores detenidos: ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` Esta accion no construye imagenes, no migra la base de datos y no borra datos. @@ -283,7 +283,7 @@ Arrancar y repetirla para reparar tambien los volumenes montados: ```bash podman compose --env-file .env.production.file -f docker-compose.prod.yml up -d bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` ## Operaciones utiles diff --git a/README.md b/README.md index f428e001..dc679faa 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ Docker: ```bash bash container_install.sh --action install --engine docker \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` Podman: @@ -188,7 +188,7 @@ Podman: ```bash bash container_install.sh --action install --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` The installer creates `.env.production.file`; use it for later direct Compose @@ -250,7 +250,7 @@ notes: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` Replace `podman` with `docker` for a Docker-managed deployment. Stop on build, @@ -424,7 +424,7 @@ Compatible application-only rollback: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` Full restore when schema or persistent-file formats are incompatible: @@ -442,7 +442,7 @@ mysql --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" --password \ podman volume import "$DOCUMENTS_VOLUME" "$BACKUP_DIR/documents.tar" tar -C /srv/containers/bind -xzf "$BACKUP_DIR/bind-mounts.tar.gz" bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` Then deploy the revision recorded in `git-revision.txt`, start the deployment, @@ -459,7 +459,7 @@ volume at `/data` and extracting `/backup/documents.tar` there. ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt ``` 5. Do not fake migrations, delete volumes, or rebuild from an unrecorded @@ -525,7 +525,7 @@ be replaced, move it to a timestamped backup instead of deleting evidence: sudo mv /var/log/local/relecov-iskylims/apache/modsec_debug.log \ /var/log/local/relecov-iskylims/apache/modsec_debug.log.blocked bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt + --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt podman compose --env-file .env.production.file -f docker-compose.prod.yml restart apache ``` diff --git a/conf/INSTALL_SETTINGS.md b/conf/INSTALL_SETTINGS.md index b5f59d38..4de3e9fb 100644 --- a/conf/INSTALL_SETTINGS.md +++ b/conf/INSTALL_SETTINGS.md @@ -88,6 +88,11 @@ Developer review checklist: ## Selected infrastructure add-ons +Add-ons use independent settings below `conf//`. For production, copy +the required add-on templates to protected files and pass them through the same +repeatable `--install_conf_map ,` option used by application +services. + ### Apache `APACHE_LOG_PATH`, `APACHE_BIND_HOST`, `APACHE_PORT`, From 33f472ad16b2ac8f271459e32b8a1f25dd7c0e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 18:25:06 +0200 Subject: [PATCH 25/55] docs: stage protected iSkyLIMS deployment settings --- .bu-isciii-deployment/state.json | 8 ++++---- .dockerignore | 1 + .gitignore | 1 + LEAME.md | 22 +++++++++++++++------ README.md | 33 ++++++++++++++++++++++---------- 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 92b99a0d..9d46f22f 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -31,11 +31,11 @@ "TIMEZONE": "Europe/Madrid" }, "files": { - ".dockerignore": "49a5b2223fb2a4f4201c19a833c60ba91f3e979632b6ae45fd4aca4cf22abb32", - ".gitignore": "5f08b2eb5e81fa7a1f4d5bc73df8ba6741e4a3f78925890c3fd1018f2188545b", + ".dockerignore": "a76e2a974786736d1d3744ed534ae9996514cb15d921328450f3757fafe273d2", + ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", "Dockerfile": "1a3f8ccccfd84f0a59efda8446f53954fade8b200738a630420564eb8c6e7d5f", - "LEAME.md": "694cd2f35b7d34094c3c6ec9ab4858033ffc549b5845b2b7cf25312bfe5a0db3", - "README.md": "fb7dc10bef4b3eac07eb2bddb9a3ede5d55462f5964536d4952a768abc350598", + "LEAME.md": "299a140e487d7bd983a0553946135c605cd134e69b05ee00504a0b6efd46c772", + "README.md": "72d509f37059e2ecd39791b3b430fadded441fa928b0a06997a44680e5f4a551", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "7d22c5d707efe6190d799fd75d08114dd67ef9077548cd34d5df1f174bd45677", diff --git a/.dockerignore b/.dockerignore index efb8c832..9349a560 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,6 +12,7 @@ conf/*settings*.txt .env.prod.file # Runtime-rendered add-on bind sources are not image build inputs. deployment/apache/*.conf +deployment/settings/ .venv/ venv/ diff --git a/.gitignore b/.gitignore index 6d55e1d7..250c23fc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ my_prod_settings*.txt *.bu-isciii-update # Final add-on bind sources are regenerated from repository-owned conf files. deployment/apache/*.conf +deployment/settings/ # Python and runtime artifacts __pycache__/ diff --git a/LEAME.md b/LEAME.md index b6f74cdc..31a6b26b 100644 --- a/LEAME.md +++ b/LEAME.md @@ -103,7 +103,7 @@ instalador. No modificar el arbol `/srv/containers/storage/` manualmente. ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` ## Actualizar codigo @@ -128,6 +128,16 @@ Crear un fichero ignorado y con modo `0600` por servicio a partir de su la matriz [`conf/INSTALL_SETTINGS.md`](conf/INSTALL_SETTINGS.md). No guardar secretos en `.env.production.file`, Compose, Git ni argumentos de proceso. +```bash +install -d -m 0700 deployment/settings +install -m 0600 conf/docker_production_settings.txt deployment/settings/app_production_settings.txt +install -m 0600 conf/apache/apache_production_settings.txt deployment/settings/apache_production_settings.txt +install -m 0600 conf/samba/samba_production_settings.txt deployment/settings/samba_production_settings.txt +``` + +Editar unicamente las copias bajo `deployment/settings/`. Los comandos de +instalacion y actualizacion usan estas rutas protegidas. + Valores que requieren decision del responsable de la aplicacion: - hostnames publicos, TLS y proxy; @@ -190,7 +200,7 @@ Primera instalacion: ```bash bash container_install.sh --action install --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Actualizacion: @@ -198,7 +208,7 @@ Actualizacion: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Durante `--action upgrade`, `container_install.sh`: @@ -237,7 +247,7 @@ la revision anterior registrada y repetir las pruebas: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Si no son compatibles, detener escrituras y restaurar el punto completo: @@ -273,7 +283,7 @@ Primera fase, incluso con los contenedores detenidos: ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Esta accion no construye imagenes, no migra la base de datos y no borra datos. @@ -283,7 +293,7 @@ Arrancar y repetirla para reparar tambien los volumenes montados: ```bash podman compose --env-file .env.production.file -f docker-compose.prod.yml up -d bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` ## Operaciones utiles diff --git a/README.md b/README.md index dc679faa..d5eb82aa 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,19 @@ ignored file, set mode `0600`, and replace every `CHANGE_ME` value. The exact meaning and security classification of settings is in [`conf/INSTALL_SETTINGS.md`](conf/INSTALL_SETTINGS.md). +Create the ignored deployment settings directory and copy every production +template used by this deployment: + +```bash +install -d -m 0700 deployment/settings +install -m 0600 conf/docker_production_settings.txt deployment/settings/app_production_settings.txt +install -m 0600 conf/apache/apache_production_settings.txt deployment/settings/apache_production_settings.txt +install -m 0600 conf/samba/samba_production_settings.txt deployment/settings/samba_production_settings.txt +``` + +Edit only the copies under `deployment/settings/`, replace every `CHANGE_ME`, +and keep their mode at `0600`. + ## Docker deployment Both engines use the same lifecycle and Compose files. Do not invoke Compose @@ -180,7 +193,7 @@ Docker: ```bash bash container_install.sh --action install --engine docker \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Podman: @@ -188,7 +201,7 @@ Podman: ```bash bash container_install.sh --action install --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` The installer creates `.env.production.file`; use it for later direct Compose @@ -250,7 +263,7 @@ notes: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Replace `podman` with `docker` for a Docker-managed deployment. Stop on build, @@ -287,11 +300,11 @@ after the application developer documents and tests those integrations. ```bash # Stage application files and dependencies. bash install.sh --stage install --git_revision current \ - --conf conf/docker_production_settings.txt + --conf deployment/settings/app_production_settings.txt # Bootstrap the prepared runtime (settings, migrations and static files). bash install.sh --bootstrap install \ - --conf conf/docker_production_settings.txt + --conf deployment/settings/app_production_settings.txt ``` For upgrades, take a backup and replace both `install` actions with `upgrade`. @@ -378,7 +391,7 @@ settings files, and backup identifiers. ```bash BACKUP_DIR="/srv/containers/backup/relecov-iskylims/$(date +%Y%m%d_%H%M%S)" -SETTINGS_FILE='/protected/app_production_settings.txt' +SETTINGS_FILE='deployment/settings/app_production_settings.txt' DOCUMENTS_VOLUME='CHANGE_ME' DB_HOST='CHANGE_ME' DB_PORT='3306' @@ -424,7 +437,7 @@ Compatible application-only rollback: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Full restore when schema or persistent-file formats are incompatible: @@ -442,7 +455,7 @@ mysql --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" --password \ podman volume import "$DOCUMENTS_VOLUME" "$BACKUP_DIR/documents.tar" tar -C /srv/containers/bind -xzf "$BACKUP_DIR/bind-mounts.tar.gz" bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Then deploy the revision recorded in `git-revision.txt`, start the deployment, @@ -459,7 +472,7 @@ volume at `/data` and extracting `/backup/documents.tar` there. ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` 5. Do not fake migrations, delete volumes, or rebuild from an unrecorded @@ -525,7 +538,7 @@ be replaced, move it to a timestamped backup instead of deleting evidence: sudo mv /var/log/local/relecov-iskylims/apache/modsec_debug.log \ /var/log/local/relecov-iskylims/apache/modsec_debug.log.blocked bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,/protected/app_production_settings.txt --install_conf_map apache,/protected/apache_production_settings.txt --install_conf_map samba,/protected/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt podman compose --env-file .env.production.file -f docker-compose.prod.yml restart apache ``` From b1dc6ad830989242f5de0ccb7e31da8c62d251af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 18:27:29 +0200 Subject: [PATCH 26/55] docs: add iSkyLIMS bare-metal deployment commands --- .bu-isciii-deployment/state.json | 4 ++-- LEAME.md | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 9d46f22f..02fff2ff 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -34,8 +34,8 @@ ".dockerignore": "a76e2a974786736d1d3744ed534ae9996514cb15d921328450f3757fafe273d2", ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", "Dockerfile": "1a3f8ccccfd84f0a59efda8446f53954fade8b200738a630420564eb8c6e7d5f", - "LEAME.md": "299a140e487d7bd983a0553946135c605cd134e69b05ee00504a0b6efd46c772", - "README.md": "72d509f37059e2ecd39791b3b430fadded441fa928b0a06997a44680e5f4a551", + "LEAME.md": "1eb1112dd4519e4c513d83cbf9a4a79674bdf1daf558dc82fad6337050061b28", + "README.md": "3a3c0a07653264d1537347a000a4044494ad45dcf29b43c80fc46f8edf9908c3", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "7d22c5d707efe6190d799fd75d08114dd67ef9077548cd34d5df1f174bd45677", diff --git a/LEAME.md b/LEAME.md index 31a6b26b..7c29c10c 100644 --- a/LEAME.md +++ b/LEAME.md @@ -138,6 +138,18 @@ install -m 0600 conf/samba/samba_production_settings.txt deployment/settings/sam Editar unicamente las copias bajo `deployment/settings/`. Los comandos de instalacion y actualizacion usan estas rutas protegidas. +Para una instalacion bare-metal de la aplicacion principal: + +```bash +bash install.sh --stage install --git_revision \ + --conf deployment/settings/app_production_settings.txt +bash install.sh --bootstrap install \ + --conf deployment/settings/app_production_settings.txt +``` + +En una actualizacion bare-metal, realizar primero el backup y sustituir +`install` por `upgrade` en ambos comandos. + Valores que requieren decision del responsable de la aplicacion: - hostnames publicos, TLS y proxy; From df6b11c1203c8015bb1ab4f62af30df455cd1113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 18:29:35 +0200 Subject: [PATCH 27/55] docs: keep iSkyLIMS runbook container-focused --- .bu-isciii-deployment/state.json | 2 +- LEAME.md | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 02fff2ff..668ec5b5 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -34,7 +34,7 @@ ".dockerignore": "a76e2a974786736d1d3744ed534ae9996514cb15d921328450f3757fafe273d2", ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", "Dockerfile": "1a3f8ccccfd84f0a59efda8446f53954fade8b200738a630420564eb8c6e7d5f", - "LEAME.md": "1eb1112dd4519e4c513d83cbf9a4a79674bdf1daf558dc82fad6337050061b28", + "LEAME.md": "299a140e487d7bd983a0553946135c605cd134e69b05ee00504a0b6efd46c772", "README.md": "3a3c0a07653264d1537347a000a4044494ad45dcf29b43c80fc46f8edf9908c3", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", diff --git a/LEAME.md b/LEAME.md index 7c29c10c..31a6b26b 100644 --- a/LEAME.md +++ b/LEAME.md @@ -138,18 +138,6 @@ install -m 0600 conf/samba/samba_production_settings.txt deployment/settings/sam Editar unicamente las copias bajo `deployment/settings/`. Los comandos de instalacion y actualizacion usan estas rutas protegidas. -Para una instalacion bare-metal de la aplicacion principal: - -```bash -bash install.sh --stage install --git_revision \ - --conf deployment/settings/app_production_settings.txt -bash install.sh --bootstrap install \ - --conf deployment/settings/app_production_settings.txt -``` - -En una actualizacion bare-metal, realizar primero el backup y sustituir -`install` por `upgrade` en ambos comandos. - Valores que requieren decision del responsable de la aplicacion: - hostnames publicos, TLS y proxy; From b91d591d482c115f3bbc3c610d4ba84ad6c73bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 19:17:32 +0200 Subject: [PATCH 28/55] fix: use mysqlclient during iSkyLIMS bootstrap --- install.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/install.sh b/install.sh index 9e025646..85a25784 100755 --- a/install.sh +++ b/install.sh @@ -158,8 +158,8 @@ check_required_modules() { } check_database() { - # The MySQL client is used by RELECOV/iSkyLIMS; PyMySQL fallback supports - # minimal Patho Core-style containers without the client package. + # Prefer the MySQL CLI when available; container images use mysqlclient's + # MySQLdb module from the application virtual environment. if command -v mysql >/dev/null 2>&1; then MYSQL_PWD="$DB_PASSWORD" mysql --host="$DB_HOST" --port="$DB_PORT" \ --user="$DB_USER" --database="$DB_NAME" --execute='SELECT 1' >/dev/null \ @@ -168,9 +168,9 @@ check_database() { fi "$INSTALL_PATH/virtualenv/bin/python" - "$DB_HOST" "$DB_PORT" "$DB_USER" "$DB_PASSWORD" "$DB_NAME" <<'PY' import sys -import pymysql -connection = pymysql.connect(host=sys.argv[1], port=int(sys.argv[2]), - user=sys.argv[3], password=sys.argv[4], database=sys.argv[5]) +import MySQLdb +connection = MySQLdb.connect(host=sys.argv[1], port=int(sys.argv[2]), + user=sys.argv[3], passwd=sys.argv[4], db=sys.argv[5]) connection.close() PY } From a5809f50e4408185781038486d202a91eea36b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 11 Aug 2026 19:17:32 +0200 Subject: [PATCH 29/55] fix: avoid occupied iSkyLIMS test port --- conf/docker_test_settings.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index 4976417a..25ce3fa9 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -15,7 +15,7 @@ MIGRATION_MODULES='core drylab wetlab django_utils' APP_UID='1212' APP_GID='1212' APP_SHELL='/bin/bash' -APP_PORT='8001' +APP_PORT='8201' ### Disposable persistence paths HOST_LOG_PATH='/tmp/relecov-iskylims/logs' @@ -34,7 +34,7 @@ DB_ROOT_PASSWORD='root' DJANGO_DEBUG='true' DJANGO_SECRET_KEY='test-only-change-me' DJANGO_ALLOWED_HOSTS='*' -DJANGO_CSRF_TRUSTED_ORIGINS='http://localhost:8001,http://127.0.0.1:8001,http://0.0.0.0:8001' +DJANGO_CSRF_TRUSTED_ORIGINS='http://localhost:8201,http://127.0.0.1:8201,http://0.0.0.0:8201' DB_CONN_MAX_AGE='60' ### Local email defaults From 1d4932a77b2aa35ee58620287c13e028e3a48fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Wed, 12 Aug 2026 09:35:01 +0200 Subject: [PATCH 30/55] fix: name iSkyLIMS test database service explicitly --- conf/docker_test_settings.txt | 2 +- docker-compose.test.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index 25ce3fa9..ed7df81c 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -23,7 +23,7 @@ HOST_LOG_PATH='/tmp/relecov-iskylims/logs' DJANGO_SETTINGS_PATH='/tmp/relecov-iskylims/data/settings/iskylims/settings.py' ### Compose test database -DB_HOST='app_db' +DB_HOST='iskylims_app_db' DB_PORT='3306' DB_NAME='iskylims' DB_USER='django' diff --git a/docker-compose.test.yml b/docker-compose.test.yml index fce8e72d..3baf9e5b 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -24,7 +24,7 @@ services: APP_INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} APP_PORT: ${APP_APP_PORT:?APP_APP_PORT is required} PROJECT_MODULE: "iskylims" - DB_HOST: app_db + DB_HOST: iskylims_app_db DB_PORT: ${APP_DB_PORT:?APP_DB_PORT is required} DB_NAME: ${APP_DB_NAME:?APP_DB_NAME is required} DB_USER: ${APP_DB_USER:?APP_DB_USER is required} @@ -32,7 +32,7 @@ services: DJANGO_SECRET_KEY: ${APP_DJANGO_SECRET_KEY:?APP_DJANGO_SECRET_KEY is required} DJANGO_ALLOWED_HOSTS: ${APP_DJANGO_ALLOWED_HOSTS:?APP_DJANGO_ALLOWED_HOSTS is required} depends_on: - app_db: + iskylims_app_db: condition: service_healthy volumes: - app_test_documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:z @@ -83,7 +83,7 @@ services: - samba_test_data:/mnt:z networks: [deployment_net] # END BU-ISCIII ADDON: samba - app_db: + iskylims_app_db: image: docker.io/library/mysql:8.0 environment: MYSQL_DATABASE: ${APP_DB_NAME:?APP_DB_NAME is required} From 3ec307abe9c53f75e1ef8f1f312550324d4b1e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Wed, 12 Aug 2026 10:11:10 +0200 Subject: [PATCH 31/55] fix: expose iSkyLIMS virtualenv to health checks --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 32e7cf58..8395946a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ ARG RENDER_DJANGO_SETTINGS=false ENV APP_REPO_PATH=${APP_REPO_PATH} \ APP_INSTALL_PATH=${APP_INSTALL_PATH} \ INSTALL_PATH=${APP_INSTALL_PATH} \ + PATH=${APP_INSTALL_PATH}/virtualenv/bin:${PATH} \ APP_PORT=${APP_PORT} \ PROJECT_MODULE=iskylims \ PYTHONDONTWRITEBYTECODE=1 \ From 79a4fa99c64e8d2e9522fd63118d2428b65e74ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Wed, 12 Aug 2026 10:11:10 +0200 Subject: [PATCH 32/55] fix: validate the iSkyLIMS health route --- install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.sh b/install.sh index 85a25784..b190758a 100755 --- a/install.sh +++ b/install.sh @@ -152,6 +152,9 @@ check_required_modules() { local module [[ -f "$install_script_dir/conf/urls.py" ]] \ || die "Django URL configuration is missing: conf/urls.py" + grep -Eq 'include\(["'"']deployment_health\.urls["'"']\)' \ + "$install_script_dir/conf/urls.py" \ + || die "conf/urls.py must include deployment_health.urls for the /health/ endpoint" for module in $REQUIRED_MODULES; do [[ -e "$install_script_dir/$module" ]] || die "Required application module is missing: $module" done From 6b7a858b1e42ca8d07f5802a3780843e0ddac040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Wed, 12 Aug 2026 10:15:01 +0200 Subject: [PATCH 33/55] fix: make iSkyLIMS health validation shell-safe --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index b190758a..eb28c36a 100755 --- a/install.sh +++ b/install.sh @@ -152,7 +152,7 @@ check_required_modules() { local module [[ -f "$install_script_dir/conf/urls.py" ]] \ || die "Django URL configuration is missing: conf/urls.py" - grep -Eq 'include\(["'"']deployment_health\.urls["'"']\)' \ + grep -Fq 'deployment_health.urls' \ "$install_script_dir/conf/urls.py" \ || die "conf/urls.py must include deployment_health.urls for the /health/ endpoint" for module in $REQUIRED_MODULES; do From 75ff77f3737d60f12862ff6928617808c9dea3e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Thu, 13 Aug 2026 11:49:03 +0200 Subject: [PATCH 34/55] Fix production setup documentation order --- .bu-isciii-deployment/state.json | 10 +++++----- LEAME.md | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 668ec5b5..1ea2478a 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -33,8 +33,8 @@ "files": { ".dockerignore": "a76e2a974786736d1d3744ed534ae9996514cb15d921328450f3757fafe273d2", ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", - "Dockerfile": "1a3f8ccccfd84f0a59efda8446f53954fade8b200738a630420564eb8c6e7d5f", - "LEAME.md": "299a140e487d7bd983a0553946135c605cd134e69b05ee00504a0b6efd46c772", + "Dockerfile": "57f0682100debbf2e3268c9d6d7540a6bd3e20cf575931c2cebd054d4bc7e935", + "LEAME.md": "b68d7ad9b325f51bb9ac52115f592337df52b1a5ab13a5f6831cf6b0dcfc9860", "README.md": "3a3c0a07653264d1537347a000a4044494ad45dcf29b43c80fc46f8edf9908c3", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", @@ -43,7 +43,7 @@ "conf/apache/apache_production_settings.txt": "a48153d241283e28d27f4eee1d4bd1f2daf0880c0b3c24cf2c27d4d7526eaff0", "conf/apache/apache_test_settings.txt": "878b30eba2244891abadfac957a503be71fcc454549fd27afa83f4f2ea09371a", "conf/docker_production_settings.txt": "bc697e64ba67fe81eb389adf93bf8a5ad5ecbe857516c08f6f5e2f2ee4d75bf6", - "conf/docker_test_settings.txt": "f7a64f7a9c3e06e3845d7b8b424c8b520a6b20f0b24bb6dd5ee8a15492cbcb5a", + "conf/docker_test_settings.txt": "31c288df87d48c93985fd7f8b289e0888838845a6f2885ff9088de3e14d8fb2b", "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", "conf/template_settings.py": "590369c8b3db2e18b35deb04b2a4abf05e60f65d41ba0983b3b4af8ef20e5039", @@ -53,8 +53,8 @@ "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", "docker-compose.prod.yml": "9feb8d66df5aee56822fbec2fa190ad151d17c3b838623211a41d42fb5480f3f", - "docker-compose.test.yml": "ad5b5f540757859812160d03b4fb05c1b2ab208cb9fa6f359a3ffb12ec6e2973", - "install.sh": "6258861f0fd5e2423d37f573dad725c87a29e6cdfd44c061c258c96094769760", + "docker-compose.test.yml": "7abc7acd601a7602590161f4d05436dfcaaafb9455147941cb3c5b3a220038bf", + "install.sh": "93fccb73e9f07beb68fd4a2eb398ba88479680bc46f5946557abe32b2d2b9a87", "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, diff --git a/LEAME.md b/LEAME.md index 31a6b26b..f7d5110d 100644 --- a/LEAME.md +++ b/LEAME.md @@ -98,14 +98,6 @@ sudo chown -R : \ /var/log/local/relecov-iskylims ``` -Aplicar despues UID/GID internos, modos y etiquetas SELinux mediante el -instalador. No modificar el arbol `/srv/containers/storage/` manualmente. - -```bash -bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt -``` - ## Actualizar codigo Para un checkout nuevo: @@ -146,6 +138,16 @@ Valores que requieren decision del responsable de la aplicacion: - correo, identidad, almacenamiento y ajustes propios de la aplicacion; - administrador inicial y transferencia segura de sus credenciales. +Solo despues de crear y completar todos los ficheros bajo +`deployment/settings/`, aplicar UID/GID internos, modos y etiquetas SELinux +mediante el instalador. No modificar el arbol `/srv/containers/storage/` +manualmente. + +```bash +bash container_install.sh --action fix-permissions --engine podman \ + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt +``` + ## Backup antes de actualizar Crear un directorio identificado y registrar el estado desplegado: From 2fc406eebbf96532e2574d17b659c4464cb7d103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Thu, 13 Aug 2026 12:35:09 +0200 Subject: [PATCH 35/55] Synchronize deployment scaffold state --- .bu-isciii-deployment/state.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 1ea2478a..41381f24 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -42,11 +42,11 @@ "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", "conf/apache/apache_production_settings.txt": "a48153d241283e28d27f4eee1d4bd1f2daf0880c0b3c24cf2c27d4d7526eaff0", "conf/apache/apache_test_settings.txt": "878b30eba2244891abadfac957a503be71fcc454549fd27afa83f4f2ea09371a", - "conf/docker_production_settings.txt": "bc697e64ba67fe81eb389adf93bf8a5ad5ecbe857516c08f6f5e2f2ee4d75bf6", - "conf/docker_test_settings.txt": "31c288df87d48c93985fd7f8b289e0888838845a6f2885ff9088de3e14d8fb2b", + "conf/docker_production_settings.txt": "e98f367b23d1b5cfbf7835d473ea15f55ceb36fc49467aa03b08409b8e7a1ec6", + "conf/docker_test_settings.txt": "9dec2eb2d6faa05130f567049e558aafd7408b2c68b36f0af1517787a657262a", "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", - "conf/template_settings.py": "590369c8b3db2e18b35deb04b2a4abf05e60f65d41ba0983b3b4af8ef20e5039", + "conf/template_settings.py": "ed15033d4c3ccd3de585cea8679976b1c108f1ecfd4453cb77d5af606c984b3a", "container_install.sh": "62a65dcdcf9b8c58c2cdbbe072e3deb236098e734c20aaf1971e2cbde7ac3188", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", From 08fbe8285ec944042d266aacb061db2745aa9bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Thu, 13 Aug 2026 12:42:58 +0200 Subject: [PATCH 36/55] Synchronize production demo data support --- .bu-isciii-deployment/state.json | 4 ++-- README.md | 3 +++ container_install.sh | 18 ++++++++++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 41381f24..e2db67ea 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -35,7 +35,7 @@ ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", "Dockerfile": "57f0682100debbf2e3268c9d6d7540a6bd3e20cf575931c2cebd054d4bc7e935", "LEAME.md": "b68d7ad9b325f51bb9ac52115f592337df52b1a5ab13a5f6831cf6b0dcfc9860", - "README.md": "3a3c0a07653264d1537347a000a4044494ad45dcf29b43c80fc46f8edf9908c3", + "README.md": "0bbcf653bc69d014ddb8888a4ddc42c35c88cb94b3da67f0d521b928dda1d283", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "7d22c5d707efe6190d799fd75d08114dd67ef9077548cd34d5df1f174bd45677", @@ -47,7 +47,7 @@ "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", "conf/template_settings.py": "ed15033d4c3ccd3de585cea8679976b1c108f1ecfd4453cb77d5af606c984b3a", - "container_install.sh": "62a65dcdcf9b8c58c2cdbbe072e3deb236098e734c20aaf1971e2cbde7ac3188", + "container_install.sh": "35f02681397cf10d42f7b4e5e1c2b8a4e65d2d518db7eb2ba3b62230ce203938", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", diff --git a/README.md b/README.md index d5eb82aa..09a61699 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,9 @@ bash container_install.sh --test --action install --engine docker \ standard interface. A project that supplies fixtures or demo files must set `application_supports_test_data=true` and implement `load_test_deployment_data` in its wrapper; otherwise `--demo_data` is rejected explicitly. +The same explicit `--demo_data ` input may be used on a fresh production +install when an application needs a controlled seed import. Production never +selects or loads demo data by default, and upgrades never reload it. For an automatic first administrator, set `CREATE_INITIAL_SUPERUSER=true` and the `DJANGO_SUPERUSER_*` values in the selected test settings before install. diff --git a/container_install.sh b/container_install.sh index d05da234..0c7e6c3c 100755 --- a/container_install.sh +++ b/container_install.sh @@ -341,7 +341,7 @@ Options: --script_before --script_after --script - --demo_data + --demo_data Import application demo data on install. --skip_demo_data --skip_test_data --help @@ -377,10 +377,18 @@ done if [ -n "$demo_data" ] && [ "$application_supports_test_data" != true ]; then die "--demo_data is not implemented for $APPLICATION_NAME" fi +if [ -n "$demo_data" ]; then + [ -f "$demo_data" ] || die "Demo-data file not found: $demo_data" + demo_data="$(cd "$(dirname "$demo_data")" && pwd)/$(basename "$demo_data")" +fi if [ "$mode" = test ] && [ "$action" = install ] \ && [ "$application_supports_test_data" = true ]; then skip_demo_data="${skip_demo_data:-false}" skip_test_data="${skip_test_data:-false}" +elif [ "$action" = install ] && [ -n "$demo_data" ] \ + && [ "$application_supports_test_data" = true ]; then + skip_demo_data="${skip_demo_data:-false}" + skip_test_data=true else skip_demo_data=true skip_test_data=true @@ -499,9 +507,11 @@ for service_name in "${install_services[@]}"; do bootstrap_service "$service_name" "$container_id" "$action" || die "$service_name bootstrap failed" done -# 11. Load application-owned fixtures/demo files only for a fresh test install. -if [ "$mode" = test ] && [ "$action" = install ] \ - && [ "$application_supports_test_data" = true ]; then +# 11. Load application-owned data for a fresh test install, or for an explicit +# production --demo_data request. Production never imports data implicitly. +if [ "$action" = install ] \ + && [ "$application_supports_test_data" = true ] \ + && { [ "$mode" = test ] || [ -n "$demo_data" ]; }; then load_test_deployment_data fi From 31bfa000477343788f9920fa5b456f032b6c735b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Thu, 13 Aug 2026 12:56:27 +0200 Subject: [PATCH 37/55] Fix Django project scaffolding module conflict --- .bu-isciii-deployment/state.json | 2 +- install.sh | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index e2db67ea..9c7f99e1 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -54,7 +54,7 @@ "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", "docker-compose.prod.yml": "9feb8d66df5aee56822fbec2fa190ad151d17c3b838623211a41d42fb5480f3f", "docker-compose.test.yml": "7abc7acd601a7602590161f4d05436dfcaaafb9455147941cb3c5b3a220038bf", - "install.sh": "93fccb73e9f07beb68fd4a2eb398ba88479680bc46f5946557abe32b2d2b9a87", + "install.sh": "00fe2b02c2de82474af5fd449f1c36424dea23550012073fa644e7f3c61581e6", "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, diff --git a/install.sh b/install.sh index eb28c36a..7227f6aa 100755 --- a/install.sh +++ b/install.sh @@ -437,8 +437,13 @@ stage_application_files() { mkdir -p "$INSTALL_PATH/logs" "$INSTALL_PATH/documents" \ "$INSTALL_PATH/static" "$INSTALL_PATH/cron" "$INSTALL_PATH/tmp" prepare_application_directories "$INSTALL_PATH" - "$INSTALL_PATH/virtualenv/bin/python" -m django startproject \ - "$PROJECT_MODULE" "$INSTALL_PATH" + # Run from the clean staged tree so a source directory such as conf/ cannot + # be mistaken for an importable module that conflicts with PROJECT_MODULE. + ( + cd "$INSTALL_PATH" + PYTHONPATH= "$INSTALL_PATH/virtualenv/bin/python" -m django startproject \ + "$PROJECT_MODULE" . + ) install -m 0644 "$install_script_dir/conf/urls.py" \ "$INSTALL_PATH/$PROJECT_MODULE/urls.py" if [[ -f "$install_script_dir/conf/routing.py" ]]; then From 1f4d9af7a4929ef1db3b710f8f08f2ddf6434006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Thu, 13 Aug 2026 14:48:03 +0200 Subject: [PATCH 38/55] Show service ports after container install --- .bu-isciii-deployment/state.json | 2 +- container_install.sh | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 9c7f99e1..bbfde7da 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -47,7 +47,7 @@ "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", "conf/template_settings.py": "ed15033d4c3ccd3de585cea8679976b1c108f1ecfd4453cb77d5af606c984b3a", - "container_install.sh": "35f02681397cf10d42f7b4e5e1c2b8a4e65d2d518db7eb2ba3b62230ce203938", + "container_install.sh": "dc8ab9b25ade9fb255dba1d323959e988cf9252240f5c9f8664960e09ac2ee0b", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", diff --git a/container_install.sh b/container_install.sh index 0c7e6c3c..574bba92 100755 --- a/container_install.sh +++ b/container_install.sh @@ -118,6 +118,12 @@ current_service_container() { resolve_service_container "$1" } +print_service_summary() { + echo + echo "Running services and published ports:" + deployment_compose -f "$compose_file" ps +} + # Each Django service renders its own protected host settings bind. React # services and add-ons have no Django settings source. prepare_application_host_sources() { @@ -520,3 +526,4 @@ smoke_args=(--engine "$engine" --compose_file "$compose_file" --env_file "$compo [ "$mode" = test ] && smoke_args+=(--test) bash "$script_dir/scripts/smoke_test.sh" "${smoke_args[@]}" echo "$action completed successfully for $APPLICATION_NAME." +print_service_summary From 781e5c16ef61c696ce2d161d1d08cc1d6043bbbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Fri, 14 Aug 2026 11:45:34 +0200 Subject: [PATCH 39/55] linting --- conf/template_settings.py | 4 +--- deployment_health/urls.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/conf/template_settings.py b/conf/template_settings.py index 4b389fda..0705fc6a 100644 --- a/conf/template_settings.py +++ b/conf/template_settings.py @@ -17,9 +17,7 @@ host.strip() for host in "djangoallowedhosts".split(",") if host.strip() ] CSRF_TRUSTED_ORIGINS = [ - origin.strip() - for origin in "djangocsrftrustedorigins".split(",") - if origin.strip() + origin.strip() for origin in "djangocsrftrustedorigins".split(",") if origin.strip() ] # iSkyLIMS local applications. Add new applications here when their models, diff --git a/deployment_health/urls.py b/deployment_health/urls.py index b8cf7974..c62284ef 100644 --- a/deployment_health/urls.py +++ b/deployment_health/urls.py @@ -4,7 +4,6 @@ from .views import health_check - urlpatterns = [ path("", health_check, name="deployment-health"), ] From e2304e288f9687d8ca7a832286d4cf704ae9f98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 08:52:52 +0200 Subject: [PATCH 40/55] Sync deployment standard log policy --- .bu-isciii-deployment/state.json | 14 +++++----- LEAME.md | 14 +++------- conf/apache/01-reverse-proxy.conf | 31 ++++++++++++++++++++++ conf/apache/apache_production_settings.txt | 29 ++++++++++++++++++-- conf/apache/apache_test_settings.txt | 11 ++++++++ 5 files changed, 79 insertions(+), 20 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index bbfde7da..d4ce17a7 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -34,27 +34,27 @@ ".dockerignore": "a76e2a974786736d1d3744ed534ae9996514cb15d921328450f3757fafe273d2", ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", "Dockerfile": "57f0682100debbf2e3268c9d6d7540a6bd3e20cf575931c2cebd054d4bc7e935", - "LEAME.md": "b68d7ad9b325f51bb9ac52115f592337df52b1a5ab13a5f6831cf6b0dcfc9860", + "LEAME.md": "a9348c581cb50e5e55961c919b454abc536820a9f8a933e4d64601a14c06eb5e", "README.md": "0bbcf653bc69d014ddb8888a4ddc42c35c88cb94b3da67f0d521b928dda1d283", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", - "conf/apache/01-reverse-proxy.conf": "7d22c5d707efe6190d799fd75d08114dd67ef9077548cd34d5df1f174bd45677", + "conf/apache/01-reverse-proxy.conf": "d5a0b6143fc4b0ba87f1f7f827e0d6c84dda1857e9697cd6acba4344beae375e", "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", - "conf/apache/apache_production_settings.txt": "a48153d241283e28d27f4eee1d4bd1f2daf0880c0b3c24cf2c27d4d7526eaff0", - "conf/apache/apache_test_settings.txt": "878b30eba2244891abadfac957a503be71fcc454549fd27afa83f4f2ea09371a", - "conf/docker_production_settings.txt": "e98f367b23d1b5cfbf7835d473ea15f55ceb36fc49467aa03b08409b8e7a1ec6", + "conf/apache/apache_production_settings.txt": "b070f6c82138f0772e52460e24868e4b48d38d412c9f78900a45e9bb00216506", + "conf/apache/apache_test_settings.txt": "102c1b747def3ce56c5b977829eb07dddbca91c8c0012e36a130357851846554", + "conf/docker_production_settings.txt": "ba4059934c970cf4d334348203e3241121cd745b3cff0d6a4d78a01965e61c51", "conf/docker_test_settings.txt": "9dec2eb2d6faa05130f567049e558aafd7408b2c68b36f0af1517787a657262a", "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", "conf/template_settings.py": "ed15033d4c3ccd3de585cea8679976b1c108f1ecfd4453cb77d5af606c984b3a", - "container_install.sh": "dc8ab9b25ade9fb255dba1d323959e988cf9252240f5c9f8664960e09ac2ee0b", + "container_install.sh": "3ea6dd7e1f4355c8c6e92c65d5d1e1801d8d9b3f2dfc485935f26e381efdd1bf", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", "docker-compose.prod.yml": "9feb8d66df5aee56822fbec2fa190ad151d17c3b838623211a41d42fb5480f3f", "docker-compose.test.yml": "7abc7acd601a7602590161f4d05436dfcaaafb9455147941cb3c5b3a220038bf", - "install.sh": "00fe2b02c2de82474af5fd449f1c36424dea23550012073fa644e7f3c61581e6", + "install.sh": "cbed1598382c3c15327c09f4ad71ee254b7596904c6a95b1dd8cc47bc2d77f84", "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, diff --git a/LEAME.md b/LEAME.md index f7d5110d..72f59392 100644 --- a/LEAME.md +++ b/LEAME.md @@ -83,7 +83,7 @@ Persistencia declarada por el despliegue: ## Preparar directorios del host Crear la estructura comun antes de la primera instalacion. Sustituir -`` por la cuenta que ejecutara siempre Podman y el instalador. +`` por la cuenta que ejecutara siempre Podman y el instalador. Es decir, normalmente la cuenta con la que estás conectado a la máquina virtual. ```bash sudo mkdir -p /opt/containers_apps/relecov-iskylims @@ -197,20 +197,12 @@ de restauracion. ## Ejecutar la actualizacion -Primera instalacion: - -```bash -bash container_install.sh --action install --engine podman \ - --git_revision \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt -``` - -Actualizacion: +Ejecutar el comando de instalación/upgrade: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt 2>&1 | tee "${date +%Y%m%d_%H%M%S}_prod_install.log" ``` Durante `--action upgrade`, `container_install.sh`: diff --git a/conf/apache/01-reverse-proxy.conf b/conf/apache/01-reverse-proxy.conf index 4fa9083c..2e4883ac 100644 --- a/conf/apache/01-reverse-proxy.conf +++ b/conf/apache/01-reverse-proxy.conf @@ -71,3 +71,34 @@ CustomLog logs/${APACHE_LOG_STEM}-apache.access.log proxy env=forwarded ErrorLog logs/${APACHE_LOG_STEM}-apache.error.log + +# OPTIONAL SECOND DNS VIRTUAL HOST +# Copy this commented example into an application's source configuration and +# replace every angle-bracket value. Give it the same forwarded-header, +# request-limit, timeout, and per-host logging contract as the default vhost. +# +# +# ServerName +# +# ProxyRequests Off +# ProxyPreserveHost On +# AllowEncodedSlashes NoDecode +# RequestHeader set X-Forwarded-Proto "" +# RequestHeader set X-Forwarded-Port "" +# RequestHeader set X-Forwarded-Host "" +# +# LimitRequestBody +# +# SecRequestBodyLimit +# SecRequestBodyNoFilesLimit +# +# +# ProxyPass / http://:/ +# ProxyPassReverse / http://:/ +# ProxyTimeout +# TimeOut +# +# CustomLog logs/-apache.access.log combined env=!forwarded +# CustomLog logs/-apache.access.log proxy env=forwarded +# ErrorLog logs/-apache.error.log +# diff --git a/conf/apache/apache_production_settings.txt b/conf/apache/apache_production_settings.txt index d267f537..bfd542ff 100644 --- a/conf/apache/apache_production_settings.txt +++ b/conf/apache/apache_production_settings.txt @@ -9,44 +9,69 @@ APACHE_CONF_PATH='' # Persistent host directory mounted as the Apache log destination. +# Example: /var/log/local/relecov-iskylims/apache APACHE_LOG_PATH='/var/log/local/relecov-iskylims/apache' # Public host interface and port. Use 127.0.0.1 when another host reverse proxy # is the only component that should reach this Apache container. +# Examples: APACHE_BIND_HOST='0.0.0.0', APACHE_PORT='8080'. APACHE_BIND_HOST='0.0.0.0' APACHE_PORT='8080' # Public host name handled by the default VirtualHost. Use localhost for a # workstation deployment or the externally visible DNS name in production. +# Example: api.example.org APACHE_SERVER_NAME='CHANGE_ME_DNS_NAME' # Compose service receiving proxied requests. Leave empty to use the service # selected by ADDONS.apache.CONFIG_SERVICE (normally app for a single service). +# Example override: app APACHE_UPSTREAM_SERVICE='' # Internal application port used by ProxyPass. Leave empty to use APP_PORT from -# the selected service's production settings. +# the selected service's production settings. Example override: 8001 APACHE_UPSTREAM_PORT='' # Maximum proxy request duration in seconds. Leave empty to use the selected # service's GUNICORN_TIMEOUT, falling back to 120 seconds when it is undefined. +# Example override: 300 APACHE_PROXY_TIMEOUT='' # Prefix for the generated -apache.access.log and # -apache.error.log files. Leave empty to derive it from # APACHE_SERVER_NAME, with URL schemes, paths and ports removed. +# Example override: mepram-api APACHE_LOG_STEM='' # Server-status stays restricted by default. Use a local-only diagnostic name; # aliases and allow-list accept Apache-compatible values. +# Example diagnostic host: localhost SERVER_STATUS_SERVER_NAME='localhost' +# Space-separated aliases. Example: 127.0.0.1 localhost SERVER_STATUS_ALIASES='127.0.0.1 localhost' +# Apache Require ip/host values. Example: 127.0.0.1 localhost SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' -# Forwarded headers received by Django behind the production proxy. +# Forwarded scheme received by Django. Example: https APACHE_FORWARDED_PROTO='https' +# Forwarded public port received by Django. Example: 443 APACHE_FORWARDED_PORT='443' # 50 MiB. The reverse-proxy template applies this value to Apache and, when # loaded, ModSecurity. Increase it only for endpoints that require larger data. +# Example: 52428800 (50 MiB) APACHE_LIMIT_REQUEST_BODY='52428800' + +# Optional additional VirtualHosts are application-owned and are not generated +# automatically when another add-on (for example Keycloak) is selected. For +# every enabled extra vhost, define one uniquely prefixed set matching every +# variable referenced by its block in conf/apache/01-reverse-proxy.conf. +# Duplicate and rename this complete example for each additional public DNS: +# APACHE_SECOND_SERVER_NAME='CHANGE_ME_SECOND_DNS_NAME' +# APACHE_SECOND_UPSTREAM_SERVICE='CHANGE_ME_COMPOSE_SERVICE' +# APACHE_SECOND_UPSTREAM_PORT='CHANGE_ME_INTERNAL_PORT' +# APACHE_SECOND_PROXY_TIMEOUT='120' +# APACHE_SECOND_LOG_STEM='CHANGE_ME_LOG_STEM' +# APACHE_SECOND_FORWARDED_PROTO='https' +# APACHE_SECOND_FORWARDED_PORT='443' +# APACHE_SECOND_LIMIT_REQUEST_BODY='52428800' diff --git a/conf/apache/apache_test_settings.txt b/conf/apache/apache_test_settings.txt index 615c3f6a..948160be 100644 --- a/conf/apache/apache_test_settings.txt +++ b/conf/apache/apache_test_settings.txt @@ -39,3 +39,14 @@ SERVER_STATUS_ALLOW_FROM='127.0.0.1 localhost' APACHE_FORWARDED_PROTO='http' APACHE_FORWARDED_PORT='8081' APACHE_LIMIT_REQUEST_BODY='52428800' + +# Extra test VirtualHosts follow the same explicit one-variable-set-per-vhost +# contract as production; selecting another add-on does not create these. +# APACHE_SECOND_SERVER_NAME='second.localhost' +# APACHE_SECOND_UPSTREAM_SERVICE='CHANGE_ME_COMPOSE_SERVICE' +# APACHE_SECOND_UPSTREAM_PORT='CHANGE_ME_INTERNAL_PORT' +# APACHE_SECOND_PROXY_TIMEOUT='120' +# APACHE_SECOND_LOG_STEM='second' +# APACHE_SECOND_FORWARDED_PROTO='http' +# APACHE_SECOND_FORWARDED_PORT='8081' +# APACHE_SECOND_LIMIT_REQUEST_BODY='52428800' From cff49e6918d8059f5c813a12a11c1e1412008508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 09:09:05 +0200 Subject: [PATCH 41/55] Validate committed Django migrations --- .bu-isciii-deployment/state.json | 2 +- install.sh | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index d4ce17a7..da770bee 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -54,7 +54,7 @@ "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", "docker-compose.prod.yml": "9feb8d66df5aee56822fbec2fa190ad151d17c3b838623211a41d42fb5480f3f", "docker-compose.test.yml": "7abc7acd601a7602590161f4d05436dfcaaafb9455147941cb3c5b3a220038bf", - "install.sh": "cbed1598382c3c15327c09f4ad71ee254b7596904c6a95b1dd8cc47bc2d77f84", + "install.sh": "025b475c3dd6f9da608d1576a1de7e0826d8a084b0456d4a65f7927248c6b09e", "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, diff --git a/install.sh b/install.sh index 7227f6aa..21e9aed1 100755 --- a/install.sh +++ b/install.sh @@ -470,6 +470,13 @@ run_hook() { python "${args[@]}" } +check_for_missing_migrations() { + # Deployment must never invent schema history. Fail when model changes need + # migration files that have not been generated and committed by developers. + python manage.py makemigrations --check --dry-run --noinput \ + || die "Model changes detected without committed Django migrations" +} + bootstrap_application() { [[ -f "$INSTALL_PATH/manage.py" ]] || die "manage.py not found; run --stage first" [[ -x "$INSTALL_PATH/virtualenv/bin/python" ]] || die "virtualenv not found; run --stage first" @@ -481,6 +488,7 @@ bootstrap_application() { python manage.py check --deploy local hook for hook in "${SCRIPT_BEFORE[@]}"; do run_hook "$hook"; done + check_for_missing_migrations before_django_migrate "$ACTION" "$MIGRATION_MODULES" python manage.py migrate --noinput if [[ "$LOAD_TABLES" == "true" && "$SKIP_TABLES" == "false" ]]; then From bf80268648731e3ebf7a4fee6939e290b5c16b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 10:28:36 +0200 Subject: [PATCH 42/55] Sync standard initial table loading --- README.md | 4 ++++ container_install.sh | 7 +++++++ install.sh | 8 +++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 09a61699..a1b3fdc7 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,10 @@ bash container_install.sh --test --action install --engine docker \ --script migrate_optional_values ``` +Fresh installs automatically load `conf/first_install_tables.json` when the +application provides it. Use `--skip_tables` for an exceptional fresh install +without that fixture, or `--tables` to load it explicitly during an upgrade. + `--demo_data`, `--skip_demo_data`, and `--skip_test_data` are part of the standard interface. A project that supplies fixtures or demo files must set `application_supports_test_data=true` and implement `load_test_deployment_data` diff --git a/container_install.sh b/container_install.sh index 574bba92..0029a87f 100755 --- a/container_install.sh +++ b/container_install.sh @@ -246,6 +246,8 @@ bootstrap_service() { uid="$(service_uid "$service_name")"; gid="$(service_gid "$service_name")" stage_container_runtime_config "$container_id" "${install_conf_host_by_service[$service_name]}" "$runtime_conf" "$uid" "$gid" args=(--bootstrap "$deployment_action" --git_revision "$git_revision" --conf "$runtime_conf" --skip_apache_restart) + [ "$load_tables" = false ] || args+=(--tables) + [ "$skip_tables" = false ] || args+=(--skip_tables) for hook in "${migration_script_before[@]}"; do args+=(--script_before "$hook"); done for hook in "${migration_script_after[@]}"; do args+=(--script_after "$hook"); done status=0; engine_exec exec "$container_id" bash "$repo_path/install.sh" "${args[@]}" || status=$? @@ -331,6 +333,7 @@ action="install"; mode="production"; engine="docker"; git_revision="current" install_conf=""; compose_file=""; compose_env_file="" install_conf_map_entries=(); migration_script_before=(); migration_script_after=() demo_data=""; skip_demo_data=""; skip_test_data="" +load_tables=false; skip_tables=false usage() { cat <<'EOF' @@ -347,6 +350,8 @@ Options: --script_before --script_after --script + --tables Load initial tables; opt-in on upgrades. + --skip_tables Skip initial tables on a fresh install. --demo_data Import application demo data on install. --skip_demo_data --skip_test_data @@ -368,6 +373,8 @@ while (($#)); do --compose_file) compose_file="${2:-}"; shift 2 ;; --script_before) migration_script_before+=("${2:-}"); shift 2 ;; --script_after|--script) migration_script_after+=("${2:-}"); shift 2 ;; + --tables) load_tables=true; skip_tables=false; shift ;; + --skip_tables) skip_tables=true; load_tables=false; shift ;; --demo_data) demo_data="${2:-}"; shift 2 ;; --skip_demo_data) skip_demo_data=true; shift ;; --skip_test_data) skip_test_data=true; shift ;; diff --git a/install.sh b/install.sh index 21e9aed1..a0915cd4 100755 --- a/install.sh +++ b/install.sh @@ -86,9 +86,11 @@ while (($#)); do esac done -# iSkyLIMS ships its reference catalog as part of a fresh installation. Keep -# upgrades opt-in so existing operator-managed data is never reloaded. -if [[ "$ACTION" == "install" && "$SKIP_TABLES" == "false" ]]; then +# A fresh installation loads an application-owned initial fixture by default. +# Upgrades remain opt-in through --tables; --skip_tables is an explicit escape +# hatch for recovery or externally restored databases. +if [[ "$ACTION" == "install" && "$SKIP_TABLES" == "false" \ + && -f "$install_script_dir/conf/first_install_tables.json" ]]; then LOAD_TABLES="true" fi From de64d084a8b1f34bca3ce7313348f568b0592c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 11:07:06 +0200 Subject: [PATCH 43/55] Sync protected deployment environment guidance --- LEAME.md | 9 ++++++--- README.md | 8 +++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/LEAME.md b/LEAME.md index 72f59392..822756e1 100644 --- a/LEAME.md +++ b/LEAME.md @@ -117,8 +117,11 @@ Registrar el commit exacto con `git rev-parse HEAD`. Crear un fichero ignorado y con modo `0600` por servicio a partir de su `conf/docker_production_settings.txt`. Resolver todos los `CHANGE_ME` y revisar -la matriz [`conf/INSTALL_SETTINGS.md`](conf/INSTALL_SETTINGS.md). No guardar -secretos en `.env.production.file`, Compose, Git ni argumentos de proceso. +la matriz [`conf/INSTALL_SETTINGS.md`](conf/INSTALL_SETTINGS.md). El instalador +genera `.env.production.file` con valores runtime, incluidos secretos copiados +desde estos ficheros protegidos. Mantenerlo con modo `0600`, fuera de Git y +dentro del backup protegido de configuracion. Ninguno de estos ficheros se +copia en las capas de las imagenes. ```bash install -d -m 0700 deployment/settings @@ -202,7 +205,7 @@ Ejecutar el comando de instalación/upgrade: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt 2>&1 | tee "${date +%Y%m%d_%H%M%S}_prod_install.log" + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt 2>&1 | tee "$(date +%Y%m%d_%H%M%S)_prod_install.log" ``` Durante `--action upgrade`, `container_install.sh`: diff --git a/README.md b/README.md index a1b3fdc7..0179ade6 100644 --- a/README.md +++ b/README.md @@ -211,9 +211,11 @@ bash container_install.sh --action install --engine podman \ --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` -The installer creates `.env.production.file`; use it for later direct Compose -operations. Production secrets stay in the protected settings files and are -not copied into image layers. +The installer creates `.env.production.file` for later direct Compose +operations. It contains generated runtime values, including secrets copied +from the protected settings sources, so keep it mode `0600`, excluded from Git, +and inside the protected configuration backup. Neither the protected settings +nor this generated environment file is copied into image layers. #### Persist logs/documents on the host From e66ada7c74e912bfa77bb7cb50b7967e3603ce50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 11:40:00 +0200 Subject: [PATCH 44/55] Sync topology-aware deployment runbook --- .bu-isciii-deployment/state.json | 10 ++++---- LEAME.md | 40 +++++++++++++++++++++--------- README.md | 10 ++++++-- deployment/lib/container/django.sh | 15 +++++++++-- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index da770bee..23b98b61 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -34,8 +34,8 @@ ".dockerignore": "a76e2a974786736d1d3744ed534ae9996514cb15d921328450f3757fafe273d2", ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", "Dockerfile": "57f0682100debbf2e3268c9d6d7540a6bd3e20cf575931c2cebd054d4bc7e935", - "LEAME.md": "a9348c581cb50e5e55961c919b454abc536820a9f8a933e4d64601a14c06eb5e", - "README.md": "0bbcf653bc69d014ddb8888a4ddc42c35c88cb94b3da67f0d521b928dda1d283", + "LEAME.md": "92800e2471ee2bc59ca15adb7322d4e9a302d2197d5fc848bafa0075e8c78d79", + "README.md": "6a394bdaa9e468a585da66b46bd1f42e3655a8bb14b2373cbd2b243d4cbd9978", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "d5a0b6143fc4b0ba87f1f7f827e0d6c84dda1857e9697cd6acba4344beae375e", @@ -46,15 +46,15 @@ "conf/docker_test_settings.txt": "9dec2eb2d6faa05130f567049e558aafd7408b2c68b36f0af1517787a657262a", "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", - "conf/template_settings.py": "ed15033d4c3ccd3de585cea8679976b1c108f1ecfd4453cb77d5af606c984b3a", - "container_install.sh": "3ea6dd7e1f4355c8c6e92c65d5d1e1801d8d9b3f2dfc485935f26e381efdd1bf", + "conf/template_settings.py": "624f1d9701da67f506e8884af30e82ff24777b7369a6c485b77173f7d55611d6", + "container_install.sh": "ea91c5e67ab1fdd505cd97c56915a27e1dbb489f5d99fb61beb94a2514fc79e1", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", "docker-compose.prod.yml": "9feb8d66df5aee56822fbec2fa190ad151d17c3b838623211a41d42fb5480f3f", "docker-compose.test.yml": "7abc7acd601a7602590161f4d05436dfcaaafb9455147941cb3c5b3a220038bf", - "install.sh": "025b475c3dd6f9da608d1576a1de7e0826d8a084b0456d4a65f7927248c6b09e", + "install.sh": "de6bb7fedbc70a08fed652680943a85ce30855e44e1eb3e6e44eda727ee51c88", "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, diff --git a/LEAME.md b/LEAME.md index 822756e1..8a8015d5 100644 --- a/LEAME.md +++ b/LEAME.md @@ -1,9 +1,9 @@ # Actualizacion de iSkyLIMS con Podman rootless Esta guia es la lista de ejecucion para instalar, actualizar y recuperar el -despliegue de produccion. Los comandos generados son reutilizables; antes de la -aprobacion, el responsable de la aplicacion debe completar los campos marcados -`` con valores o referencias institucionales verificadas. +despliegue de produccion. Los comandos generados son reutilizables. Antes de la +aprobacion, el responsable debe registrar las entradas de despliegue indicadas +abajo con valores o referencias institucionales verificadas. ## Indice @@ -24,10 +24,15 @@ aprobacion, el responsable de la aplicacion debe completar los campos marcados - Podman rootless y un proveedor de Compose funcionales. - El mismo usuario sin privilegios para el instalador y Podman. -- Revision aprobada: ``. -- DNS/TLS, base de datos, almacenamiento, correo e identidad: ``. -- Responsable operativo y contacto de escalado: ``. -- Objetivos RPO/RTO y ubicacion de backups: ``. +Entradas de despliegue que deben quedar registradas antes de ejecutar: + +| Entrada | Evidencia requerida | +|---|---| +| Revision aprobada | Tag o commit inmutable y aprobacion asociada | +| Exposicion publica | URL, DNS, propietario de TLS y reglas de proxy | +| Dependencias | Base de datos, almacenamiento, correo e identidad aplicables | +| Operacion | Responsable del servicio y contacto de escalado | +| Recuperacion | RPO, RTO, retencion y ubicacion de backups | ```bash podman info @@ -162,7 +167,9 @@ git rev-parse HEAD > "$BACKUP_DIR/git-revision.txt" podman compose --env-file .env.production.file -f docker-compose.prod.yml \ images > "$BACKUP_DIR/images.txt" cp .env.production.file "$BACKUP_DIR/" -cp "$BACKUP_DIR/" +cp deployment/settings/app_production_settings.txt "$BACKUP_DIR/" +cp deployment/settings/apache_production_settings.txt "$BACKUP_DIR/" +cp deployment/settings/samba_production_settings.txt "$BACKUP_DIR/" chmod -R go-rwx "$BACKUP_DIR" ``` @@ -232,9 +239,15 @@ podman compose --env-file .env.production.file -f docker-compose.prod.yml logs - bash scripts/smoke_test.sh --engine podman ``` -Verificar tambien ``, autenticacion, correo, tareas -programadas y un flujo real de lectura. Registrar estado, imagenes, revision y -resultado de aceptacion. +Completar las comprobaciones que corresponden a la topologia seleccionada: + +- `app`: confirmar `/health/` y un flujo real de iSkyLIMS. +- Apache: confirmar la URL publica, DNS/TLS, proxy y cabeceras reenviadas. +- Samba: en los modos habilitados, confirmar acceso autenticado y un flujo de + lectura/escritura desde un cliente aprobado. + +Verificar tambien correo y tareas programadas. Registrar URL y resultados junto +con estado, imagenes y revision desplegada. ## Rollback @@ -256,7 +269,10 @@ mysql --host= --port= --user= --password \ podman volume import "$BACKUP_DIR/documents.tar" podman volume import "$BACKUP_DIR/static.tar" tar -C /srv/containers/bind -xzf "$BACKUP_DIR/bind-mounts.tar.gz" -cp "$BACKUP_DIR/" / +install -d -m 0700 deployment/settings +install -m 0600 "$BACKUP_DIR/app_production_settings.txt" deployment/settings/app_production_settings.txt +install -m 0600 "$BACKUP_DIR/apache_production_settings.txt" deployment/settings/apache_production_settings.txt +install -m 0600 "$BACKUP_DIR/samba_production_settings.txt" deployment/settings/samba_production_settings.txt ``` Restaurar el fichero de ajustes protegido, desplegar la revision anotada en diff --git a/README.md b/README.md index 0179ade6..a0ccb803 100644 --- a/README.md +++ b/README.md @@ -400,7 +400,6 @@ settings files, and backup identifiers. ```bash BACKUP_DIR="/srv/containers/backup/relecov-iskylims/$(date +%Y%m%d_%H%M%S)" -SETTINGS_FILE='deployment/settings/app_production_settings.txt' DOCUMENTS_VOLUME='CHANGE_ME' DB_HOST='CHANGE_ME' DB_PORT='3306' @@ -408,7 +407,10 @@ DB_NAME='CHANGE_ME' DB_USER='CHANGE_ME' mkdir -p "$BACKUP_DIR" git rev-parse HEAD > "$BACKUP_DIR/git-revision.txt" -cp .env.production.file "$SETTINGS_FILE" "$BACKUP_DIR/" +cp .env.production.file "$BACKUP_DIR/" +cp deployment/settings/app_production_settings.txt "$BACKUP_DIR/" +cp deployment/settings/apache_production_settings.txt "$BACKUP_DIR/" +cp deployment/settings/samba_production_settings.txt "$BACKUP_DIR/" chmod -R go-rwx "$BACKUP_DIR" mysqldump --single-transaction --routines --triggers \ @@ -463,6 +465,10 @@ mysql --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" --password \ "$DB_NAME" < "$BACKUP_DIR/database.sql" podman volume import "$DOCUMENTS_VOLUME" "$BACKUP_DIR/documents.tar" tar -C /srv/containers/bind -xzf "$BACKUP_DIR/bind-mounts.tar.gz" +install -d -m 0700 deployment/settings +install -m 0600 "$BACKUP_DIR/app_production_settings.txt" deployment/settings/app_production_settings.txt +install -m 0600 "$BACKUP_DIR/apache_production_settings.txt" deployment/settings/apache_production_settings.txt +install -m 0600 "$BACKUP_DIR/samba_production_settings.txt" deployment/settings/samba_production_settings.txt bash container_install.sh --action fix-permissions --engine podman \ --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` diff --git a/deployment/lib/container/django.sh b/deployment/lib/container/django.sh index 56dbcd28..2b07ae07 100644 --- a/deployment/lib/container/django.sh +++ b/deployment/lib/container/django.sh @@ -7,7 +7,7 @@ # host before the application container can start. The inner installer cannot # create or repair that host path because it runs after mounts are established. -BU_ISCIII_DJANGO_CONTAINER_LIB_VERSION="0.1.0" +BU_ISCIII_DJANGO_CONTAINER_LIB_VERSION="0.2.0" # Generate a cryptographically random Django-compatible SECRET_KEY without # requiring Django itself to be installed on the deployment host. It belongs in @@ -31,6 +31,8 @@ render_django_settings_file() { local secret_line="" local template_secret_line="" local django_debug="" + local token variable value python_literal + local -a application_replacements=() if [ -f "$settings_path" ]; then secret_line="$(grep -E "^SECRET_KEY[[:space:]]*=" "$settings_path" | tail -n 1 || true)" @@ -53,6 +55,14 @@ render_django_settings_file() { return 1 ;; esac + while IFS= read -r token; do + [ -n "$token" ] || continue + variable="${token#settingsconf_}" + value="$(read_install_conf_value "$variable" "$install_conf_path")" || return 1 + python_literal="$(python3 -c \ + 'import json, sys; print(json.dumps(sys.argv[1]))' "$value")" || return 1 + application_replacements+=("$token" "$python_literal") + done < <(grep -oE 'settingsconf_[A-Z][A-Z0-9_]*' "$template_path" | sort -u || true) render_config_template "$template_path" "$settings_path" 0664 \ "$template_secret_line" "$secret_line" \ djangouser "$(read_install_conf_first "$install_conf_path" DB_USER)" \ @@ -68,7 +78,8 @@ render_django_settings_file() { djangodebug "$django_debug" \ djangoallowedhosts "$(read_install_conf_value DJANGO_ALLOWED_HOSTS "$install_conf_path")" \ djangocsrftrustedorigins "$(read_install_conf_value DJANGO_CSRF_TRUSTED_ORIGINS "$install_conf_path")" \ - dbconnmaxage "$(config_value_or_default DB_CONN_MAX_AGE "$install_conf_path" 0)" + dbconnmaxage "$(config_value_or_default DB_CONN_MAX_AGE "$install_conf_path" 0)" \ + "${application_replacements[@]}" } # Ensure the production settings bind source exists and reflects the selected From cd1156ab8d6bc6d23f4c1ec0a4d665a7fbabdb19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 11:48:16 +0200 Subject: [PATCH 45/55] Generate host directories from protected settings --- .bu-isciii-deployment/state.json | 4 ++-- LEAME.md | 35 ++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 23b98b61..07f2cc98 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -34,8 +34,8 @@ ".dockerignore": "a76e2a974786736d1d3744ed534ae9996514cb15d921328450f3757fafe273d2", ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", "Dockerfile": "57f0682100debbf2e3268c9d6d7540a6bd3e20cf575931c2cebd054d4bc7e935", - "LEAME.md": "92800e2471ee2bc59ca15adb7322d4e9a302d2197d5fc848bafa0075e8c78d79", - "README.md": "6a394bdaa9e468a585da66b46bd1f42e3655a8bb14b2373cbd2b243d4cbd9978", + "LEAME.md": "349f23a1081c4a936fc99d9520d77d6cca8cc36630609df310e013979de9f5ac", + "README.md": "40bc17dc018d317a1f546afd1aa5e8b49a2e61ecf69b2a12b7f81da9c618aeb8", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "d5a0b6143fc4b0ba87f1f7f827e0d6c84dda1857e9697cd6acba4344beae375e", diff --git a/LEAME.md b/LEAME.md index 8a8015d5..ab4a61eb 100644 --- a/LEAME.md +++ b/LEAME.md @@ -87,22 +87,41 @@ Persistencia declarada por el despliegue: ## Preparar directorios del host -Crear la estructura comun antes de la primera instalacion. Sustituir -`` por la cuenta que ejecutara siempre Podman y el instalador. Es decir, normalmente la cuenta con la que estás conectado a la máquina virtual. +Crear primero la estructura comun. Sustituir `` por la cuenta +que ejecutara siempre Podman y el instalador; normalmente es la cuenta de la +sesion actual. ```bash sudo mkdir -p /opt/containers_apps/relecov-iskylims sudo mkdir -p /srv/containers/backup/relecov-iskylims -sudo mkdir -p /srv/containers/bind/relecov-iskylims/settings -sudo mkdir -p /var/log/local/relecov-iskylims/apps -sudo mkdir -p /var/log/local/relecov-iskylims/apache sudo chown -R : \ /opt/containers_apps/relecov-iskylims \ - /srv/containers/backup/relecov-iskylims \ - /srv/containers/bind/relecov-iskylims \ - /var/log/local/relecov-iskylims + /srv/containers/backup/relecov-iskylims ``` +Despues de copiar y completar todos los ficheros protegidos en +`deployment/settings/`, crear los binds exactamente donde indica cada servicio: + +```bash +PODMAN_USER='' +( + source deployment/settings/app_production_settings.txt + : "${HOST_LOG_PATH:?HOST_LOG_PATH is required for app}" + : "${DJANGO_SETTINGS_PATH:?DJANGO_SETTINGS_PATH is required for app}" + sudo install -d -o "$PODMAN_USER" -g "$PODMAN_USER" \ + "$HOST_LOG_PATH" "$(dirname "$DJANGO_SETTINGS_PATH")" +) +( + source deployment/settings/apache_production_settings.txt + : "${APACHE_LOG_PATH:?APACHE_LOG_PATH is required for apache}" + sudo install -d -o "$PODMAN_USER" -g "$PODMAN_USER" "$APACHE_LOG_PATH" +) +``` + +Los ficheros se cargan como el usuario actual dentro de subshells; solo +`install -d` usa privilegios. Revisar antes las rutas y no ejecutar los +ficheros completos con `sudo`. + ## Actualizar codigo Para un checkout nuevo: From 1778d00b4a91e897fb780b836383dd4bc5af5231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 14:01:20 +0200 Subject: [PATCH 46/55] Order host setup after protected settings --- .bu-isciii-deployment/state.json | 4 +-- LEAME.md | 45 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 07f2cc98..7208934c 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -34,8 +34,8 @@ ".dockerignore": "a76e2a974786736d1d3744ed534ae9996514cb15d921328450f3757fafe273d2", ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", "Dockerfile": "57f0682100debbf2e3268c9d6d7540a6bd3e20cf575931c2cebd054d4bc7e935", - "LEAME.md": "349f23a1081c4a936fc99d9520d77d6cca8cc36630609df310e013979de9f5ac", - "README.md": "40bc17dc018d317a1f546afd1aa5e8b49a2e61ecf69b2a12b7f81da9c618aeb8", + "LEAME.md": "1cc21db46ffdf5ca853aaddeb476822f08a71608cb5c9c8546c4994a8fa79876", + "README.md": "a7aad5530abda43654a28cfeb50c3da05852faaf198141e6533027a10ecfd9d0", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "d5a0b6143fc4b0ba87f1f7f827e0d6c84dda1857e9697cd6acba4344beae375e", diff --git a/LEAME.md b/LEAME.md index ab4a61eb..cd90f769 100644 --- a/LEAME.md +++ b/LEAME.md @@ -99,29 +99,6 @@ sudo chown -R : \ /srv/containers/backup/relecov-iskylims ``` -Despues de copiar y completar todos los ficheros protegidos en -`deployment/settings/`, crear los binds exactamente donde indica cada servicio: - -```bash -PODMAN_USER='' -( - source deployment/settings/app_production_settings.txt - : "${HOST_LOG_PATH:?HOST_LOG_PATH is required for app}" - : "${DJANGO_SETTINGS_PATH:?DJANGO_SETTINGS_PATH is required for app}" - sudo install -d -o "$PODMAN_USER" -g "$PODMAN_USER" \ - "$HOST_LOG_PATH" "$(dirname "$DJANGO_SETTINGS_PATH")" -) -( - source deployment/settings/apache_production_settings.txt - : "${APACHE_LOG_PATH:?APACHE_LOG_PATH is required for apache}" - sudo install -d -o "$PODMAN_USER" -g "$PODMAN_USER" "$APACHE_LOG_PATH" -) -``` - -Los ficheros se cargan como el usuario actual dentro de subshells; solo -`install -d` usa privilegios. Revisar antes las rutas y no ejecutar los -ficheros completos con `sudo`. - ## Actualizar codigo Para un checkout nuevo: @@ -157,6 +134,28 @@ install -m 0600 conf/samba/samba_production_settings.txt deployment/settings/sam Editar unicamente las copias bajo `deployment/settings/`. Los comandos de instalacion y actualizacion usan estas rutas protegidas. +Despues de completar y revisar esos ficheros, crear los binds exactamente donde +indica cada servicio: + +```bash +PODMAN_USER='' +( + source deployment/settings/app_production_settings.txt + : "${HOST_LOG_PATH:?HOST_LOG_PATH is required for app}" + : "${DJANGO_SETTINGS_PATH:?DJANGO_SETTINGS_PATH is required for app}" + sudo install -d -o "$PODMAN_USER" -g "$PODMAN_USER" \ + "$HOST_LOG_PATH" "$(dirname "$DJANGO_SETTINGS_PATH")" +) +( + source deployment/settings/apache_production_settings.txt + : "${APACHE_LOG_PATH:?APACHE_LOG_PATH is required for apache}" + sudo install -d -o "$PODMAN_USER" -g "$PODMAN_USER" "$APACHE_LOG_PATH" +) +``` + +Los ficheros se cargan como el usuario actual dentro de subshells; solo +`install -d` usa privilegios. No ejecutar los ficheros completos con `sudo`. + Valores que requieren decision del responsable de la aplicacion: - hostnames publicos, TLS y proxy; From 0008dab1d3656cdc3a94cecc71880e63617d4503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 14:08:28 +0200 Subject: [PATCH 47/55] Clarify settings before host directory setup --- .bu-isciii-deployment/state.json | 2 +- LEAME.md | 40 ++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 7208934c..4c5e89ce 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -34,7 +34,7 @@ ".dockerignore": "a76e2a974786736d1d3744ed534ae9996514cb15d921328450f3757fafe273d2", ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", "Dockerfile": "57f0682100debbf2e3268c9d6d7540a6bd3e20cf575931c2cebd054d4bc7e935", - "LEAME.md": "1cc21db46ffdf5ca853aaddeb476822f08a71608cb5c9c8546c4994a8fa79876", + "LEAME.md": "f6751e0ca0d8811349a2c5a9d9ec6d59b8b7cab18f3ce1441feb2a894dfb6b52", "README.md": "a7aad5530abda43654a28cfeb50c3da05852faaf198141e6533027a10ecfd9d0", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", diff --git a/LEAME.md b/LEAME.md index cd90f769..0f557851 100644 --- a/LEAME.md +++ b/LEAME.md @@ -9,9 +9,10 @@ abajo con valores o referencias institucionales verificadas. - [Requisitos](#requisitos) - [Estructura de directorios en los servidores](#estructura-de-directorios-en-los-servidores) -- [Preparar directorios del host](#preparar-directorios-del-host) +- [Preparar checkout y backup](#preparar-checkout-y-backup) - [Actualizar codigo](#actualizar-codigo) - [Configurar los ajustes de produccion](#configurar-los-ajustes-de-produccion) +- [Preparar directorios persistentes del host](#preparar-directorios-persistentes-del-host) - [Backup antes de actualizar](#backup-antes-de-actualizar) - [Ejecutar la actualizacion](#ejecutar-la-actualizacion) - [Comprobaciones posteriores](#comprobaciones-posteriores) @@ -85,11 +86,11 @@ Persistencia declarada por el despliegue: | Rendered Apache configuration | `deployment/apache/` in the deployment checkout | Rebuildable; preserve reviewed source configuration | | Samba test data | `samba_test_data` named volume | Disposable test/demo files | -## Preparar directorios del host +## Preparar checkout y backup -Crear primero la estructura comun. Sustituir `` por la cuenta -que ejecutara siempre Podman y el instalador; normalmente es la cuenta de la -sesion actual. +Crear solo las ubicaciones necesarias para obtener el codigo y guardar backups. +Sustituir `` por la cuenta que ejecutara siempre Podman y el +instalador. Los binds y logs se crean despues de completar los ajustes. ```bash sudo mkdir -p /opt/containers_apps/relecov-iskylims @@ -134,8 +135,20 @@ install -m 0600 conf/samba/samba_production_settings.txt deployment/settings/sam Editar unicamente las copias bajo `deployment/settings/`. Los comandos de instalacion y actualizacion usan estas rutas protegidas. -Despues de completar y revisar esos ficheros, crear los binds exactamente donde -indica cada servicio: +Valores que requieren decision del responsable de la aplicacion: + +- hostnames publicos, TLS y proxy; +- base de datos y credenciales de minimo privilegio; +- rutas persistentes, UID/GID, SELinux y politica de backup; +- correo, identidad, almacenamiento y ajustes propios de la aplicacion; +- administrador inicial y transferencia segura de sus credenciales. + +Completar todas esas decisiones y resolver cada `CHANGE_ME` antes de continuar. + +## Preparar directorios persistentes del host + +Solo despues de completar y revisar esos ficheros, crear los binds exactamente +donde indica cada servicio: ```bash PODMAN_USER='' @@ -156,17 +169,8 @@ PODMAN_USER='' Los ficheros se cargan como el usuario actual dentro de subshells; solo `install -d` usa privilegios. No ejecutar los ficheros completos con `sudo`. -Valores que requieren decision del responsable de la aplicacion: - -- hostnames publicos, TLS y proxy; -- base de datos y credenciales de minimo privilegio; -- rutas persistentes, UID/GID, SELinux y politica de backup; -- correo, identidad, almacenamiento y ajustes propios de la aplicacion; -- administrador inicial y transferencia segura de sus credenciales. - -Solo despues de crear y completar todos los ficheros bajo -`deployment/settings/`, aplicar UID/GID internos, modos y etiquetas SELinux -mediante el instalador. No modificar el arbol `/srv/containers/storage/` +Con los directorios preparados, aplicar UID/GID internos, modos y etiquetas +SELinux mediante el instalador. No modificar `/srv/containers/storage/` manualmente. ```bash From 67e0419bfe93e5fb5544d90a0c5c212daf41ad40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 15:13:01 +0200 Subject: [PATCH 48/55] Sync Django boolean normalization --- deployment/lib/container/django.sh | 34 +++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/deployment/lib/container/django.sh b/deployment/lib/container/django.sh index 2b07ae07..b0ece434 100644 --- a/deployment/lib/container/django.sh +++ b/deployment/lib/container/django.sh @@ -21,6 +21,22 @@ generate_django_secret_key() { fi } +# Convert conventional configuration boolean spellings to a Python literal. +# Arguments: setting name and raw value. +django_python_boolean() { + local setting_name="$1" + local value="$2" + + case "${value,,}" in + true|1|yes|on) printf 'True\n' ;; + false|0|no|off) printf 'False\n' ;; + *) + echo "$setting_name must be a boolean value." >&2 + return 1 + ;; + esac +} + # Render a Django settings template from the standard BU-ISCIII installation # keys while preserving an existing non-placeholder SECRET_KEY. # Arguments: template path, destination settings path, install-conf path. @@ -31,6 +47,7 @@ render_django_settings_file() { local secret_line="" local template_secret_line="" local django_debug="" + local email_use_tls="" local token variable value python_literal local -a application_replacements=() @@ -46,15 +63,12 @@ render_django_settings_file() { echo "Django template '$template_path' has no SECRET_KEY assignment." >&2 return 1 fi - django_debug="$(config_value_or_default DJANGO_DEBUG "$install_conf_path" false)" - case "${django_debug,,}" in - true|1|yes|on) django_debug=True ;; - false|0|no|off) django_debug=False ;; - *) - echo "DJANGO_DEBUG must be a boolean value." >&2 - return 1 - ;; - esac + django_debug="$(django_python_boolean DJANGO_DEBUG \ + "$(config_value_or_default DJANGO_DEBUG "$install_conf_path" false)")" \ + || return 1 + email_use_tls="$(django_python_boolean EMAIL_USE_TLS \ + "$(read_install_conf_value EMAIL_USE_TLS "$install_conf_path")")" \ + || return 1 while IFS= read -r token; do [ -n "$token" ] || continue variable="${token#settingsconf_}" @@ -74,7 +88,7 @@ render_django_settings_file() { emailport "$(read_install_conf_value EMAIL_PORT "$install_conf_path")" \ emailhostuser "$(read_install_conf_value EMAIL_HOST_USER "$install_conf_path")" \ emailhostpassword "$(read_install_conf_value EMAIL_HOST_PASSWORD "$install_conf_path")" \ - emailhosttls "$(read_install_conf_value EMAIL_USE_TLS "$install_conf_path")" \ + emailhosttls "$email_use_tls" \ djangodebug "$django_debug" \ djangoallowedhosts "$(read_install_conf_value DJANGO_ALLOWED_HOSTS "$install_conf_path")" \ djangocsrftrustedorigins "$(read_install_conf_value DJANGO_CSRF_TRUSTED_ORIGINS "$install_conf_path")" \ From b5d11e323cfca1ac16741cbd65edd00c663d34db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 15:30:39 +0200 Subject: [PATCH 49/55] Install timezone data in iSkyLIMS image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8395946a..5c62b489 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ ENV APP_REPO_PATH=${APP_REPO_PATH} \ RUN microdnf -y update && \ microdnf -y install python3.11 python3.11-pip \ - tar gcc git rsync wget && \ + tar gcc git rsync wget tzdata && \ microdnf clean all ARG SUPERCRONIC_VERSION=v0.2.38 From 6ac0ea95252bd916ed075c66b00e2e816f7ac0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Mon, 17 Aug 2026 15:36:03 +0200 Subject: [PATCH 50/55] Restore timezone payload during iSkyLIMS build --- install.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index a0915cd4..1f5aea58 100755 --- a/install.sh +++ b/install.sh @@ -212,7 +212,7 @@ install_application_system_packages() { apt-utils wget tar gcc g++ make \ libmysqlclient-dev default-mysql-client \ python3-venv libpq-dev python3-dev python3-pip python3-wheel \ - apache2-dev cifs-utils gnuplot + apache2-dev cifs-utils gnuplot tzdata ;; rhel|centos|fedora|ubi) command_required microdnf @@ -243,6 +243,9 @@ install_application_system_packages() { python3.11-devel httpd-devel sqlite sqlite-devel mariadb \ mariadb-connector-c-devel libffi-devel gnuplot cifs-utils \ git rsync shadow-utils + # UBI minimal can record tzdata as installed without its zoneinfo + # payload. Reinstall it so Python can resolve Django TIME_ZONE values. + microdnf reinstall -y tzdata microdnf clean all ;; *) From f91f16ac9736038d7bc813789f58dc2439d2b478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 18 Aug 2026 10:54:49 +0200 Subject: [PATCH 51/55] Sync mapped demo data deployment updates --- .bu-isciii-deployment/state.json | 12 +++---- .dockerignore | 1 + .gitignore | 3 ++ LEAME.md | 49 +++++++++++++++------------- README.md | 10 +++--- container_install.sh | 56 +++++++++++++++++++++++++------- install.sh | 25 +++++++++----- 7 files changed, 103 insertions(+), 53 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 4c5e89ce..c395728f 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -31,11 +31,11 @@ "TIMEZONE": "Europe/Madrid" }, "files": { - ".dockerignore": "a76e2a974786736d1d3744ed534ae9996514cb15d921328450f3757fafe273d2", - ".gitignore": "86a6d029356710031e592d78821b3c5a2ef0bc510fa746703bfcef56ea10fc15", - "Dockerfile": "57f0682100debbf2e3268c9d6d7540a6bd3e20cf575931c2cebd054d4bc7e935", + ".dockerignore": "bd9c115511d6b9dae318e4b6a85ddb282e60a46fd89c6cdecccdc20c668eb0e9", + ".gitignore": "de487336ed72efd6601b985eda1c83754d6ee33b1cb4ce9f8973411caae833ed", + "Dockerfile": "4747a22b4332ae6c6ffbd6d67f8fe92b80fb4a2ea9e102a6fca0eb68009cbef2", "LEAME.md": "f6751e0ca0d8811349a2c5a9d9ec6d59b8b7cab18f3ce1441feb2a894dfb6b52", - "README.md": "a7aad5530abda43654a28cfeb50c3da05852faaf198141e6533027a10ecfd9d0", + "README.md": "5251291d6f543a49364069b6239f81e5528af10bc6a687d4a4702166ea20c71c", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "d5a0b6143fc4b0ba87f1f7f827e0d6c84dda1857e9697cd6acba4344beae375e", @@ -47,14 +47,14 @@ "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", "conf/template_settings.py": "624f1d9701da67f506e8884af30e82ff24777b7369a6c485b77173f7d55611d6", - "container_install.sh": "ea91c5e67ab1fdd505cd97c56915a27e1dbb489f5d99fb61beb94a2514fc79e1", + "container_install.sh": "44afb988f19b8e32edb9641a500f7efe4f533b71380e1647aa03bb7708a7cd2e", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", "docker-compose.prod.yml": "9feb8d66df5aee56822fbec2fa190ad151d17c3b838623211a41d42fb5480f3f", "docker-compose.test.yml": "7abc7acd601a7602590161f4d05436dfcaaafb9455147941cb3c5b3a220038bf", - "install.sh": "de6bb7fedbc70a08fed652680943a85ce30855e44e1eb3e6e44eda727ee51c88", + "install.sh": "1a4f9bdd6d3082aa17798a360fa30c580c6391c78b2b5e9e75b49c9735168a0f", "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" }, diff --git a/.dockerignore b/.dockerignore index 9349a560..f481d804 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,6 +19,7 @@ venv/ virtualenv/ node_modules/ .npm/ +.next/ .vite/ __pycache__/ *.py[cod] diff --git a/.gitignore b/.gitignore index 250c23fc..2d9f31ae 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,8 @@ __pycache__/ *.py[cod] .venv/ virtualenv/ +node_modules/ +.next/ +.npm/ logs/ documents/ diff --git a/LEAME.md b/LEAME.md index 0f557851..381cb6ed 100644 --- a/LEAME.md +++ b/LEAME.md @@ -47,9 +47,10 @@ completo. La libreria compartida detecta ambos proveedores automaticamente. ## Estructura de directorios en los servidores -Todos los despliegues usan esta estructura institucional. El nombre de la -aplicacion separa sus fuentes bind, logs y backups; Podman administra su propio -storage y no debe modificarse manualmente. +Todos los despliegues usan esta estructura institucional. El despliegue separa +sus fuentes, binds, logs y backups; las rutas protegidas pueden definir un +namespace distinto. Podman administra su propio storage y no debe modificarse +manualmente. ```text /opt/containers_apps/ @@ -80,8 +81,8 @@ Persistencia declarada por el despliegue: | `app` database | External production database | Database backup before migration | | `app` documents | `app_documents` named volume | Volume backup | | `app` static | `app_static` named volume | Replaceable through collectstatic | -| `app` logs | `/var/log/local/relecov-iskylims/apps` host bind | Retain/rotate per institutional log policy | -| `app` rendered settings | `/srv/containers/bind/relecov-iskylims/settings/` host bind | Protected configuration backup | +| `app` logs | Host bind configured by `HOST_LOG_PATH` in `app_production_settings.txt` | Retain/rotate per institutional log policy | +| `app` rendered settings | Host bind configured by `DJANGO_SETTINGS_PATH` in `app_production_settings.txt` | Protected configuration backup | | Apache logs | `/var/log/local/relecov-iskylims/apache` host bind | Retain/rotate per institutional log policy | | Rendered Apache configuration | `deployment/apache/` in the deployment checkout | Rebuildable; preserve reviewed source configuration | | Samba test data | `samba_test_data` named volume | Disposable test/demo files | @@ -90,7 +91,8 @@ Persistencia declarada por el despliegue: Crear solo las ubicaciones necesarias para obtener el codigo y guardar backups. Sustituir `` por la cuenta que ejecutara siempre Podman y el -instalador. Los binds y logs se crean despues de completar los ajustes. +instalador; normalmente es la cuenta de la sesion actual. Los binds y logs se +crean despues de completar los ajustes protegidos. ```bash sudo mkdir -p /opt/containers_apps/relecov-iskylims @@ -132,9 +134,6 @@ install -m 0600 conf/apache/apache_production_settings.txt deployment/settings/a install -m 0600 conf/samba/samba_production_settings.txt deployment/settings/samba_production_settings.txt ``` -Editar unicamente las copias bajo `deployment/settings/`. Los comandos de -instalacion y actualizacion usan estas rutas protegidas. - Valores que requieren decision del responsable de la aplicacion: - hostnames publicos, TLS y proxy; @@ -143,12 +142,15 @@ Valores que requieren decision del responsable de la aplicacion: - correo, identidad, almacenamiento y ajustes propios de la aplicacion; - administrador inicial y transferencia segura de sus credenciales. -Completar todas esas decisiones y resolver cada `CHANGE_ME` antes de continuar. +Editar unicamente las copias bajo `deployment/settings/`, completar todas esas +decisiones y resolver cada `CHANGE_ME` antes de continuar. Los comandos de +instalacion y actualizacion usan estas rutas protegidas. ## Preparar directorios persistentes del host -Solo despues de completar y revisar esos ficheros, crear los binds exactamente -donde indica cada servicio: +Solo despues de completar y revisar todos los ajustes, crear los binds +exactamente donde indica cada servicio. Los ficheros se cargan como el usuario +actual dentro de subshells; solo `install -d` usa privilegios. ```bash PODMAN_USER='' @@ -166,12 +168,10 @@ PODMAN_USER='' ) ``` -Los ficheros se cargan como el usuario actual dentro de subshells; solo -`install -d` usa privilegios. No ejecutar los ficheros completos con `sudo`. - -Con los directorios preparados, aplicar UID/GID internos, modos y etiquetas -SELinux mediante el instalador. No modificar `/srv/containers/storage/` -manualmente. +Revisar las rutas resueltas antes de ejecutar. No usar valores procedentes de +una configuracion no revisada y no ejecutar los ficheros completos con `sudo`. +Aplicar despues UID/GID internos, modos y etiquetas SELinux mediante el +instalador. No modificar `/srv/containers/storage/` manualmente. ```bash bash container_install.sh --action fix-permissions --engine podman \ @@ -295,13 +295,16 @@ install -d -m 0700 deployment/settings install -m 0600 "$BACKUP_DIR/app_production_settings.txt" deployment/settings/app_production_settings.txt install -m 0600 "$BACKUP_DIR/apache_production_settings.txt" deployment/settings/apache_production_settings.txt install -m 0600 "$BACKUP_DIR/samba_production_settings.txt" deployment/settings/samba_production_settings.txt +bash container_install.sh --action fix-permissions --engine podman \ + --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` -Restaurar el fichero de ajustes protegido, desplegar la revision anotada en -`git-revision.txt` y dejar que el instalador regenere `.env.production.file`. -Ejecutar `fix-permissions`, arrancar y validar antes de -reabrir el servicio. Los volumenes deben existir y estar vacios antes de -`podman volume import`; recrearlos con Compose cuando sea necesario. +Restaurar todos los ficheros de ajustes protegidos y desplegar la revision +anotada en `git-revision.txt`. `fix-permissions` regenera +`.env.production.file` antes de cualquier restauracion gestionada por un +add-on. Arrancar y validar antes de reabrir el servicio. Los volumenes deben +existir y estar vacios antes de `podman volume import`; recrearlos con Compose +cuando sea necesario. ## Reparar permisos diff --git a/README.md b/README.md index a0ccb803..a902dcab 100644 --- a/README.md +++ b/README.md @@ -174,12 +174,12 @@ Fresh installs automatically load `conf/first_install_tables.json` when the application provides it. Use `--skip_tables` for an exceptional fresh install without that fixture, or `--tables` to load it explicitly during an upgrade. -`--demo_data`, `--skip_demo_data`, and `--skip_test_data` are part of the -standard interface. A project that supplies fixtures or demo files must set +`--demo_data_map `, `--skip_demo_data`, `--skip_test_data`, and +`--skip_test_data_service ` are part of the standard interface. +`--demo_data ` remains a single-service compatibility option. A project +that supplies fixtures or demo files must set `application_supports_test_data=true` and implement `load_test_deployment_data` -in its wrapper; otherwise `--demo_data` is rejected explicitly. -The same explicit `--demo_data ` input may be used on a fresh production -install when an application needs a controlled seed import. Production never +in its wrapper; otherwise explicit demo data is rejected. Production never selects or loads demo data by default, and upgrades never reload it. For an automatic first administrator, set `CREATE_INITIAL_SUPERUSER=true` and diff --git a/container_install.sh b/container_install.sh index 0029a87f..9f37ff0b 100755 --- a/container_install.sh +++ b/container_install.sh @@ -332,7 +332,8 @@ PY action="install"; mode="production"; engine="docker"; git_revision="current" install_conf=""; compose_file=""; compose_env_file="" install_conf_map_entries=(); migration_script_before=(); migration_script_after=() -demo_data=""; skip_demo_data=""; skip_test_data="" +demo_data=""; demo_data_service=""; demo_data_map_entries=() +skip_demo_data=""; skip_test_data=""; skip_test_data_services=() load_tables=false; skip_tables=false usage() { @@ -352,9 +353,11 @@ Options: --script --tables Load initial tables; opt-in on upgrades. --skip_tables Skip initial tables on a fresh install. - --demo_data Import application demo data on install. + --demo_data Single-service compatibility option. + --demo_data_map Repeat for service-specific data imports. --skip_demo_data --skip_test_data + --skip_test_data_service Repeat to skip one service's test fixtures. --help --version EOF @@ -376,8 +379,10 @@ while (($#)); do --tables) load_tables=true; skip_tables=false; shift ;; --skip_tables) skip_tables=true; load_tables=false; shift ;; --demo_data) demo_data="${2:-}"; shift 2 ;; + --demo_data_map) demo_data_map_entries+=("${2:-}"); shift 2 ;; --skip_demo_data) skip_demo_data=true; shift ;; --skip_test_data) skip_test_data=true; shift ;; + --skip_test_data_service) skip_test_data_services+=("${2:-}"); shift 2 ;; --help) usage; exit 0 ;; --version) echo "$APP_VERSION"; exit 0 ;; *) die "Unknown option: $1" ;; @@ -387,18 +392,37 @@ done # 2. Validate arguments before modifying deployment state. [[ "$action" =~ ^(install|upgrade|fix-permissions)$ ]] || die "Invalid action: $action" [[ "$engine" =~ ^(docker|podman)$ ]] || die "Invalid engine: $engine" -if [ -n "$demo_data" ] && [ "$application_supports_test_data" != true ]; then - die "--demo_data is not implemented for $APPLICATION_NAME" -fi +declare -A demo_data_by_service=() if [ -n "$demo_data" ]; then - [ -f "$demo_data" ] || die "Demo-data file not found: $demo_data" - demo_data="$(cd "$(dirname "$demo_data")" && pwd)/$(basename "$demo_data")" + [ "${#install_services[@]}" -eq 1 ] || die "--demo_data is valid only for a single-service deployment; use --demo_data_map service,path" + demo_data_map_entries+=("${install_services[0]},$demo_data") +fi +for mapping in "${demo_data_map_entries[@]}"; do + [[ "$mapping" == *,* ]] || die "Invalid --demo_data_map: $mapping" + service_name="${mapping%%,*}"; path="${mapping#*,}" + array_contains "$service_name" "${install_services[@]}" || die "Unknown demo-data service: $service_name" + [ -z "${demo_data_by_service[$service_name]+present}" ] || die "Duplicate --demo_data_map service: $service_name" + [ -n "$path" ] || die "Empty demo-data path for $service_name" + [ -f "$path" ] || die "Demo-data file not found for $service_name: $path" + demo_data_by_service["$service_name"]="$(cd "$(dirname "$path")" && pwd)/$(basename "$path")" +done +for service_name in "${skip_test_data_services[@]}"; do + array_contains "$service_name" "${install_services[@]}" || die "Unknown --skip_test_data_service: $service_name" +done +if [ "${#demo_data_by_service[@]}" -gt 0 ] && [ "$application_supports_test_data" != true ]; then + die "--demo_data_map is not implemented for $APPLICATION_NAME" +fi +if [ "${#demo_data_by_service[@]}" -gt 0 ] && [ "$action" != install ]; then + die "--demo_data_map is supported only with --action install" +fi +if [ "${#demo_data_by_service[@]}" -gt 0 ] && [ "${skip_demo_data:-false}" = true ]; then + die "--demo_data_map cannot be combined with --skip_demo_data" fi if [ "$mode" = test ] && [ "$action" = install ] \ && [ "$application_supports_test_data" = true ]; then skip_demo_data="${skip_demo_data:-false}" skip_test_data="${skip_test_data:-false}" -elif [ "$action" = install ] && [ -n "$demo_data" ] \ +elif [ "$action" = install ] && [ "${#demo_data_by_service[@]}" -gt 0 ] \ && [ "$application_supports_test_data" = true ]; then skip_demo_data="${skip_demo_data:-false}" skip_test_data=true @@ -521,11 +545,21 @@ for service_name in "${install_services[@]}"; do done # 11. Load application-owned data for a fresh test install, or for an explicit -# production --demo_data request. Production never imports data implicitly. +# production --demo_data_map request. Production never imports data implicitly. if [ "$action" = install ] \ && [ "$application_supports_test_data" = true ] \ - && { [ "$mode" = test ] || [ -n "$demo_data" ]; }; then - load_test_deployment_data + && { [ "$mode" = test ] || [ "${#demo_data_by_service[@]}" -gt 0 ]; }; then + if [ "${#demo_data_by_service[@]}" -gt 0 ]; then + for service_name in "${install_services[@]}"; do + [ -n "${demo_data_by_service[$service_name]+present}" ] || continue + demo_data_service="$service_name"; demo_data="${demo_data_by_service[$service_name]}"; skip_test_data=true + load_test_deployment_data "$demo_data_service" "$demo_data" + done + else + demo_data_service="${install_services[0]}"; demo_data="" + array_contains "$demo_data_service" "${skip_test_data_services[@]}" && skip_test_data=true + load_test_deployment_data "$demo_data_service" "$demo_data" + fi fi # 12. Execute the common smoke dispatcher with generated profile checks. diff --git a/install.sh b/install.sh index 1f5aea58..49e739b1 100755 --- a/install.sh +++ b/install.sh @@ -164,20 +164,29 @@ check_required_modules() { check_database() { # Prefer the MySQL CLI when available; container images use mysqlclient's - # MySQLdb module from the application virtual environment. - if command -v mysql >/dev/null 2>&1; then - MYSQL_PWD="$DB_PASSWORD" mysql --host="$DB_HOST" --port="$DB_PORT" \ - --user="$DB_USER" --database="$DB_NAME" --execute='SELECT 1' >/dev/null \ - || die "Unable to connect to database $DB_NAME at $DB_HOST:$DB_PORT" - return - fi - "$INSTALL_PATH/virtualenv/bin/python" - "$DB_HOST" "$DB_PORT" "$DB_USER" "$DB_PASSWORD" "$DB_NAME" <<'PY' + # MySQLdb module from the application virtual environment. Podman network + # aliases can become resolvable shortly after the container process starts, + # so retry this existing readiness check for up to 60 seconds. + local deadline=$((SECONDS + 60)) + while true; do + if command -v mysql >/dev/null 2>&1; then + MYSQL_PWD="$DB_PASSWORD" mysql --host="$DB_HOST" --port="$DB_PORT" \ + --user="$DB_USER" --database="$DB_NAME" --execute='SELECT 1' \ + >/dev/null 2>&1 && return 0 + elif "$INSTALL_PATH/virtualenv/bin/python" - "$DB_HOST" "$DB_PORT" "$DB_USER" "$DB_PASSWORD" "$DB_NAME" >/dev/null 2>&1 <<'PY' import sys import MySQLdb connection = MySQLdb.connect(host=sys.argv[1], port=int(sys.argv[2]), user=sys.argv[3], passwd=sys.argv[4], db=sys.argv[5]) connection.close() PY + then + return 0 + fi + ((SECONDS < deadline)) \ + || die "Unable to connect to database $DB_NAME at $DB_HOST:$DB_PORT after 60 seconds" + sleep 2 + done } # ============================================================================ From 0f75cad1c2e9c83ef823dff7203592fed027c3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 18 Aug 2026 11:34:47 +0200 Subject: [PATCH 52/55] Sync service-specific test database identity --- .bu-isciii-deployment/state.json | 6 +++--- conf/docker_test_settings.txt | 2 +- conf/template_settings.py | 2 +- docker-compose.test.yml | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index c395728f..7dcf98fd 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -43,17 +43,17 @@ "conf/apache/apache_production_settings.txt": "b070f6c82138f0772e52460e24868e4b48d38d412c9f78900a45e9bb00216506", "conf/apache/apache_test_settings.txt": "102c1b747def3ce56c5b977829eb07dddbca91c8c0012e36a130357851846554", "conf/docker_production_settings.txt": "ba4059934c970cf4d334348203e3241121cd745b3cff0d6a4d78a01965e61c51", - "conf/docker_test_settings.txt": "9dec2eb2d6faa05130f567049e558aafd7408b2c68b36f0af1517787a657262a", + "conf/docker_test_settings.txt": "44a7a32e588cd8779f3295523786cb421ea4fea8c0cd574d9d83cfc13d65d732", "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", - "conf/template_settings.py": "624f1d9701da67f506e8884af30e82ff24777b7369a6c485b77173f7d55611d6", + "conf/template_settings.py": "af539ad10729a371e0c735c121e6e62dfece96afce77fb86e3a0820df2e9dd2f", "container_install.sh": "44afb988f19b8e32edb9641a500f7efe4f533b71380e1647aa03bb7708a7cd2e", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", "docker-compose.prod.yml": "9feb8d66df5aee56822fbec2fa190ad151d17c3b838623211a41d42fb5480f3f", - "docker-compose.test.yml": "7abc7acd601a7602590161f4d05436dfcaaafb9455147941cb3c5b3a220038bf", + "docker-compose.test.yml": "ad5b5f540757859812160d03b4fb05c1b2ab208cb9fa6f359a3ffb12ec6e2973", "install.sh": "1a4f9bdd6d3082aa17798a360fa30c580c6391c78b2b5e9e75b49c9735168a0f", "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index ed7df81c..25ce3fa9 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -23,7 +23,7 @@ HOST_LOG_PATH='/tmp/relecov-iskylims/logs' DJANGO_SETTINGS_PATH='/tmp/relecov-iskylims/data/settings/iskylims/settings.py' ### Compose test database -DB_HOST='iskylims_app_db' +DB_HOST='app_db' DB_PORT='3306' DB_NAME='iskylims' DB_USER='django' diff --git a/conf/template_settings.py b/conf/template_settings.py index 0705fc6a..6c2a5f7f 100644 --- a/conf/template_settings.py +++ b/conf/template_settings.py @@ -92,7 +92,7 @@ "PASSWORD": "djangopass", "PORT": "djangoport", "NAME": "djangodbname", - "HOST": "djangohost", + "HOST": os.getenv("DB_HOST", "djangohost"), "CONN_MAX_AGE": dbconnmaxage, "TEST": { "NAME": "iSkyLIMS_test", diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 3baf9e5b..fce8e72d 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -24,7 +24,7 @@ services: APP_INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} APP_PORT: ${APP_APP_PORT:?APP_APP_PORT is required} PROJECT_MODULE: "iskylims" - DB_HOST: iskylims_app_db + DB_HOST: app_db DB_PORT: ${APP_DB_PORT:?APP_DB_PORT is required} DB_NAME: ${APP_DB_NAME:?APP_DB_NAME is required} DB_USER: ${APP_DB_USER:?APP_DB_USER is required} @@ -32,7 +32,7 @@ services: DJANGO_SECRET_KEY: ${APP_DJANGO_SECRET_KEY:?APP_DJANGO_SECRET_KEY is required} DJANGO_ALLOWED_HOSTS: ${APP_DJANGO_ALLOWED_HOSTS:?APP_DJANGO_ALLOWED_HOSTS is required} depends_on: - iskylims_app_db: + app_db: condition: service_healthy volumes: - app_test_documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:z @@ -83,7 +83,7 @@ services: - samba_test_data:/mnt:z networks: [deployment_net] # END BU-ISCIII ADDON: samba - iskylims_app_db: + app_db: image: docker.io/library/mysql:8.0 environment: MYSQL_DATABASE: ${APP_DB_NAME:?APP_DB_NAME is required} From a9dc0f6650bd73591a02f6805a3a961d763210ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Fri, 28 Aug 2026 14:03:43 +0200 Subject: [PATCH 53/55] Use scoped service names --- .bu-isciii-deployment/state.json | 22 ++++----- LEAME.md | 44 +++++++++--------- README.md | 44 +++++++++--------- container_install.sh | 72 +++++++++++++++--------------- deployment/project.json | 10 +++-- docker-compose.prod.yml | 62 +++++++++++++------------- docker-compose.test.yml | 76 ++++++++++++++++---------------- scripts/smoke_test.sh | 10 ++--- 8 files changed, 171 insertions(+), 169 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 7dcf98fd..5defbf3c 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -2,10 +2,10 @@ "config": { "ADDONS": { "apache": { - "CONFIG_SERVICE": "app" + "CONFIG_SERVICE": "relecov-iskylims" }, "samba": { - "CONFIG_SERVICE": "app", + "CONFIG_SERVICE": "relecov-iskylims", "MODES": [ "test" ] @@ -18,7 +18,7 @@ "PYTHON_VERSION": "3.11", "REPOSITORY_URL": "https://github.com/BU-ISCIII/iSkyLIMS.git", "SERVICES": { - "app": { + "relecov-iskylims": { "BUILD_CONTEXT": ".", "DOCKERFILE": "Dockerfile", "IMAGE": "relecov-iskylims:local", @@ -34,29 +34,29 @@ ".dockerignore": "bd9c115511d6b9dae318e4b6a85ddb282e60a46fd89c6cdecccdc20c668eb0e9", ".gitignore": "de487336ed72efd6601b985eda1c83754d6ee33b1cb4ce9f8973411caae833ed", "Dockerfile": "4747a22b4332ae6c6ffbd6d67f8fe92b80fb4a2ea9e102a6fca0eb68009cbef2", - "LEAME.md": "f6751e0ca0d8811349a2c5a9d9ec6d59b8b7cab18f3ce1441feb2a894dfb6b52", - "README.md": "5251291d6f543a49364069b6239f81e5528af10bc6a687d4a4702166ea20c71c", + "LEAME.md": "038ca4abd9ebda78c21c7533f9d626e0b759d15fcbd38d94c46968e31cf3b949", + "README.md": "94a3dd53e45686783e06d82516d3ac979f9f2dedfb1fac0a227a2c06b48c632e", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", "conf/apache/01-reverse-proxy.conf": "d5a0b6143fc4b0ba87f1f7f827e0d6c84dda1857e9697cd6acba4344beae375e", "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", "conf/apache/apache_production_settings.txt": "b070f6c82138f0772e52460e24868e4b48d38d412c9f78900a45e9bb00216506", "conf/apache/apache_test_settings.txt": "102c1b747def3ce56c5b977829eb07dddbca91c8c0012e36a130357851846554", - "conf/docker_production_settings.txt": "ba4059934c970cf4d334348203e3241121cd745b3cff0d6a4d78a01965e61c51", - "conf/docker_test_settings.txt": "44a7a32e588cd8779f3295523786cb421ea4fea8c0cd574d9d83cfc13d65d732", + "conf/docker_production_settings.txt": "9179c28b1b8e14dc78c869198332fcd95027babfa01f97137ee4c65e6b09b927", + "conf/docker_test_settings.txt": "6fb28bef94415425ef035c039c942e90fda367a35991d078d467247915311d2a", "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", "conf/template_settings.py": "af539ad10729a371e0c735c121e6e62dfece96afce77fb86e3a0820df2e9dd2f", - "container_install.sh": "44afb988f19b8e32edb9641a500f7efe4f533b71380e1647aa03bb7708a7cd2e", + "container_install.sh": "a1603eac3bcba6d2e1d84dc685cbeb003922737ed0d9b4d2183a9e1fa036e27d", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", - "docker-compose.prod.yml": "9feb8d66df5aee56822fbec2fa190ad151d17c3b838623211a41d42fb5480f3f", - "docker-compose.test.yml": "ad5b5f540757859812160d03b4fb05c1b2ab208cb9fa6f359a3ffb12ec6e2973", + "docker-compose.prod.yml": "d02262bb160de49c8f51deda1f771c9d89e2c5473a485d6229d1caf5ab4ad999", + "docker-compose.test.yml": "b4d01a68f8ff24dc350ff8ec6d4552067c814a805950f590d5de733890504420", "install.sh": "1a4f9bdd6d3082aa17798a360fa30c580c6391c78b2b5e9e75b49c9735168a0f", "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", - "scripts/smoke_test.sh": "04604071e36355a1cb2f6fda8c0161c2a09b393938c010264a7aac06f7d972ee" + "scripts/smoke_test.sh": "74d7b18bd8554758c72376f84d0028d580cddc26fd4113bcf1e4aa80078b1754" }, "source": "BU-ISCIII deployment standards", "standard_version": "0.1.0" diff --git a/LEAME.md b/LEAME.md index 381cb6ed..deb42c86 100644 --- a/LEAME.md +++ b/LEAME.md @@ -78,11 +78,11 @@ Persistencia declarada por el despliegue: | Activo | Ubicacion de produccion | Requisito de recuperacion | |---|---|---| -| `app` database | External production database | Database backup before migration | -| `app` documents | `app_documents` named volume | Volume backup | -| `app` static | `app_static` named volume | Replaceable through collectstatic | -| `app` logs | Host bind configured by `HOST_LOG_PATH` in `app_production_settings.txt` | Retain/rotate per institutional log policy | -| `app` rendered settings | Host bind configured by `DJANGO_SETTINGS_PATH` in `app_production_settings.txt` | Protected configuration backup | +| `relecov-iskylims` database | External production database | Database backup before migration | +| `relecov-iskylims` documents | `relecov-iskylims_documents` named volume | Volume backup | +| `relecov-iskylims` static | `relecov-iskylims_static` named volume | Replaceable through collectstatic | +| `relecov-iskylims` logs | Host bind configured by `HOST_LOG_PATH` in `app_production_settings.txt` | Retain/rotate per institutional log policy | +| `relecov-iskylims` rendered settings | Host bind configured by `DJANGO_SETTINGS_PATH` in `app_production_settings.txt` | Protected configuration backup | | Apache logs | `/var/log/local/relecov-iskylims/apache` host bind | Retain/rotate per institutional log policy | | Rendered Apache configuration | `deployment/apache/` in the deployment checkout | Rebuildable; preserve reviewed source configuration | | Samba test data | `samba_test_data` named volume | Disposable test/demo files | @@ -156,8 +156,8 @@ actual dentro de subshells; solo `install -d` usa privilegios. PODMAN_USER='' ( source deployment/settings/app_production_settings.txt - : "${HOST_LOG_PATH:?HOST_LOG_PATH is required for app}" - : "${DJANGO_SETTINGS_PATH:?DJANGO_SETTINGS_PATH is required for app}" + : "${HOST_LOG_PATH:?HOST_LOG_PATH is required for relecov-iskylims}" + : "${DJANGO_SETTINGS_PATH:?DJANGO_SETTINGS_PATH is required for relecov-iskylims}" sudo install -d -o "$PODMAN_USER" -g "$PODMAN_USER" \ "$HOST_LOG_PATH" "$(dirname "$DJANGO_SETTINGS_PATH")" ) @@ -175,7 +175,7 @@ instalador. No modificar `/srv/containers/storage/` manualmente. ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` ## Backup antes de actualizar @@ -234,7 +234,7 @@ Ejecutar el comando de instalación/upgrade: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt 2>&1 | tee "$(date +%Y%m%d_%H%M%S)_prod_install.log" + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt 2>&1 | tee "$(date +%Y%m%d_%H%M%S)_prod_install.log" ``` Durante `--action upgrade`, `container_install.sh`: @@ -263,7 +263,7 @@ bash scripts/smoke_test.sh --engine podman Completar las comprobaciones que corresponden a la topologia seleccionada: -- `app`: confirmar `/health/` y un flujo real de iSkyLIMS. +- `relecov-iskylims`: confirmar `/health/` y un flujo real de iSkyLIMS. - Apache: confirmar la URL publica, DNS/TLS, proxy y cabeceras reenviadas. - Samba: en los modos habilitados, confirmar acceso autenticado y un flujo de lectura/escritura desde un cliente aprobado. @@ -279,7 +279,7 @@ la revision anterior registrada y repetir las pruebas: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Si no son compatibles, detener escrituras y restaurar el punto completo: @@ -296,7 +296,7 @@ install -m 0600 "$BACKUP_DIR/app_production_settings.txt" deployment/settings/ap install -m 0600 "$BACKUP_DIR/apache_production_settings.txt" deployment/settings/apache_production_settings.txt install -m 0600 "$BACKUP_DIR/samba_production_settings.txt" deployment/settings/samba_production_settings.txt bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Restaurar todos los ficheros de ajustes protegidos y desplegar la revision @@ -321,7 +321,7 @@ Primera fase, incluso con los contenedores detenidos: ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Esta accion no construye imagenes, no migra la base de datos y no borra datos. @@ -331,7 +331,7 @@ Arrancar y repetirla para reparar tambien los volumenes montados: ```bash podman compose --env-file .env.production.file -f docker-compose.prod.yml up -d bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` ## Operaciones utiles @@ -344,25 +344,25 @@ podman compose --env-file .env.production.file -f docker-compose.prod.yml restar podman compose --env-file .env.production.file -f docker-compose.prod.yml down ``` -### Servicio Django `app` +### Servicio Django `relecov-iskylims` ```bash # Logs separados del servicio. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - logs --tail 200 app + logs --tail 200 relecov-iskylims # Entrar al contenedor. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec app bash + exec relecov-iskylims bash # Regenerar static sin ejecutar migraciones. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec app bash -lc \ + exec relecov-iskylims bash -lc \ 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py collectstatic --noinput' # Diagnostico previo a una recuperacion de bootstrap. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec app bash -lc \ + exec relecov-iskylims bash -lc \ 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check --deploy && python manage.py showmigrations --plan' ``` @@ -373,7 +373,7 @@ autoriza un bootstrap manual despues del backup: ```bash podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec app bash -lc \ + exec relecov-iskylims bash -lc \ 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py migrate --noinput && python manage.py collectstatic --noinput' ``` @@ -384,9 +384,9 @@ Registrar este procedimiento excepcional y ejecutar despues el smoke test. ```bash # Logs separados de Apache y validacion de configuracion. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - logs --tail 200 apache + logs --tail 200 relecov-iskylims-apache podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec apache httpd -t + exec relecov-iskylims-apache httpd -t # Estado restringido; usar valores del fichero protegido. APACHE_PORT='CHANGE_ME' diff --git a/README.md b/README.md index a902dcab..a0c8fc72 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ iSkyLIMS connects wet-lab and dry-lab activities in one traceable workflow: The supported deployment separates application runtime from institutional services and persistent data: -- The `app` service runs Django under Gunicorn as an unprivileged user. +- The `relecov-iskylims` service runs Django under Gunicorn as an unprivileged user. - The `apache` service is the public container reverse proxy and serves the shared static files. Production TLS may terminate there or at the institution's upstream proxy, according to the reviewed deployment. @@ -90,7 +90,7 @@ Services: | Service | Profile | Build context | Internal port | |---|---|---|---:| -| `app` | `django` | `.` | settings: `APP_PORT` | +| `relecov-iskylims` | `django` | `.` | settings: `APP_PORT` | - Django services build with an ephemeral settings secret, render protected host settings, and run controlled migration/bootstrap steps. @@ -200,7 +200,7 @@ Docker: ```bash bash container_install.sh --action install --engine docker \ --git_revision \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Podman: @@ -208,7 +208,7 @@ Podman: ```bash bash container_install.sh --action install --engine podman \ --git_revision \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` The installer creates `.env.production.file` for later direct Compose @@ -221,11 +221,11 @@ nor this generated environment file is copied into image layers. | Asset | Production location | Backup/rebuild policy | |---|---|---| -| `app` database | External production database | Database backup before migration | -| `app` documents | `app_documents` named volume | Volume backup | -| `app` static | `app_static` named volume | Replaceable through collectstatic | -| `app` logs | `/var/log/local/relecov-iskylims/apps` host bind | Retain/rotate per institutional log policy | -| `app` rendered settings | `/srv/containers/bind/relecov-iskylims/settings/` host bind | Protected configuration backup | +| `relecov-iskylims` database | External production database | Database backup before migration | +| `relecov-iskylims` documents | `relecov-iskylims_documents` named volume | Volume backup | +| `relecov-iskylims` static | `relecov-iskylims_static` named volume | Replaceable through collectstatic | +| `relecov-iskylims` logs | `/var/log/local/relecov-iskylims/apps` host bind | Retain/rotate per institutional log policy | +| `relecov-iskylims` rendered settings | `/srv/containers/bind/relecov-iskylims/settings/` host bind | Protected configuration backup | | Apache logs | `/var/log/local/relecov-iskylims/apache` host bind | Retain/rotate per institutional log policy | | Rendered Apache configuration | `deployment/apache/` in the deployment checkout | Rebuildable; preserve reviewed source configuration | | Samba test data | `samba_test_data` named volume | Disposable test/demo files | @@ -272,7 +272,7 @@ notes: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Replace `podman` with `docker` for a Docker-managed deployment. Stop on build, @@ -448,7 +448,7 @@ Compatible application-only rollback: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Full restore when schema or persistent-file formats are incompatible: @@ -470,7 +470,7 @@ install -m 0600 "$BACKUP_DIR/app_production_settings.txt" deployment/settings/ap install -m 0600 "$BACKUP_DIR/apache_production_settings.txt" deployment/settings/apache_production_settings.txt install -m 0600 "$BACKUP_DIR/samba_production_settings.txt" deployment/settings/samba_production_settings.txt bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Then deploy the revision recorded in `git-revision.txt`, start the deployment, @@ -487,7 +487,7 @@ volume at `/data` and extracting `/backup/documents.tar` there. ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` 5. Do not fake migrations, delete volumes, or rebuild from an unrecorded @@ -495,23 +495,23 @@ volume at `/data` and extracting `/backup/documents.tar` there. ### Service-specific operational commands -#### Django service `app` +#### Django service `relecov-iskylims` ```bash # Logs and an interactive shell (replace podman with docker when applicable). podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - logs --tail 200 app + logs --tail 200 relecov-iskylims podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec app bash + exec relecov-iskylims bash # Rebuild static assets without running migrations. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec app bash -lc \ + exec relecov-iskylims bash -lc \ 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py collectstatic --noinput' # Inspect Django and migration state before deciding whether to recover. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec app bash -lc \ + exec relecov-iskylims bash -lc \ 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check --deploy && python manage.py showmigrations --plan' ``` @@ -525,9 +525,9 @@ diagnostic last resort and must use the same backup and release procedure. ```bash podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - logs --tail 200 apache + logs --tail 200 relecov-iskylims-apache podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec apache httpd -t + exec relecov-iskylims-apache httpd -t APACHE_PORT='CHANGE_ME' SERVER_STATUS_SERVER_NAME='localhost' @@ -553,8 +553,8 @@ be replaced, move it to a timestamped backup instead of deleting evidence: sudo mv /var/log/local/relecov-iskylims/apache/modsec_debug.log \ /var/log/local/relecov-iskylims/apache/modsec_debug.log.blocked bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map app,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt -podman compose --env-file .env.production.file -f docker-compose.prod.yml restart apache + --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt +podman compose --env-file .env.production.file -f docker-compose.prod.yml restart relecov-iskylims-apache ``` ## Final configuration steps diff --git a/container_install.sh b/container_install.sh index 9f37ff0b..0f9d05b9 100755 --- a/container_install.sh +++ b/container_install.sh @@ -15,14 +15,14 @@ APPLICATION_NAME="iSkyLIMS" # Regenerate these callbacks from the descriptor; keep application-neutral # lifecycle mechanics below unchanged. # ============================================================================ -install_services=(app) +install_services=(relecov-iskylims) addon_build_services=() -permission_services=(app apache) -configured_services=(app apache samba) +permission_services=(relecov-iskylims relecov-iskylims-apache) +configured_services=(relecov-iskylims apache samba) default_service_install_conf() { case "$1" in - app) [ "$mode" = test ] && echo conf/docker_test_settings.txt || echo conf/docker_production_settings.txt ;; + relecov-iskylims) [ "$mode" = test ] && echo conf/docker_test_settings.txt || echo conf/docker_production_settings.txt ;; apache) [ "$mode" = test ] && echo conf/apache/apache_test_settings.txt || echo conf/apache/apache_production_settings.txt ;; samba) [ "$mode" = test ] && echo conf/samba/samba_test_settings.txt || echo conf/samba/samba_production_settings.txt ;; *) return 1 ;; @@ -30,7 +30,7 @@ default_service_install_conf() { } service_build_context_dir() { case "$1" in - app) echo . ;; + relecov-iskylims) echo . ;; *) return 1 ;; esac } @@ -60,31 +60,31 @@ service_install_path() { } service_readiness_path() { case "$1" in - app) echo "$(service_install_path "$1")/manage.py" ;; + relecov-iskylims) echo "$(service_install_path "$1")/manage.py" ;; *) return 1 ;; esac } service_image_name() { case "$1" in - app) echo relecov-iskylims:local ;; + relecov-iskylims) echo relecov-iskylims:local ;; *) return 1 ;; esac } service_profile() { case "$1" in - app) echo django ;; + relecov-iskylims) echo django ;; *) return 1 ;; esac } service_dockerfile() { case "$1" in - app) echo Dockerfile ;; + relecov-iskylims) echo Dockerfile ;; *) return 1 ;; esac } service_container_install_conf() { case "$1" in - app) echo conf/.runtime_install_settings.txt ;; + relecov-iskylims) echo conf/.runtime_install_settings.txt ;; *) return 1 ;; esac } @@ -97,13 +97,13 @@ service_gid() { prepare_compose_environment() { local -a settings_sources=( - "APP|${install_conf_host_by_service[app]}" + "RELECOV_ISKYLIMS|${install_conf_host_by_service[relecov-iskylims]}" "|${install_conf_host_by_service[apache]}" "|${install_conf_host_by_service[samba]}" ) local -a deployment_values=( "GIT_REVISION|$git_revision" - "APP_IMAGE|relecov-iskylims:local" + "RELECOV_ISKYLIMS_IMAGE|relecov-iskylims:local" ) compose_env_file="$script_dir/.env.${mode}.file" write_compose_environment_file "$compose_env_file" settings_sources deployment_values @@ -129,10 +129,10 @@ print_service_summary() { prepare_application_host_sources() { local settings_output if [ "$mode" = production ]; then - settings_output="$(service_environment_value app DJANGO_SETTINGS_PATH)" - [ -n "$settings_output" ] || { echo "DJANGO_SETTINGS_PATH is required for app" >&2; return 1; } + settings_output="$(service_environment_value relecov-iskylims DJANGO_SETTINGS_PATH)" + [ -n "$settings_output" ] || { echo "DJANGO_SETTINGS_PATH is required for relecov-iskylims" >&2; return 1; } mkdir -p "$(dirname "$settings_output")" - prepare_django_settings_bind_mount ./conf/template_settings.py "$settings_output" "${install_conf_host_by_service[app]}" + prepare_django_settings_bind_mount ./conf/template_settings.py "$settings_output" "${install_conf_host_by_service[relecov-iskylims]}" fi # conf/apache contains the application-owned Apache sources. Render every # deployment value only after the protected settings environment is loaded, @@ -140,7 +140,7 @@ prepare_application_host_sources() { local apache_source_dir="$script_dir/conf/apache" local apache_output_dir="$script_dir/deployment/apache" local apache_conf_name apache_config_service apache_log_path - apache_config_service=app + apache_config_service=relecov-iskylims [ -d "$apache_source_dir" ] || { echo "Apache source configuration directory not found: $apache_source_dir" >&2 return 1 @@ -180,17 +180,17 @@ prepare_application_host_sources() { prepare_host_bind_source_permissions() { [ "$mode" = production ] || return 0 local log_path settings_path uid gid - log_path="$(service_environment_value app HOST_LOG_PATH)" - settings_path="$(service_environment_value app DJANGO_SETTINGS_PATH)" - [ -n "$log_path" ] || { echo "HOST_LOG_PATH is required for app" >&2; return 1; } - [ -n "$settings_path" ] || { echo "DJANGO_SETTINGS_PATH is required for app" >&2; return 1; } - uid="$(service_uid app)"; gid="$(service_gid app)" - local -a app_host_bind_permission_spec=( + log_path="$(service_environment_value relecov-iskylims HOST_LOG_PATH)" + settings_path="$(service_environment_value relecov-iskylims DJANGO_SETTINGS_PATH)" + [ -n "$log_path" ] || { echo "HOST_LOG_PATH is required for relecov-iskylims" >&2; return 1; } + [ -n "$settings_path" ] || { echo "DJANGO_SETTINGS_PATH is required for relecov-iskylims" >&2; return 1; } + uid="$(service_uid relecov-iskylims)"; gid="$(service_gid relecov-iskylims)" + local -a relecov_iskylims_host_bind_permission_spec=( "$log_path|$uid:$gid|0775" "$(dirname "$settings_path")|-|0755" "$settings_path|$uid:$gid|0664" ) - apply_host_permission_spec "${app_host_bind_permission_spec[@]}" + apply_host_permission_spec "${relecov_iskylims_host_bind_permission_spec[@]}" # Generated proxy configuration is read-only in Apache. Its host files need # traversal/read permissions, while the production log bind must be writable. apache_log_path="${APACHE_LOG_PATH:?APACHE_LOG_PATH is required}" @@ -212,18 +212,18 @@ prepare_running_container_mount_permissions() { local service_name="$1" container_id="$2" local install_path uid gid case "$service_name" in - app) + relecov-iskylims) install_path="$(service_install_path "$service_name")" uid="$(service_uid "$service_name")"; gid="$(service_gid "$service_name")" - local -a app_running_mount_permission_spec=( + local -a relecov_iskylims_running_mount_permission_spec=( "$install_path/logs|$uid:$gid|u+rwX,g+rwX" "$install_path/documents|$uid:$gid|u+rwX,g+rwX" "$install_path/static|$uid:$gid|u+rwX,g+rwX,o+rX" ) - apply_container_directory_permission_spec "$container_id" "${app_running_mount_permission_spec[@]}" + apply_container_directory_permission_spec "$container_id" "${relecov_iskylims_running_mount_permission_spec[@]}" prepare_django_container_settings_permissions "$container_id" "$install_path/iskylims/settings.py" "$uid" "$gid" ;; - apache) + relecov-iskylims-apache) # Apache currently needs no ownership repair inside its running # container. Keep an explicit add-on policy ready for future mounts. local -a apache_running_mount_permission_spec=() @@ -238,7 +238,7 @@ bootstrap_service() { local repo_path runtime_conf uid gid status local -a args case "$service_name" in - app) + relecov-iskylims) repo_path="$(service_repo_path "$service_name")" # Fixed temporary in-container path; this is not operator configuration. runtime_conf=conf/.runtime_install_settings.txt @@ -264,17 +264,17 @@ bootstrap_service() { # layout here so the complete test installation remains readable in one file. application_supports_test_data=true load_test_deployment_data() { - local app_container app_install_path samba_container archive downloaded_archive + local relecov_iskylims_container relecov_iskylims_install_path samba_container archive downloaded_archive local admin_groups_code - app_container="$(current_service_container app)" \ - || die "Unable to resolve the app container for test-data loading" - app_install_path="$(service_install_path app)" + relecov_iskylims_container="$(current_service_container relecov-iskylims)" \ + || die "Unable to resolve the relecov-iskylims container for test-data loading" + relecov_iskylims_install_path="$(service_install_path relecov-iskylims)" if [ "$skip_test_data" = false ]; then echo "Loading iSkyLIMS test fixtures" - engine_exec exec -w "$app_install_path" "$app_container" \ - "$app_install_path/virtualenv/bin/python" manage.py \ + engine_exec exec -w "$relecov_iskylims_install_path" "$relecov_iskylims_container" \ + "$relecov_iskylims_install_path/virtualenv/bin/python" manage.py \ loaddata test/test_data.json admin_groups_code=$(cat <<'PY' from django.contrib.auth.models import Group, User @@ -287,8 +287,8 @@ admin.groups.add( print("admin groups:", list(admin.groups.values_list("name", flat=True))) PY ) - engine_exec exec -w "$app_install_path" "$app_container" \ - "$app_install_path/virtualenv/bin/python" manage.py \ + engine_exec exec -w "$relecov_iskylims_install_path" "$relecov_iskylims_container" \ + "$relecov_iskylims_install_path/virtualenv/bin/python" manage.py \ shell -c "$admin_groups_code" else echo "Skipping iSkyLIMS test fixtures as requested" diff --git a/deployment/project.json b/deployment/project.json index 6ce39fc7..002f740e 100644 --- a/deployment/project.json +++ b/deployment/project.json @@ -7,7 +7,7 @@ "PYTHON_VERSION": "3.11", "TIMEZONE": "Europe/Madrid", "SERVICES": { - "app": { + "relecov-iskylims": { "PROFILE": "django", "BUILD_CONTEXT": ".", "DOCKERFILE": "Dockerfile", @@ -19,11 +19,13 @@ }, "ADDONS": { "apache": { - "CONFIG_SERVICE": "app" + "CONFIG_SERVICE": "relecov-iskylims" }, "samba": { - "CONFIG_SERVICE": "app", - "MODES": ["test"] + "CONFIG_SERVICE": "relecov-iskylims", + "MODES": [ + "test" + ] } } } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index f5d345fc..31a9f021 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,7 +1,7 @@ services: - # BEGIN BU-ISCIII SERVICE: app (django) - app: - image: "${APP_IMAGE:-relecov-iskylims:local}" + # BEGIN BU-ISCIII SERVICE: relecov-iskylims (django) + relecov-iskylims: + image: "${RELECOV_ISKYLIMS_IMAGE:-relecov-iskylims:local}" build: context: "." dockerfile: "Dockerfile" @@ -11,51 +11,51 @@ services: INSTALL_CONF: "conf/.runtime_install_settings.txt" USE_INSTALL_CONF_SECRET: "true" RENDER_DJANGO_SETTINGS: "false" - APP_UID: ${APP_APP_UID:?APP_APP_UID is required} - APP_GID: ${APP_APP_GID:?APP_APP_GID is required} - APP_PORT: ${APP_APP_PORT:?APP_APP_PORT is required} - APP_REPO_PATH: ${APP_REPO_PATH:?APP_REPO_PATH is required} - APP_INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} + APP_UID: ${RELECOV_ISKYLIMS_APP_UID:?RELECOV_ISKYLIMS_APP_UID is required} + APP_GID: ${RELECOV_ISKYLIMS_APP_GID:?RELECOV_ISKYLIMS_APP_GID is required} + APP_PORT: ${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required} + APP_REPO_PATH: ${RELECOV_ISKYLIMS_REPO_PATH:?RELECOV_ISKYLIMS_REPO_PATH is required} + APP_INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} restart: unless-stopped - user: "${APP_APP_UID:?APP_APP_UID is required}:${APP_APP_GID:?APP_APP_GID is required}" + user: "${RELECOV_ISKYLIMS_APP_UID:?RELECOV_ISKYLIMS_APP_UID is required}:${RELECOV_ISKYLIMS_APP_GID:?RELECOV_ISKYLIMS_APP_GID is required}" environment: APP_MODE: prod - INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} - APP_INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} - APP_PORT: ${APP_APP_PORT:?APP_APP_PORT is required} + INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} + APP_INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} + APP_PORT: ${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required} PROJECT_MODULE: "iskylims" - DB_HOST: ${APP_DB_HOST:?APP_DB_HOST is required} - DB_PORT: ${APP_DB_PORT:?APP_DB_PORT is required} - DB_NAME: ${APP_DB_NAME:?APP_DB_NAME is required} - DB_USER: ${APP_DB_USER:?APP_DB_USER is required} - DB_PASSWORD: ${APP_DB_PASSWORD:?APP_DB_PASSWORD is required} - DJANGO_SECRET_KEY: ${APP_DJANGO_SECRET_KEY:?APP_DJANGO_SECRET_KEY is required} - DJANGO_ALLOWED_HOSTS: ${APP_DJANGO_ALLOWED_HOSTS:?APP_DJANGO_ALLOWED_HOSTS is required} + DB_HOST: ${RELECOV_ISKYLIMS_DB_HOST:?RELECOV_ISKYLIMS_DB_HOST is required} + DB_PORT: ${RELECOV_ISKYLIMS_DB_PORT:?RELECOV_ISKYLIMS_DB_PORT is required} + DB_NAME: ${RELECOV_ISKYLIMS_DB_NAME:?RELECOV_ISKYLIMS_DB_NAME is required} + DB_USER: ${RELECOV_ISKYLIMS_DB_USER:?RELECOV_ISKYLIMS_DB_USER is required} + DB_PASSWORD: ${RELECOV_ISKYLIMS_DB_PASSWORD:?RELECOV_ISKYLIMS_DB_PASSWORD is required} + DJANGO_SECRET_KEY: ${RELECOV_ISKYLIMS_DJANGO_SECRET_KEY:?RELECOV_ISKYLIMS_DJANGO_SECRET_KEY is required} + DJANGO_ALLOWED_HOSTS: ${RELECOV_ISKYLIMS_DJANGO_ALLOWED_HOSTS:?RELECOV_ISKYLIMS_DJANGO_ALLOWED_HOSTS is required} volumes: - - app_documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:z - - ${APP_HOST_LOG_PATH:?APP_HOST_LOG_PATH is required}:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/logs:Z - - ${APP_DJANGO_SETTINGS_PATH:?APP_DJANGO_SETTINGS_PATH is required}:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/iskylims/settings.py:Z - - app_static:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/static:z + - relecov-iskylims_documents:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/documents:z + - ${RELECOV_ISKYLIMS_HOST_LOG_PATH:?RELECOV_ISKYLIMS_HOST_LOG_PATH is required}:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/logs:Z + - ${RELECOV_ISKYLIMS_DJANGO_SETTINGS_PATH:?RELECOV_ISKYLIMS_DJANGO_SETTINGS_PATH is required}:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/iskylims/settings.py:Z + - relecov-iskylims_static:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/static:z healthcheck: test: ["CMD", "python", "-c", "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ['APP_PORT'] + '/health/', timeout=3)"] interval: 10s timeout: 5s retries: 12 ports: - - "127.0.0.1:${APP_APP_PORT:?APP_APP_PORT is required}:${APP_APP_PORT:?APP_APP_PORT is required}" + - "127.0.0.1:${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required}:${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required}" # Docker Engine on Linux needs this explicit mapping. Podman also supports # host-gateway and conventionally exposes host.docker.internal itself, so # host-based dependencies can use one hostname with either engine. extra_hosts: - "host.docker.internal:host-gateway" networks: [deployment_net] - # END BU-ISCIII SERVICE: app + # END BU-ISCIII SERVICE: relecov-iskylims # BEGIN BU-ISCIII ADDON: apache - apache: + relecov-iskylims-apache: image: registry.access.redhat.com/ubi9/httpd-24:latest restart: unless-stopped depends_on: - app: + relecov-iskylims: condition: service_healthy ports: # Public binding can be restricted to loopback when another host proxy @@ -81,8 +81,8 @@ services: - /usr/share/zoneinfo:/usr/share/zoneinfo:ro # GENERATED MULTI-APP MOUNTS: one read-only static/documents pair for # every Django service. React assets remain inside the React image. - - app_static:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/static:ro,z - - app_documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:ro,z + - relecov-iskylims_static:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/static:ro,z + - relecov-iskylims_documents:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/documents:ro,z # OPTIONAL APP MOUNTS belong in ADDONS.apache.MOUNTS. Examples: # - reports_data:/var/www/reports:ro,z # - /srv/public-downloads:/var/www/downloads:ro,Z @@ -93,5 +93,5 @@ networks: deployment_net: volumes: - app_documents: - app_static: + relecov-iskylims_documents: + relecov-iskylims_static: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index fce8e72d..f6c8e7d4 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,7 +1,7 @@ services: - # BEGIN BU-ISCIII SERVICE: app (django) - app: - image: "${APP_IMAGE:-relecov-iskylims:local}" + # BEGIN BU-ISCIII SERVICE: relecov-iskylims (django) + relecov-iskylims: + image: "${RELECOV_ISKYLIMS_IMAGE:-relecov-iskylims:local}" build: context: "." dockerfile: "Dockerfile" @@ -11,47 +11,47 @@ services: INSTALL_CONF: "conf/docker_test_settings.txt" USE_INSTALL_CONF_SECRET: "false" RENDER_DJANGO_SETTINGS: "true" - APP_UID: ${APP_APP_UID:?APP_APP_UID is required} - APP_GID: ${APP_APP_GID:?APP_APP_GID is required} - APP_PORT: ${APP_APP_PORT:?APP_APP_PORT is required} - APP_REPO_PATH: ${APP_REPO_PATH:?APP_REPO_PATH is required} - APP_INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} + APP_UID: ${RELECOV_ISKYLIMS_APP_UID:?RELECOV_ISKYLIMS_APP_UID is required} + APP_GID: ${RELECOV_ISKYLIMS_APP_GID:?RELECOV_ISKYLIMS_APP_GID is required} + APP_PORT: ${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required} + APP_REPO_PATH: ${RELECOV_ISKYLIMS_REPO_PATH:?RELECOV_ISKYLIMS_REPO_PATH is required} + APP_INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} restart: unless-stopped - user: "${APP_APP_UID:?APP_APP_UID is required}:${APP_APP_GID:?APP_APP_GID is required}" + user: "${RELECOV_ISKYLIMS_APP_UID:?RELECOV_ISKYLIMS_APP_UID is required}:${RELECOV_ISKYLIMS_APP_GID:?RELECOV_ISKYLIMS_APP_GID is required}" environment: APP_MODE: dev - INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} - APP_INSTALL_PATH: ${APP_INSTALL_PATH:?APP_INSTALL_PATH is required} - APP_PORT: ${APP_APP_PORT:?APP_APP_PORT is required} + INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} + APP_INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} + APP_PORT: ${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required} PROJECT_MODULE: "iskylims" - DB_HOST: app_db - DB_PORT: ${APP_DB_PORT:?APP_DB_PORT is required} - DB_NAME: ${APP_DB_NAME:?APP_DB_NAME is required} - DB_USER: ${APP_DB_USER:?APP_DB_USER is required} - DB_PASSWORD: ${APP_DB_PASSWORD:?APP_DB_PASSWORD is required} - DJANGO_SECRET_KEY: ${APP_DJANGO_SECRET_KEY:?APP_DJANGO_SECRET_KEY is required} - DJANGO_ALLOWED_HOSTS: ${APP_DJANGO_ALLOWED_HOSTS:?APP_DJANGO_ALLOWED_HOSTS is required} + DB_HOST: relecov-iskylims-db + DB_PORT: ${RELECOV_ISKYLIMS_DB_PORT:?RELECOV_ISKYLIMS_DB_PORT is required} + DB_NAME: ${RELECOV_ISKYLIMS_DB_NAME:?RELECOV_ISKYLIMS_DB_NAME is required} + DB_USER: ${RELECOV_ISKYLIMS_DB_USER:?RELECOV_ISKYLIMS_DB_USER is required} + DB_PASSWORD: ${RELECOV_ISKYLIMS_DB_PASSWORD:?RELECOV_ISKYLIMS_DB_PASSWORD is required} + DJANGO_SECRET_KEY: ${RELECOV_ISKYLIMS_DJANGO_SECRET_KEY:?RELECOV_ISKYLIMS_DJANGO_SECRET_KEY is required} + DJANGO_ALLOWED_HOSTS: ${RELECOV_ISKYLIMS_DJANGO_ALLOWED_HOSTS:?RELECOV_ISKYLIMS_DJANGO_ALLOWED_HOSTS is required} depends_on: - app_db: + relecov-iskylims-db: condition: service_healthy volumes: - - app_test_documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:z - - app_test_static:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/static:z + - relecov-iskylims_test_documents:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/documents:z + - relecov-iskylims_test_static:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/static:z healthcheck: test: ["CMD", "python", "-c", "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ['APP_PORT'] + '/health/', timeout=3)"] interval: 10s timeout: 5s retries: 12 ports: - - "0.0.0.0:${APP_APP_PORT:?APP_APP_PORT is required}:${APP_APP_PORT:?APP_APP_PORT is required}" + - "0.0.0.0:${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required}:${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required}" networks: [deployment_net] - # END BU-ISCIII SERVICE: app + # END BU-ISCIII SERVICE: relecov-iskylims # BEGIN BU-ISCIII ADDON: apache - apache: + relecov-iskylims-apache: image: registry.access.redhat.com/ubi9/httpd-24:latest restart: unless-stopped depends_on: - app: + relecov-iskylims: condition: service_healthy ports: - "${APACHE_BIND_HOST:?APACHE_BIND_HOST is required}:${APACHE_PORT:?APACHE_PORT is required}:8080" @@ -69,13 +69,13 @@ services: # Disposable log volume avoids writing test logs into production paths. - apache_test_logs:/var/log/httpd:z # GENERATED MULTI-APP MOUNTS: disposable test static/document volumes. - - app_test_static:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/static:ro,z - - app_test_documents:${APP_INSTALL_PATH:?APP_INSTALL_PATH is required}/documents:ro,z + - relecov-iskylims_test_static:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/static:ro,z + - relecov-iskylims_test_documents:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/documents:ro,z # OPTIONAL APP MOUNTS belong in ADDONS.apache.MOUNTS. networks: [deployment_net] # END BU-ISCIII ADDON: apache # BEGIN BU-ISCIII ADDON: samba (test only) - samba: + relecov-iskylims-samba: image: docker.io/dperson/samba:latest restart: unless-stopped command: '-S -s "ngs_data;/mnt/test_ngs_data;yes;yes;no;${SAMBA_USER:?SAMBA_USER is required};none;none;ngs data samba share" -u "${SAMBA_USER:?SAMBA_USER is required};${SAMBA_PASSWORD:?SAMBA_PASSWORD is required}" -p' @@ -83,27 +83,27 @@ services: - samba_test_data:/mnt:z networks: [deployment_net] # END BU-ISCIII ADDON: samba - app_db: + relecov-iskylims-db: image: docker.io/library/mysql:8.0 environment: - MYSQL_DATABASE: ${APP_DB_NAME:?APP_DB_NAME is required} - MYSQL_USER: ${APP_DB_USER:?APP_DB_USER is required} - MYSQL_PASSWORD: ${APP_DB_PASSWORD:?APP_DB_PASSWORD is required} - MYSQL_ROOT_PASSWORD: ${APP_DB_ROOT_PASSWORD:?APP_DB_ROOT_PASSWORD is required} + MYSQL_DATABASE: ${RELECOV_ISKYLIMS_DB_NAME:?RELECOV_ISKYLIMS_DB_NAME is required} + MYSQL_USER: ${RELECOV_ISKYLIMS_DB_USER:?RELECOV_ISKYLIMS_DB_USER is required} + MYSQL_PASSWORD: ${RELECOV_ISKYLIMS_DB_PASSWORD:?RELECOV_ISKYLIMS_DB_PASSWORD is required} + MYSQL_ROOT_PASSWORD: ${RELECOV_ISKYLIMS_DB_ROOT_PASSWORD:?RELECOV_ISKYLIMS_DB_ROOT_PASSWORD is required} healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"] interval: 5s timeout: 5s retries: 20 - volumes: [app_test_db:/var/lib/mysql] + volumes: [relecov-iskylims_test_db:/var/lib/mysql] networks: [deployment_net] networks: deployment_net: volumes: - app_test_db: - app_test_documents: - app_test_static: + relecov-iskylims_test_db: + relecov-iskylims_test_documents: + relecov-iskylims_test_static: apache_test_logs: samba_test_data: diff --git a/scripts/smoke_test.sh b/scripts/smoke_test.sh index 0a1fb5cb..26e3fb26 100755 --- a/scripts/smoke_test.sh +++ b/scripts/smoke_test.sh @@ -9,7 +9,7 @@ repo_root="$(cd "$script_dir/.." && pwd)" # shellcheck disable=SC1091 source "$repo_root/deployment/lib/container/common.sh" -install_services=(app) +install_services=(relecov-iskylims) engine="docker"; mode="production"; compose_file=""; env_file="" while (($#)); do case "$1" in @@ -35,11 +35,11 @@ fi compose_run() { compose_with_env_exec -f "$compose_file" "$@"; } fail() { echo "FAIL: $*" >&2; exit 1; } compose_run config >/dev/null - container_id="$(resolve_service_container app)" - [ -n "$container_id" ] || fail "Service app has no container" - ensure_service_running app "$container_id" >/dev/null + container_id="$(resolve_service_container relecov-iskylims)" + [ -n "$container_id" ] || fail "Service relecov-iskylims has no container" + ensure_service_running relecov-iskylims "$container_id" >/dev/null engine_exec exec "$container_id" bash -lc 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check && ! python manage.py showmigrations --plan | grep -F '"'"'[ ]'"'"'' - echo "PASS: app Django checks and migrations" + echo "PASS: relecov-iskylims Django checks and migrations" check_url() { local service="$1" url="$2" curl --fail --silent --show-error --location --max-time 20 --output /dev/null "$url" \ From 79da4e1915d10323a09ba32acdfc58be0bc55f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 22 Sep 2026 12:32:01 +0200 Subject: [PATCH 54/55] Use standalone iSkyLIMS deployment identity --- .bu-isciii-deployment/state.json | 38 ++++----- Dockerfile | 4 +- LEAME.md | 90 +++++++++++----------- README.md | 84 ++++++++++---------- conf/apache/01-reverse-proxy.conf | 2 +- conf/apache/apache_production_settings.txt | 4 +- conf/apache/apache_test_settings.txt | 2 +- conf/docker_production_settings.txt | 16 ++-- conf/docker_test_settings.txt | 10 +-- container_install.sh | 77 +++++++++++------- deployment/project.json | 10 +-- docker-compose.prod.yml | 62 +++++++-------- docker-compose.test.yml | 76 +++++++++--------- scripts/container_start.sh | 4 +- scripts/smoke_test.sh | 10 +-- 15 files changed, 258 insertions(+), 231 deletions(-) diff --git a/.bu-isciii-deployment/state.json b/.bu-isciii-deployment/state.json index 5defbf3c..10abcca6 100644 --- a/.bu-isciii-deployment/state.json +++ b/.bu-isciii-deployment/state.json @@ -2,26 +2,26 @@ "config": { "ADDONS": { "apache": { - "CONFIG_SERVICE": "relecov-iskylims" + "CONFIG_SERVICE": "iskylims" }, "samba": { - "CONFIG_SERVICE": "relecov-iskylims", + "CONFIG_SERVICE": "iskylims", "MODES": [ "test" ] } }, "APP_NAME": "iSkyLIMS", - "APP_SLUG": "relecov-iskylims", + "APP_SLUG": "iskylims", "DEFAULT_BRANCH": "main", "DESCRIPTION": "Laboratory information management system for wet-lab and dry-lab sequencing workflows.", "PYTHON_VERSION": "3.11", "REPOSITORY_URL": "https://github.com/BU-ISCIII/iSkyLIMS.git", "SERVICES": { - "relecov-iskylims": { + "iskylims": { "BUILD_CONTEXT": ".", "DOCKERFILE": "Dockerfile", - "IMAGE": "relecov-iskylims:local", + "IMAGE": "iskylims:local", "INSTALL_CONF": "conf/docker_production_settings.txt", "PROFILE": "django", "PROJECT_MODULE": "iskylims", @@ -33,30 +33,30 @@ "files": { ".dockerignore": "bd9c115511d6b9dae318e4b6a85ddb282e60a46fd89c6cdecccdc20c668eb0e9", ".gitignore": "de487336ed72efd6601b985eda1c83754d6ee33b1cb4ce9f8973411caae833ed", - "Dockerfile": "4747a22b4332ae6c6ffbd6d67f8fe92b80fb4a2ea9e102a6fca0eb68009cbef2", - "LEAME.md": "038ca4abd9ebda78c21c7533f9d626e0b759d15fcbd38d94c46968e31cf3b949", - "README.md": "94a3dd53e45686783e06d82516d3ac979f9f2dedfb1fac0a227a2c06b48c632e", + "Dockerfile": "95d77c59cec771592957755e699c19401f70e631eb42bd6a9ac73c2024786de1", + "LEAME.md": "9c7353110884db0a9e48b92d35c3ed8813b83e331d5ae2ab75a70f2878bd4811", + "README.md": "f8808b6ee80aa7a942dd505463f155b2314bd70f948d74ce6e3f9fad938d8c22", "conf/INSTALL_SETTINGS.md": "f5b1437a0e13a803ff020b5cb3aa1476b6876c169b23e6bcfc3e5042224af636", "conf/apache/00-logs.conf": "3c2ab4e693e528d2cad37c0c5bb0bc4511803a7f206b339182de192d4e398092", - "conf/apache/01-reverse-proxy.conf": "d5a0b6143fc4b0ba87f1f7f827e0d6c84dda1857e9697cd6acba4344beae375e", + "conf/apache/01-reverse-proxy.conf": "1c93e81d44ef88091c4ec7872d9220128813ba5111908ea70b22996182ffd5be", "conf/apache/02-server-status.conf": "c3b5d26f9b551083faf0d85b188bfa9c577ff0bd3b805f0ec104fe4f7d5101cb", - "conf/apache/apache_production_settings.txt": "b070f6c82138f0772e52460e24868e4b48d38d412c9f78900a45e9bb00216506", - "conf/apache/apache_test_settings.txt": "102c1b747def3ce56c5b977829eb07dddbca91c8c0012e36a130357851846554", - "conf/docker_production_settings.txt": "9179c28b1b8e14dc78c869198332fcd95027babfa01f97137ee4c65e6b09b927", - "conf/docker_test_settings.txt": "6fb28bef94415425ef035c039c942e90fda367a35991d078d467247915311d2a", + "conf/apache/apache_production_settings.txt": "169c2d22d4933090003875da7359c49926b9dd4be0838a7875744723c9cf771a", + "conf/apache/apache_test_settings.txt": "8092b49e5b3950a9e6ef64f6fc9d004d51e487cfdd4c17c71bcfa3469494e0b6", + "conf/docker_production_settings.txt": "51182733d5cd3b12d826cd085a804e310c9a89bced71d7396cbef59df2801c82", + "conf/docker_test_settings.txt": "c1d910b42629559304ee6a750d6f37a5ed322ca9efa7a9cfffd373de7a75cae6", "conf/samba/samba_production_settings.txt": "f19da30ab0890a79254e22c571629b34de2d37ad9c12449607158bff9a9188a1", "conf/samba/samba_test_settings.txt": "c383b88d43de8d65886cdd2b45bf9f27d91f5f3fe290f8f82378578fbeeff0b2", "conf/template_settings.py": "af539ad10729a371e0c735c121e6e62dfece96afce77fb86e3a0820df2e9dd2f", - "container_install.sh": "a1603eac3bcba6d2e1d84dc685cbeb003922737ed0d9b4d2183a9e1fa036e27d", + "container_install.sh": "82500f1e800e1a5dbd3429b14f1c3bae7f3554f988408b5f7939478afde45abd", "deployment_health/README.md": "3d7c4295938708813369deeac9edc0eb8fe2153c77d03f9138febf3aed715aa5", "deployment_health/__init__.py": "bedcff132309f8970b1ca72fcea20c7cb38bb727d48f82d96dbf7104922abda0", "deployment_health/urls.py": "8d274b44d326f26d71b05021a34969c742478efbe47691aadd8c3a90ba16f340", "deployment_health/views.py": "d19cc3e8fef8519bae2b7fbdc25d17f6ae7df02479aa8890f4b4d250bb07f2e4", - "docker-compose.prod.yml": "d02262bb160de49c8f51deda1f771c9d89e2c5473a485d6229d1caf5ab4ad999", - "docker-compose.test.yml": "b4d01a68f8ff24dc350ff8ec6d4552067c814a805950f590d5de733890504420", - "install.sh": "1a4f9bdd6d3082aa17798a360fa30c580c6391c78b2b5e9e75b49c9735168a0f", - "scripts/container_start.sh": "14646aeebd73aa540a64a473b15ab46270c4c81017dc16de0f032039ea3012db", - "scripts/smoke_test.sh": "74d7b18bd8554758c72376f84d0028d580cddc26fd4113bcf1e4aa80078b1754" + "docker-compose.prod.yml": "3408e0e424a06587f61e9e04d46e4225bdfab473206bd28a1f18d2d014b2d9dc", + "docker-compose.test.yml": "a56a89a26ae8e30627e401405e273d99cfeb24b8e508f24204882d57ba79f51d", + "install.sh": "6cedd3a990de21bf2343b75d8f93dd37420c1b358a1529b5e50892cf6df1526c", + "scripts/container_start.sh": "8d6e24f848d0562c17f323bbcf8d49e89a247648895bc209c4eb79bc9c8adf12", + "scripts/smoke_test.sh": "24aabe5c89c2ede2d373d282f1c3d17ed4a19f3c8e73b07a148e8472869bf17f" }, "source": "BU-ISCIII deployment standards", "standard_version": "0.1.0" diff --git a/Dockerfile b/Dockerfile index 5c62b489..8f323f0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,8 @@ FROM registry.access.redhat.com/ubi9/ubi-minimal ARG APP_UID=1212 ARG APP_GID=1212 ARG APP_PORT=8001 -ARG APP_REPO_PATH=/srv/relecov-iskylims -ARG APP_INSTALL_PATH=/opt/relecov-iskylims +ARG APP_REPO_PATH=/srv/iskylims +ARG APP_INSTALL_PATH=/opt/iskylims ARG GIT_REVISION=current ARG INSTALL_CONF=conf/docker_test_settings.txt ARG USE_INSTALL_CONF_SECRET=false diff --git a/LEAME.md b/LEAME.md index deb42c86..5f60e413 100644 --- a/LEAME.md +++ b/LEAME.md @@ -54,13 +54,13 @@ manualmente. ```text /opt/containers_apps/ -└── relecov-iskylims/ +└── iskylims/ ├── backup/ # Backups locales opcionales - └── relecov-iskylims/ # Clone Git y configuracion protegida + └── iskylims/ # Clone Git y configuracion protegida /srv/containers/ ├── backup/ -│ └── relecov-iskylims/ # Backup central recomendado +│ └── iskylims/ # Backup central recomendado ├── bind/ │ └── relecov-iskylims/ │ └── settings/ # settings.py renderizado por servicio @@ -78,12 +78,12 @@ Persistencia declarada por el despliegue: | Activo | Ubicacion de produccion | Requisito de recuperacion | |---|---|---| -| `relecov-iskylims` database | External production database | Database backup before migration | -| `relecov-iskylims` documents | `relecov-iskylims_documents` named volume | Volume backup | -| `relecov-iskylims` static | `relecov-iskylims_static` named volume | Replaceable through collectstatic | -| `relecov-iskylims` logs | Host bind configured by `HOST_LOG_PATH` in `app_production_settings.txt` | Retain/rotate per institutional log policy | -| `relecov-iskylims` rendered settings | Host bind configured by `DJANGO_SETTINGS_PATH` in `app_production_settings.txt` | Protected configuration backup | -| Apache logs | `/var/log/local/relecov-iskylims/apache` host bind | Retain/rotate per institutional log policy | +| `iskylims` database | External production database | Database backup before migration | +| `iskylims` documents | `iskylims_documents` named volume | Volume backup | +| `iskylims` static | `iskylims_static` named volume | Replaceable through collectstatic | +| `iskylims` logs | Host bind configured by `HOST_LOG_PATH` in `iskylims_production_settings.txt` | Retain/rotate per institutional log policy | +| `iskylims` rendered settings | Host bind configured by `DJANGO_SETTINGS_PATH` in `iskylims_production_settings.txt` | Protected configuration backup | +| Apache logs | `/var/log/local/iskylims/apache` host bind | Retain/rotate per institutional log policy | | Rendered Apache configuration | `deployment/apache/` in the deployment checkout | Rebuildable; preserve reviewed source configuration | | Samba test data | `samba_test_data` named volume | Disposable test/demo files | @@ -95,11 +95,11 @@ instalador; normalmente es la cuenta de la sesion actual. Los binds y logs se crean despues de completar los ajustes protegidos. ```bash -sudo mkdir -p /opt/containers_apps/relecov-iskylims -sudo mkdir -p /srv/containers/backup/relecov-iskylims +sudo mkdir -p /opt/containers_apps/iskylims +sudo mkdir -p /srv/containers/backup/iskylims sudo chown -R : \ - /opt/containers_apps/relecov-iskylims \ - /srv/containers/backup/relecov-iskylims + /opt/containers_apps/iskylims \ + /srv/containers/backup/iskylims ``` ## Actualizar codigo @@ -107,9 +107,9 @@ sudo chown -R : \ Para un checkout nuevo: ```bash -cd /opt/containers_apps/relecov-iskylims -git clone https://github.com/BU-ISCIII/iSkyLIMS.git relecov-iskylims -cd relecov-iskylims +cd /opt/containers_apps/iskylims +git clone https://github.com/BU-ISCIII/iSkyLIMS.git iskylims +cd iskylims git checkout ``` @@ -129,7 +129,7 @@ copia en las capas de las imagenes. ```bash install -d -m 0700 deployment/settings -install -m 0600 conf/docker_production_settings.txt deployment/settings/app_production_settings.txt +install -m 0600 conf/docker_production_settings.txt deployment/settings/iskylims_production_settings.txt install -m 0600 conf/apache/apache_production_settings.txt deployment/settings/apache_production_settings.txt install -m 0600 conf/samba/samba_production_settings.txt deployment/settings/samba_production_settings.txt ``` @@ -155,9 +155,9 @@ actual dentro de subshells; solo `install -d` usa privilegios. ```bash PODMAN_USER='' ( - source deployment/settings/app_production_settings.txt - : "${HOST_LOG_PATH:?HOST_LOG_PATH is required for relecov-iskylims}" - : "${DJANGO_SETTINGS_PATH:?DJANGO_SETTINGS_PATH is required for relecov-iskylims}" + source deployment/settings/iskylims_production_settings.txt + : "${HOST_LOG_PATH:?HOST_LOG_PATH is required for iskylims}" + : "${DJANGO_SETTINGS_PATH:?DJANGO_SETTINGS_PATH is required for iskylims}" sudo install -d -o "$PODMAN_USER" -g "$PODMAN_USER" \ "$HOST_LOG_PATH" "$(dirname "$DJANGO_SETTINGS_PATH")" ) @@ -175,7 +175,7 @@ instalador. No modificar `/srv/containers/storage/` manualmente. ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` ## Backup antes de actualizar @@ -183,13 +183,13 @@ bash container_install.sh --action fix-permissions --engine podman \ Crear un directorio identificado y registrar el estado desplegado: ```bash -BACKUP_DIR="/srv/containers/backup/relecov-iskylims/$(date +%Y%m%d_%H%M%S)" +BACKUP_DIR="/srv/containers/backup/iskylims/$(date +%Y%m%d_%H%M%S)" mkdir -p "$BACKUP_DIR" git rev-parse HEAD > "$BACKUP_DIR/git-revision.txt" podman compose --env-file .env.production.file -f docker-compose.prod.yml \ images > "$BACKUP_DIR/images.txt" cp .env.production.file "$BACKUP_DIR/" -cp deployment/settings/app_production_settings.txt "$BACKUP_DIR/" +cp deployment/settings/iskylims_production_settings.txt "$BACKUP_DIR/" cp deployment/settings/apache_production_settings.txt "$BACKUP_DIR/" cp deployment/settings/samba_production_settings.txt "$BACKUP_DIR/" chmod -R go-rwx "$BACKUP_DIR" @@ -206,7 +206,7 @@ mysqldump --single-transaction --routines --triggers \ Localizar y exportar cada volumen no reconstruible declarado en la tabla: ```bash -podman volume ls | grep 'relecov-iskylims' +podman volume ls | grep 'iskylims' podman volume export > "$BACKUP_DIR/documents.tar" podman volume export > "$BACKUP_DIR/static.tar" ``` @@ -219,8 +219,8 @@ Guardar tambien los bind mounts persistentes. Los logs se conservan segun su politica de retencion; la configuracion protegida debe incluirse siempre. ```bash -tar -C /srv/containers/bind -czf "$BACKUP_DIR/bind-mounts.tar.gz" relecov-iskylims -tar -C /var/log/local -czf "$BACKUP_DIR/logs.tar.gz" relecov-iskylims +tar -C /srv/containers/bind -czf "$BACKUP_DIR/bind-mounts.tar.gz" iskylims +tar -C /var/log/local -czf "$BACKUP_DIR/logs.tar.gz" iskylims sha256sum "$BACKUP_DIR"/* > "$BACKUP_DIR/SHA256SUMS" ``` @@ -234,7 +234,7 @@ Ejecutar el comando de instalación/upgrade: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt 2>&1 | tee "$(date +%Y%m%d_%H%M%S)_prod_install.log" + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt 2>&1 | tee "$(date +%Y%m%d_%H%M%S)_prod_install.log" ``` Durante `--action upgrade`, `container_install.sh`: @@ -263,10 +263,9 @@ bash scripts/smoke_test.sh --engine podman Completar las comprobaciones que corresponden a la topologia seleccionada: -- `relecov-iskylims`: confirmar `/health/` y un flujo real de iSkyLIMS. -- Apache: confirmar la URL publica, DNS/TLS, proxy y cabeceras reenviadas. -- Samba: en los modos habilitados, confirmar acceso autenticado y un flujo de - lectura/escritura desde un cliente aprobado. +- `iskylims`: confirmar su endpoint `/health/` y un flujo representativo de lectura. +- Apache: confirmar la URL publica registrada, DNS/TLS, proxy, cabeceras reenviadas y el endpoint restringido de server-status. +- Samba: en cada modo habilitado, confirmar acceso autenticado y un flujo representativo de lectura/escritura desde un cliente aprobado. Verificar tambien correo y tareas programadas. Registrar URL y resultados junto con estado, imagenes y revision desplegada. @@ -279,7 +278,7 @@ la revision anterior registrada y repetir las pruebas: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Si no son compatibles, detener escrituras y restaurar el punto completo: @@ -292,11 +291,12 @@ podman volume import "$BACKUP_DIR/documents.tar" podman volume import "$BACKUP_DIR/static.tar" tar -C /srv/containers/bind -xzf "$BACKUP_DIR/bind-mounts.tar.gz" install -d -m 0700 deployment/settings -install -m 0600 "$BACKUP_DIR/app_production_settings.txt" deployment/settings/app_production_settings.txt +install -m 0600 "$BACKUP_DIR/iskylims_production_settings.txt" deployment/settings/iskylims_production_settings.txt install -m 0600 "$BACKUP_DIR/apache_production_settings.txt" deployment/settings/apache_production_settings.txt install -m 0600 "$BACKUP_DIR/samba_production_settings.txt" deployment/settings/samba_production_settings.txt bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + ``` Restaurar todos los ficheros de ajustes protegidos y desplegar la revision @@ -321,7 +321,7 @@ Primera fase, incluso con los contenedores detenidos: ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Esta accion no construye imagenes, no migra la base de datos y no borra datos. @@ -331,7 +331,7 @@ Arrancar y repetirla para reparar tambien los volumenes montados: ```bash podman compose --env-file .env.production.file -f docker-compose.prod.yml up -d bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` ## Operaciones utiles @@ -344,25 +344,25 @@ podman compose --env-file .env.production.file -f docker-compose.prod.yml restar podman compose --env-file .env.production.file -f docker-compose.prod.yml down ``` -### Servicio Django `relecov-iskylims` +### Servicio Django `iskylims` ```bash # Logs separados del servicio. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - logs --tail 200 relecov-iskylims + logs --tail 200 iskylims # Entrar al contenedor. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec relecov-iskylims bash + exec iskylims bash # Regenerar static sin ejecutar migraciones. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec relecov-iskylims bash -lc \ + exec iskylims bash -lc \ 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py collectstatic --noinput' # Diagnostico previo a una recuperacion de bootstrap. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec relecov-iskylims bash -lc \ + exec iskylims bash -lc \ 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check --deploy && python manage.py showmigrations --plan' ``` @@ -373,7 +373,7 @@ autoriza un bootstrap manual despues del backup: ```bash podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec relecov-iskylims bash -lc \ + exec iskylims bash -lc \ 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py migrate --noinput && python manage.py collectstatic --noinput' ``` @@ -384,9 +384,9 @@ Registrar este procedimiento excepcional y ejecutar despues el smoke test. ```bash # Logs separados de Apache y validacion de configuracion. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - logs --tail 200 relecov-iskylims-apache + logs --tail 200 iskylims-apache podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec relecov-iskylims-apache httpd -t + exec iskylims-apache httpd -t # Estado restringido; usar valores del fichero protegido. APACHE_PORT='CHANGE_ME' @@ -400,7 +400,7 @@ Para diagnosticos SELinux y ModSecurity, comprobar el bind de logs antes de reiniciar: ```bash -ls -ldZ /var/log/local/relecov-iskylims/apache +ls -ldZ /var/log/local/iskylims/apache ``` Si aparece `ModSecurity: Failed to open debug log file`, conservar el fichero diff --git a/README.md b/README.md index a0c8fc72..f44b608d 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,8 @@ plus the reviewed Django bare-metal procedure. For issues, use the ## Get the code (required) ```bash -git clone https://github.com/BU-ISCIII/iSkyLIMS.git relecov-iskylims -cd relecov-iskylims +git clone https://github.com/BU-ISCIII/iSkyLIMS.git iskylims +cd iskylims ``` For an orchestrated deployment, every external build context in the service @@ -90,7 +90,7 @@ Services: | Service | Profile | Build context | Internal port | |---|---|---|---:| -| `relecov-iskylims` | `django` | `.` | settings: `APP_PORT` | +| `iskylims` | `django` | `.` | settings: `APP_PORT` | - Django services build with an ephemeral settings secret, render protected host settings, and run controlled migration/bootstrap steps. @@ -118,7 +118,7 @@ template used by this deployment: ```bash install -d -m 0700 deployment/settings -install -m 0600 conf/docker_production_settings.txt deployment/settings/app_production_settings.txt +install -m 0600 conf/docker_production_settings.txt deployment/settings/iskylims_production_settings.txt install -m 0600 conf/apache/apache_production_settings.txt deployment/settings/apache_production_settings.txt install -m 0600 conf/samba/samba_production_settings.txt deployment/settings/samba_production_settings.txt ``` @@ -200,7 +200,7 @@ Docker: ```bash bash container_install.sh --action install --engine docker \ --git_revision \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Podman: @@ -208,7 +208,7 @@ Podman: ```bash bash container_install.sh --action install --engine podman \ --git_revision \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` The installer creates `.env.production.file` for later direct Compose @@ -221,17 +221,17 @@ nor this generated environment file is copied into image layers. | Asset | Production location | Backup/rebuild policy | |---|---|---| -| `relecov-iskylims` database | External production database | Database backup before migration | -| `relecov-iskylims` documents | `relecov-iskylims_documents` named volume | Volume backup | -| `relecov-iskylims` static | `relecov-iskylims_static` named volume | Replaceable through collectstatic | -| `relecov-iskylims` logs | `/var/log/local/relecov-iskylims/apps` host bind | Retain/rotate per institutional log policy | -| `relecov-iskylims` rendered settings | `/srv/containers/bind/relecov-iskylims/settings/` host bind | Protected configuration backup | -| Apache logs | `/var/log/local/relecov-iskylims/apache` host bind | Retain/rotate per institutional log policy | +| `iskylims` database | External production database | Database backup before migration | +| `iskylims` documents | `iskylims_documents` named volume | Volume backup | +| `iskylims` static | `iskylims_static` named volume | Replaceable through collectstatic | +| `iskylims` logs | Host bind configured by `HOST_LOG_PATH` in `iskylims_production_settings.txt` | Retain/rotate per institutional log policy | +| `iskylims` rendered settings | Host bind configured by `DJANGO_SETTINGS_PATH` in `iskylims_production_settings.txt` | Protected configuration backup | +| Apache logs | `/var/log/local/iskylims/apache` host bind | Retain/rotate per institutional log policy | | Rendered Apache configuration | `deployment/apache/` in the deployment checkout | Rebuildable; preserve reviewed source configuration | | Samba test data | `samba_test_data` named volume | Disposable test/demo files | -The standard fixes application binds below `/srv/containers/bind/relecov-iskylims` -and logs below `/var/log/local/relecov-iskylims`. The operator must still record the +The standard fixes application binds below `/srv/containers/bind/iskylims` +and logs below `/var/log/local/iskylims`. The operator must still record the backup owner, retention, actual engine volume names, and restore-test evidence for every non-rebuildable asset. Never treat a container writable layer as persistent storage. @@ -272,7 +272,7 @@ notes: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Replace `podman` with `docker` for a Docker-managed deployment. Stop on build, @@ -309,11 +309,11 @@ after the application developer documents and tests those integrations. ```bash # Stage application files and dependencies. bash install.sh --stage install --git_revision current \ - --conf deployment/settings/app_production_settings.txt + --conf deployment/settings/iskylims_production_settings.txt # Bootstrap the prepared runtime (settings, migrations and static files). bash install.sh --bootstrap install \ - --conf deployment/settings/app_production_settings.txt + --conf deployment/settings/iskylims_production_settings.txt ``` For upgrades, take a backup and replace both `install` actions with `upgrade`. @@ -328,9 +328,9 @@ paths, runtime user, TLS ownership, and log locations. Ubuntu/Debian baseline: ```bash -sudo cp /etc/apache2/sites-available/relecov-iskylims.conf +sudo cp /etc/apache2/sites-available/iskylims.conf sudo a2enmod proxy proxy_http headers -sudo a2ensite relecov-iskylims.conf +sudo a2ensite iskylims.conf sudo apache2ctl configtest sudo systemctl reload apache2 ``` @@ -338,7 +338,7 @@ sudo systemctl reload apache2 CentOS/RHEL baseline: ```bash -sudo cp /etc/httpd/conf.d/relecov-iskylims.conf +sudo cp /etc/httpd/conf.d/iskylims.conf sudo httpd -t sudo systemctl reload httpd ``` @@ -399,7 +399,7 @@ recovery point before installation or upgrade. Record the revision, image IDs, settings files, and backup identifiers. ```bash -BACKUP_DIR="/srv/containers/backup/relecov-iskylims/$(date +%Y%m%d_%H%M%S)" +BACKUP_DIR="/srv/containers/backup/iskylims/$(date +%Y%m%d_%H%M%S)" DOCUMENTS_VOLUME='CHANGE_ME' DB_HOST='CHANGE_ME' DB_PORT='3306' @@ -408,7 +408,7 @@ DB_USER='CHANGE_ME' mkdir -p "$BACKUP_DIR" git rev-parse HEAD > "$BACKUP_DIR/git-revision.txt" cp .env.production.file "$BACKUP_DIR/" -cp deployment/settings/app_production_settings.txt "$BACKUP_DIR/" +cp deployment/settings/iskylims_production_settings.txt "$BACKUP_DIR/" cp deployment/settings/apache_production_settings.txt "$BACKUP_DIR/" cp deployment/settings/samba_production_settings.txt "$BACKUP_DIR/" chmod -R go-rwx "$BACKUP_DIR" @@ -417,9 +417,10 @@ mysqldump --single-transaction --routines --triggers \ --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" --password \ "$DB_NAME" > "$BACKUP_DIR/database.sql" -podman volume ls | grep 'relecov-iskylims' +podman volume ls | grep 'iskylims' podman volume export "$DOCUMENTS_VOLUME" > "$BACKUP_DIR/documents.tar" -tar -C /srv/containers/bind -czf "$BACKUP_DIR/bind-mounts.tar.gz" relecov-iskylims + +tar -C /srv/containers/bind -czf "$BACKUP_DIR/bind-mounts.tar.gz" iskylims sha256sum "$BACKUP_DIR"/* > "$BACKUP_DIR/SHA256SUMS" ``` @@ -448,13 +449,13 @@ Compatible application-only rollback: ```bash bash container_install.sh --action upgrade --engine podman \ --git_revision \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` Full restore when schema or persistent-file formats are incompatible: ```bash -BACKUP_DIR='/srv/containers/backup/relecov-iskylims/CHANGE_ME' +BACKUP_DIR='/srv/containers/backup/iskylims/CHANGE_ME' DOCUMENTS_VOLUME='CHANGE_ME' DB_HOST='CHANGE_ME' DB_PORT='3306' @@ -466,11 +467,12 @@ mysql --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" --password \ podman volume import "$DOCUMENTS_VOLUME" "$BACKUP_DIR/documents.tar" tar -C /srv/containers/bind -xzf "$BACKUP_DIR/bind-mounts.tar.gz" install -d -m 0700 deployment/settings -install -m 0600 "$BACKUP_DIR/app_production_settings.txt" deployment/settings/app_production_settings.txt +install -m 0600 "$BACKUP_DIR/iskylims_production_settings.txt" deployment/settings/iskylims_production_settings.txt install -m 0600 "$BACKUP_DIR/apache_production_settings.txt" deployment/settings/apache_production_settings.txt install -m 0600 "$BACKUP_DIR/samba_production_settings.txt" deployment/settings/samba_production_settings.txt bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + ``` Then deploy the revision recorded in `git-revision.txt`, start the deployment, @@ -487,7 +489,7 @@ volume at `/data` and extracting `/backup/documents.tar` there. ```bash bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt ``` 5. Do not fake migrations, delete volumes, or rebuild from an unrecorded @@ -495,23 +497,23 @@ volume at `/data` and extracting `/backup/documents.tar` there. ### Service-specific operational commands -#### Django service `relecov-iskylims` +#### Django service `iskylims` ```bash # Logs and an interactive shell (replace podman with docker when applicable). podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - logs --tail 200 relecov-iskylims + logs --tail 200 iskylims podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec relecov-iskylims bash + exec iskylims bash # Rebuild static assets without running migrations. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec relecov-iskylims bash -lc \ + exec iskylims bash -lc \ 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py collectstatic --noinput' # Inspect Django and migration state before deciding whether to recover. podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec relecov-iskylims bash -lc \ + exec iskylims bash -lc \ 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check --deploy && python manage.py showmigrations --plan' ``` @@ -525,9 +527,9 @@ diagnostic last resort and must use the same backup and release procedure. ```bash podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - logs --tail 200 relecov-iskylims-apache + logs --tail 200 iskylims-apache podman compose --env-file .env.production.file -f docker-compose.prod.yml \ - exec relecov-iskylims-apache httpd -t + exec iskylims-apache httpd -t APACHE_PORT='CHANGE_ME' SERVER_STATUS_SERVER_NAME='localhost' @@ -541,7 +543,7 @@ is enabled, inspect the persistent log bind and confirm a container-compatible label before restarting: ```bash -ls -ldZ /var/log/local/relecov-iskylims/apache +ls -ldZ /var/log/local/iskylims/apache ``` An Apache failure containing `ModSecurity: Failed to open debug log file` often @@ -550,11 +552,11 @@ Preserve it for diagnosis, run `fix-permissions`, and restart Apache. If it must be replaced, move it to a timestamped backup instead of deleting evidence: ```bash -sudo mv /var/log/local/relecov-iskylims/apache/modsec_debug.log \ - /var/log/local/relecov-iskylims/apache/modsec_debug.log.blocked +sudo mv /var/log/local/iskylims/apache/modsec_debug.log \ + /var/log/local/iskylims/apache/modsec_debug.log.blocked bash container_install.sh --action fix-permissions --engine podman \ - --install_conf_map relecov-iskylims,deployment/settings/app_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt -podman compose --env-file .env.production.file -f docker-compose.prod.yml restart relecov-iskylims-apache + --install_conf_map iskylims,deployment/settings/iskylims_production_settings.txt --install_conf_map apache,deployment/settings/apache_production_settings.txt --install_conf_map samba,deployment/settings/samba_production_settings.txt +podman compose --env-file .env.production.file -f docker-compose.prod.yml restart iskylims-apache ``` ## Final configuration steps diff --git a/conf/apache/01-reverse-proxy.conf b/conf/apache/01-reverse-proxy.conf index 2e4883ac..888b5633 100644 --- a/conf/apache/01-reverse-proxy.conf +++ b/conf/apache/01-reverse-proxy.conf @@ -1,4 +1,4 @@ -# Apache reverse proxy source configuration for relecov-iskylims. +# Apache reverse proxy source configuration for iskylims. # # This file is copied to conf/apache/01-reverse-proxy.conf by the scaffold. # Customize its VirtualHost and route blocks in the application repository. diff --git a/conf/apache/apache_production_settings.txt b/conf/apache/apache_production_settings.txt index bfd542ff..9f61a455 100644 --- a/conf/apache/apache_production_settings.txt +++ b/conf/apache/apache_production_settings.txt @@ -9,8 +9,8 @@ APACHE_CONF_PATH='' # Persistent host directory mounted as the Apache log destination. -# Example: /var/log/local/relecov-iskylims/apache -APACHE_LOG_PATH='/var/log/local/relecov-iskylims/apache' +# Example: /var/log/local/iskylims/apache +APACHE_LOG_PATH='/var/log/local/iskylims/apache' # Public host interface and port. Use 127.0.0.1 when another host reverse proxy # is the only component that should reach this Apache container. diff --git a/conf/apache/apache_test_settings.txt b/conf/apache/apache_test_settings.txt index 948160be..2ce900bd 100644 --- a/conf/apache/apache_test_settings.txt +++ b/conf/apache/apache_test_settings.txt @@ -6,7 +6,7 @@ APACHE_CONF_PATH='' # Disposable test logs and a public test endpoint. -APACHE_LOG_PATH='/tmp/relecov-iskylims/apache-logs' +APACHE_LOG_PATH='/tmp/iskylims/apache-logs' APACHE_BIND_HOST='0.0.0.0' APACHE_PORT='8081' diff --git a/conf/docker_production_settings.txt b/conf/docker_production_settings.txt index ddc057a5..8ff91158 100644 --- a/conf/docker_production_settings.txt +++ b/conf/docker_production_settings.txt @@ -4,9 +4,10 @@ # Compose environment and temporary bootstrap copy. ### Application layout and Django project -# Source repository staged in the image and final application installation. -REPO_PATH='/srv/relecov-iskylims' -INSTALL_PATH='/opt/relecov-iskylims' +# Source repository staged in the image. Example: /srv/iskylims +REPO_PATH='/srv/iskylims' +# Final application runtime path. Example: /opt/iskylims +INSTALL_PATH='/opt/iskylims' # Python package containing settings.py, urls.py and wsgi.py. This is generated # from project.json and normally should not be changed in this file. PROJECT_MODULE='iskylims' @@ -31,12 +32,14 @@ APP_SHELL='/sbin/nologin' APP_PORT='8001' ### Persistent host sources -# Documents use the Compose-managed app_documents named volume. +# Documents use the Compose-managed iskylims_documents named volume. # Application logs remain on the host and must be creatable by the installer. -HOST_LOG_PATH='/var/log/local/relecov-iskylims/apps' +# Example: /var/log/local/iskylims/apps +HOST_LOG_PATH='/var/log/local/iskylims/apps' # Host bind source rendered before Compose starts. It must be a file path, not # a directory; the parent directory is created and protected by the installer. -DJANGO_SETTINGS_PATH='/srv/containers/bind/relecov-iskylims/settings/iskylims/settings.py' +# Example: /srv/containers/bind/iskylims/settings/iskylims/settings.py +DJANGO_SETTINGS_PATH='/srv/containers/bind/iskylims/settings/iskylims/settings.py' ### External production database # Use a host name or IP reachable from inside the application container. @@ -90,4 +93,5 @@ APP_START_WAIT_TIMEOUT_SECONDS='100' # regular_folder stores logs below INSTALL_PATH. symbolic_link links that # directory to LOG_PATH, which must already exist and be writable. LOG_TYPE='regular_folder' +# Example for symbolic_link: /var/log/local/iskylims/apps; otherwise empty. LOG_PATH='' diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt index 25ce3fa9..11e4129c 100644 --- a/conf/docker_test_settings.txt +++ b/conf/docker_test_settings.txt @@ -1,8 +1,8 @@ # Safe local/test values. Never use these credentials in production. ### Application layout and Django project -REPO_PATH='/srv/relecov-iskylims' -INSTALL_PATH='/opt/relecov-iskylims' +REPO_PATH='/srv/iskylims' +INSTALL_PATH='/opt/iskylims' PROJECT_MODULE='iskylims' PYTHON_BIN_PATH='python3.11' @@ -18,12 +18,12 @@ APP_SHELL='/bin/bash' APP_PORT='8201' ### Disposable persistence paths -HOST_LOG_PATH='/tmp/relecov-iskylims/logs' +HOST_LOG_PATH='/tmp/iskylims/logs' # Test images render settings during build and do not mount this host path. -DJANGO_SETTINGS_PATH='/tmp/relecov-iskylims/data/settings/iskylims/settings.py' +DJANGO_SETTINGS_PATH='/tmp/iskylims/data/settings/iskylims/settings.py' ### Compose test database -DB_HOST='app_db' +DB_HOST='iskylims-db' DB_PORT='3306' DB_NAME='iskylims' DB_USER='django' diff --git a/container_install.sh b/container_install.sh index 0f9d05b9..101414d7 100755 --- a/container_install.sh +++ b/container_install.sh @@ -15,14 +15,14 @@ APPLICATION_NAME="iSkyLIMS" # Regenerate these callbacks from the descriptor; keep application-neutral # lifecycle mechanics below unchanged. # ============================================================================ -install_services=(relecov-iskylims) +install_services=(iskylims) addon_build_services=() -permission_services=(relecov-iskylims relecov-iskylims-apache) -configured_services=(relecov-iskylims apache samba) +permission_services=(iskylims iskylims-apache) +configured_services=(iskylims apache samba) default_service_install_conf() { case "$1" in - relecov-iskylims) [ "$mode" = test ] && echo conf/docker_test_settings.txt || echo conf/docker_production_settings.txt ;; + iskylims) [ "$mode" = test ] && echo conf/docker_test_settings.txt || echo conf/docker_production_settings.txt ;; apache) [ "$mode" = test ] && echo conf/apache/apache_test_settings.txt || echo conf/apache/apache_production_settings.txt ;; samba) [ "$mode" = test ] && echo conf/samba/samba_test_settings.txt || echo conf/samba/samba_production_settings.txt ;; *) return 1 ;; @@ -30,7 +30,7 @@ default_service_install_conf() { } service_build_context_dir() { case "$1" in - relecov-iskylims) echo . ;; + iskylims) echo . ;; *) return 1 ;; esac } @@ -60,31 +60,31 @@ service_install_path() { } service_readiness_path() { case "$1" in - relecov-iskylims) echo "$(service_install_path "$1")/manage.py" ;; + iskylims) echo "$(service_install_path "$1")/manage.py" ;; *) return 1 ;; esac } service_image_name() { case "$1" in - relecov-iskylims) echo relecov-iskylims:local ;; + iskylims) echo iskylims:local ;; *) return 1 ;; esac } service_profile() { case "$1" in - relecov-iskylims) echo django ;; + iskylims) echo django ;; *) return 1 ;; esac } service_dockerfile() { case "$1" in - relecov-iskylims) echo Dockerfile ;; + iskylims) echo Dockerfile ;; *) return 1 ;; esac } service_container_install_conf() { case "$1" in - relecov-iskylims) echo conf/.runtime_install_settings.txt ;; + iskylims) echo conf/.runtime_install_settings.txt ;; *) return 1 ;; esac } @@ -97,13 +97,13 @@ service_gid() { prepare_compose_environment() { local -a settings_sources=( - "RELECOV_ISKYLIMS|${install_conf_host_by_service[relecov-iskylims]}" + "ISKYLIMS|${install_conf_host_by_service[iskylims]}" "|${install_conf_host_by_service[apache]}" "|${install_conf_host_by_service[samba]}" ) local -a deployment_values=( "GIT_REVISION|$git_revision" - "RELECOV_ISKYLIMS_IMAGE|relecov-iskylims:local" + "ISKYLIMS_IMAGE|iskylims:local" ) compose_env_file="$script_dir/.env.${mode}.file" write_compose_environment_file "$compose_env_file" settings_sources deployment_values @@ -129,10 +129,10 @@ print_service_summary() { prepare_application_host_sources() { local settings_output if [ "$mode" = production ]; then - settings_output="$(service_environment_value relecov-iskylims DJANGO_SETTINGS_PATH)" - [ -n "$settings_output" ] || { echo "DJANGO_SETTINGS_PATH is required for relecov-iskylims" >&2; return 1; } + settings_output="$(service_environment_value iskylims DJANGO_SETTINGS_PATH)" + [ -n "$settings_output" ] || { echo "DJANGO_SETTINGS_PATH is required for iskylims" >&2; return 1; } mkdir -p "$(dirname "$settings_output")" - prepare_django_settings_bind_mount ./conf/template_settings.py "$settings_output" "${install_conf_host_by_service[relecov-iskylims]}" + prepare_django_settings_bind_mount ./conf/template_settings.py "$settings_output" "${install_conf_host_by_service[iskylims]}" fi # conf/apache contains the application-owned Apache sources. Render every # deployment value only after the protected settings environment is loaded, @@ -140,7 +140,7 @@ prepare_application_host_sources() { local apache_source_dir="$script_dir/conf/apache" local apache_output_dir="$script_dir/deployment/apache" local apache_conf_name apache_config_service apache_log_path - apache_config_service=relecov-iskylims + apache_config_service=iskylims [ -d "$apache_source_dir" ] || { echo "Apache source configuration directory not found: $apache_source_dir" >&2 return 1 @@ -180,17 +180,17 @@ prepare_application_host_sources() { prepare_host_bind_source_permissions() { [ "$mode" = production ] || return 0 local log_path settings_path uid gid - log_path="$(service_environment_value relecov-iskylims HOST_LOG_PATH)" - settings_path="$(service_environment_value relecov-iskylims DJANGO_SETTINGS_PATH)" - [ -n "$log_path" ] || { echo "HOST_LOG_PATH is required for relecov-iskylims" >&2; return 1; } - [ -n "$settings_path" ] || { echo "DJANGO_SETTINGS_PATH is required for relecov-iskylims" >&2; return 1; } - uid="$(service_uid relecov-iskylims)"; gid="$(service_gid relecov-iskylims)" - local -a relecov_iskylims_host_bind_permission_spec=( + log_path="$(service_environment_value iskylims HOST_LOG_PATH)" + settings_path="$(service_environment_value iskylims DJANGO_SETTINGS_PATH)" + [ -n "$log_path" ] || { echo "HOST_LOG_PATH is required for iskylims" >&2; return 1; } + [ -n "$settings_path" ] || { echo "DJANGO_SETTINGS_PATH is required for iskylims" >&2; return 1; } + uid="$(service_uid iskylims)"; gid="$(service_gid iskylims)" + local -a iskylims_host_bind_permission_spec=( "$log_path|$uid:$gid|0775" "$(dirname "$settings_path")|-|0755" "$settings_path|$uid:$gid|0664" ) - apply_host_permission_spec "${relecov_iskylims_host_bind_permission_spec[@]}" + apply_host_permission_spec "${iskylims_host_bind_permission_spec[@]}" # Generated proxy configuration is read-only in Apache. Its host files need # traversal/read permissions, while the production log bind must be writable. apache_log_path="${APACHE_LOG_PATH:?APACHE_LOG_PATH is required}" @@ -212,18 +212,18 @@ prepare_running_container_mount_permissions() { local service_name="$1" container_id="$2" local install_path uid gid case "$service_name" in - relecov-iskylims) + iskylims) install_path="$(service_install_path "$service_name")" uid="$(service_uid "$service_name")"; gid="$(service_gid "$service_name")" - local -a relecov_iskylims_running_mount_permission_spec=( + local -a iskylims_running_mount_permission_spec=( "$install_path/logs|$uid:$gid|u+rwX,g+rwX" "$install_path/documents|$uid:$gid|u+rwX,g+rwX" "$install_path/static|$uid:$gid|u+rwX,g+rwX,o+rX" ) - apply_container_directory_permission_spec "$container_id" "${relecov_iskylims_running_mount_permission_spec[@]}" + apply_container_directory_permission_spec "$container_id" "${iskylims_running_mount_permission_spec[@]}" prepare_django_container_settings_permissions "$container_id" "$install_path/iskylims/settings.py" "$uid" "$gid" ;; - relecov-iskylims-apache) + iskylims-apache) # Apache currently needs no ownership repair inside its running # container. Keep an explicit add-on policy ready for future mounts. local -a apache_running_mount_permission_spec=() @@ -238,7 +238,7 @@ bootstrap_service() { local repo_path runtime_conf uid gid status local -a args case "$service_name" in - relecov-iskylims) + iskylims) repo_path="$(service_repo_path "$service_name")" # Fixed temporary in-container path; this is not operator configuration. runtime_conf=conf/.runtime_install_settings.txt @@ -258,6 +258,27 @@ bootstrap_service() { esac } +build_production_service() { + local service_name="$1" context="$2" dockerfile="$3" + case "$service_name" in + iskylims) + engine_build --no-cache --file "$context/$dockerfile" \ + --secret "id=install_conf,src=${install_conf_host_by_service[$service_name]}" \ + --build-arg GIT_REVISION="$git_revision" \ + --build-arg INSTALL_CONF="$(service_container_install_conf "$service_name")" \ + --build-arg USE_INSTALL_CONF_SECRET=true \ + --build-arg RENDER_DJANGO_SETTINGS=false \ + --build-arg APP_REPO_PATH="$(service_repo_path "$service_name")" \ + --build-arg APP_INSTALL_PATH="$(service_install_path "$service_name")" \ + --build-arg APP_PORT="$(service_environment_value "$service_name" APP_PORT)" \ + --build-arg APP_UID="$(service_uid "$service_name")" \ + --build-arg APP_GID="$(service_gid "$service_name")" \ + --tag "$(service_image_name "$service_name")" "$context" + ;; + *) die "Unsupported production build service: $service_name" ;; + esac +} + # Applications with disposable fixtures or demo files customize this callback # in their generated wrapper and set application_supports_test_data=true. Keep # application-specific fixture names, users/groups, downloads, and data-service diff --git a/deployment/project.json b/deployment/project.json index 002f740e..285ec07a 100644 --- a/deployment/project.json +++ b/deployment/project.json @@ -1,17 +1,17 @@ { "APP_NAME": "iSkyLIMS", - "APP_SLUG": "relecov-iskylims", + "APP_SLUG": "iskylims", "DESCRIPTION": "Laboratory information management system for wet-lab and dry-lab sequencing workflows.", "REPOSITORY_URL": "https://github.com/BU-ISCIII/iSkyLIMS.git", "DEFAULT_BRANCH": "main", "PYTHON_VERSION": "3.11", "TIMEZONE": "Europe/Madrid", "SERVICES": { - "relecov-iskylims": { + "iskylims": { "PROFILE": "django", "BUILD_CONTEXT": ".", "DOCKERFILE": "Dockerfile", - "IMAGE": "relecov-iskylims:local", + "IMAGE": "iskylims:local", "INSTALL_CONF": "conf/docker_production_settings.txt", "TEST_INSTALL_CONF": "conf/docker_test_settings.txt", "PROJECT_MODULE": "iskylims" @@ -19,10 +19,10 @@ }, "ADDONS": { "apache": { - "CONFIG_SERVICE": "relecov-iskylims" + "CONFIG_SERVICE": "iskylims" }, "samba": { - "CONFIG_SERVICE": "relecov-iskylims", + "CONFIG_SERVICE": "iskylims", "MODES": [ "test" ] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 31a9f021..c98b7eab 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,7 +1,7 @@ services: - # BEGIN BU-ISCIII SERVICE: relecov-iskylims (django) - relecov-iskylims: - image: "${RELECOV_ISKYLIMS_IMAGE:-relecov-iskylims:local}" + # BEGIN BU-ISCIII SERVICE: iskylims (django) + iskylims: + image: "${ISKYLIMS_IMAGE:-iskylims:local}" build: context: "." dockerfile: "Dockerfile" @@ -11,51 +11,51 @@ services: INSTALL_CONF: "conf/.runtime_install_settings.txt" USE_INSTALL_CONF_SECRET: "true" RENDER_DJANGO_SETTINGS: "false" - APP_UID: ${RELECOV_ISKYLIMS_APP_UID:?RELECOV_ISKYLIMS_APP_UID is required} - APP_GID: ${RELECOV_ISKYLIMS_APP_GID:?RELECOV_ISKYLIMS_APP_GID is required} - APP_PORT: ${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required} - APP_REPO_PATH: ${RELECOV_ISKYLIMS_REPO_PATH:?RELECOV_ISKYLIMS_REPO_PATH is required} - APP_INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} + APP_UID: ${ISKYLIMS_APP_UID:?ISKYLIMS_APP_UID is required} + APP_GID: ${ISKYLIMS_APP_GID:?ISKYLIMS_APP_GID is required} + APP_PORT: ${ISKYLIMS_APP_PORT:?ISKYLIMS_APP_PORT is required} + APP_REPO_PATH: ${ISKYLIMS_REPO_PATH:?ISKYLIMS_REPO_PATH is required} + APP_INSTALL_PATH: ${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required} restart: unless-stopped - user: "${RELECOV_ISKYLIMS_APP_UID:?RELECOV_ISKYLIMS_APP_UID is required}:${RELECOV_ISKYLIMS_APP_GID:?RELECOV_ISKYLIMS_APP_GID is required}" + user: "${ISKYLIMS_APP_UID:?ISKYLIMS_APP_UID is required}:${ISKYLIMS_APP_GID:?ISKYLIMS_APP_GID is required}" environment: APP_MODE: prod - INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} - APP_INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} - APP_PORT: ${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required} + INSTALL_PATH: ${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required} + APP_INSTALL_PATH: ${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required} + APP_PORT: ${ISKYLIMS_APP_PORT:?ISKYLIMS_APP_PORT is required} PROJECT_MODULE: "iskylims" - DB_HOST: ${RELECOV_ISKYLIMS_DB_HOST:?RELECOV_ISKYLIMS_DB_HOST is required} - DB_PORT: ${RELECOV_ISKYLIMS_DB_PORT:?RELECOV_ISKYLIMS_DB_PORT is required} - DB_NAME: ${RELECOV_ISKYLIMS_DB_NAME:?RELECOV_ISKYLIMS_DB_NAME is required} - DB_USER: ${RELECOV_ISKYLIMS_DB_USER:?RELECOV_ISKYLIMS_DB_USER is required} - DB_PASSWORD: ${RELECOV_ISKYLIMS_DB_PASSWORD:?RELECOV_ISKYLIMS_DB_PASSWORD is required} - DJANGO_SECRET_KEY: ${RELECOV_ISKYLIMS_DJANGO_SECRET_KEY:?RELECOV_ISKYLIMS_DJANGO_SECRET_KEY is required} - DJANGO_ALLOWED_HOSTS: ${RELECOV_ISKYLIMS_DJANGO_ALLOWED_HOSTS:?RELECOV_ISKYLIMS_DJANGO_ALLOWED_HOSTS is required} + DB_HOST: ${ISKYLIMS_DB_HOST:?ISKYLIMS_DB_HOST is required} + DB_PORT: ${ISKYLIMS_DB_PORT:?ISKYLIMS_DB_PORT is required} + DB_NAME: ${ISKYLIMS_DB_NAME:?ISKYLIMS_DB_NAME is required} + DB_USER: ${ISKYLIMS_DB_USER:?ISKYLIMS_DB_USER is required} + DB_PASSWORD: ${ISKYLIMS_DB_PASSWORD:?ISKYLIMS_DB_PASSWORD is required} + DJANGO_SECRET_KEY: ${ISKYLIMS_DJANGO_SECRET_KEY:?ISKYLIMS_DJANGO_SECRET_KEY is required} + DJANGO_ALLOWED_HOSTS: ${ISKYLIMS_DJANGO_ALLOWED_HOSTS:?ISKYLIMS_DJANGO_ALLOWED_HOSTS is required} volumes: - - relecov-iskylims_documents:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/documents:z - - ${RELECOV_ISKYLIMS_HOST_LOG_PATH:?RELECOV_ISKYLIMS_HOST_LOG_PATH is required}:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/logs:Z - - ${RELECOV_ISKYLIMS_DJANGO_SETTINGS_PATH:?RELECOV_ISKYLIMS_DJANGO_SETTINGS_PATH is required}:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/iskylims/settings.py:Z - - relecov-iskylims_static:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/static:z + - iskylims_documents:${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required}/documents:z + - ${ISKYLIMS_HOST_LOG_PATH:?ISKYLIMS_HOST_LOG_PATH is required}:${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required}/logs:Z + - ${ISKYLIMS_DJANGO_SETTINGS_PATH:?ISKYLIMS_DJANGO_SETTINGS_PATH is required}:${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required}/iskylims/settings.py:Z + - iskylims_static:${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required}/static:z healthcheck: test: ["CMD", "python", "-c", "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ['APP_PORT'] + '/health/', timeout=3)"] interval: 10s timeout: 5s retries: 12 ports: - - "127.0.0.1:${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required}:${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required}" + - "127.0.0.1:${ISKYLIMS_APP_PORT:?ISKYLIMS_APP_PORT is required}:${ISKYLIMS_APP_PORT:?ISKYLIMS_APP_PORT is required}" # Docker Engine on Linux needs this explicit mapping. Podman also supports # host-gateway and conventionally exposes host.docker.internal itself, so # host-based dependencies can use one hostname with either engine. extra_hosts: - "host.docker.internal:host-gateway" networks: [deployment_net] - # END BU-ISCIII SERVICE: relecov-iskylims + # END BU-ISCIII SERVICE: iskylims # BEGIN BU-ISCIII ADDON: apache - relecov-iskylims-apache: + iskylims-apache: image: registry.access.redhat.com/ubi9/httpd-24:latest restart: unless-stopped depends_on: - relecov-iskylims: + iskylims: condition: service_healthy ports: # Public binding can be restricted to loopback when another host proxy @@ -81,8 +81,8 @@ services: - /usr/share/zoneinfo:/usr/share/zoneinfo:ro # GENERATED MULTI-APP MOUNTS: one read-only static/documents pair for # every Django service. React assets remain inside the React image. - - relecov-iskylims_static:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/static:ro,z - - relecov-iskylims_documents:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/documents:ro,z + - iskylims_static:${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required}/static:ro,z + - iskylims_documents:${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required}/documents:ro,z # OPTIONAL APP MOUNTS belong in ADDONS.apache.MOUNTS. Examples: # - reports_data:/var/www/reports:ro,z # - /srv/public-downloads:/var/www/downloads:ro,Z @@ -93,5 +93,5 @@ networks: deployment_net: volumes: - relecov-iskylims_documents: - relecov-iskylims_static: + iskylims_documents: + iskylims_static: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index f6c8e7d4..d30d8a3c 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,7 +1,7 @@ services: - # BEGIN BU-ISCIII SERVICE: relecov-iskylims (django) - relecov-iskylims: - image: "${RELECOV_ISKYLIMS_IMAGE:-relecov-iskylims:local}" + # BEGIN BU-ISCIII SERVICE: iskylims (django) + iskylims: + image: "${ISKYLIMS_IMAGE:-iskylims:local}" build: context: "." dockerfile: "Dockerfile" @@ -11,47 +11,47 @@ services: INSTALL_CONF: "conf/docker_test_settings.txt" USE_INSTALL_CONF_SECRET: "false" RENDER_DJANGO_SETTINGS: "true" - APP_UID: ${RELECOV_ISKYLIMS_APP_UID:?RELECOV_ISKYLIMS_APP_UID is required} - APP_GID: ${RELECOV_ISKYLIMS_APP_GID:?RELECOV_ISKYLIMS_APP_GID is required} - APP_PORT: ${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required} - APP_REPO_PATH: ${RELECOV_ISKYLIMS_REPO_PATH:?RELECOV_ISKYLIMS_REPO_PATH is required} - APP_INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} + APP_UID: ${ISKYLIMS_APP_UID:?ISKYLIMS_APP_UID is required} + APP_GID: ${ISKYLIMS_APP_GID:?ISKYLIMS_APP_GID is required} + APP_PORT: ${ISKYLIMS_APP_PORT:?ISKYLIMS_APP_PORT is required} + APP_REPO_PATH: ${ISKYLIMS_REPO_PATH:?ISKYLIMS_REPO_PATH is required} + APP_INSTALL_PATH: ${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required} restart: unless-stopped - user: "${RELECOV_ISKYLIMS_APP_UID:?RELECOV_ISKYLIMS_APP_UID is required}:${RELECOV_ISKYLIMS_APP_GID:?RELECOV_ISKYLIMS_APP_GID is required}" + user: "${ISKYLIMS_APP_UID:?ISKYLIMS_APP_UID is required}:${ISKYLIMS_APP_GID:?ISKYLIMS_APP_GID is required}" environment: APP_MODE: dev - INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} - APP_INSTALL_PATH: ${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required} - APP_PORT: ${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required} + INSTALL_PATH: ${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required} + APP_INSTALL_PATH: ${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required} + APP_PORT: ${ISKYLIMS_APP_PORT:?ISKYLIMS_APP_PORT is required} PROJECT_MODULE: "iskylims" - DB_HOST: relecov-iskylims-db - DB_PORT: ${RELECOV_ISKYLIMS_DB_PORT:?RELECOV_ISKYLIMS_DB_PORT is required} - DB_NAME: ${RELECOV_ISKYLIMS_DB_NAME:?RELECOV_ISKYLIMS_DB_NAME is required} - DB_USER: ${RELECOV_ISKYLIMS_DB_USER:?RELECOV_ISKYLIMS_DB_USER is required} - DB_PASSWORD: ${RELECOV_ISKYLIMS_DB_PASSWORD:?RELECOV_ISKYLIMS_DB_PASSWORD is required} - DJANGO_SECRET_KEY: ${RELECOV_ISKYLIMS_DJANGO_SECRET_KEY:?RELECOV_ISKYLIMS_DJANGO_SECRET_KEY is required} - DJANGO_ALLOWED_HOSTS: ${RELECOV_ISKYLIMS_DJANGO_ALLOWED_HOSTS:?RELECOV_ISKYLIMS_DJANGO_ALLOWED_HOSTS is required} + DB_HOST: iskylims-db + DB_PORT: ${ISKYLIMS_DB_PORT:?ISKYLIMS_DB_PORT is required} + DB_NAME: ${ISKYLIMS_DB_NAME:?ISKYLIMS_DB_NAME is required} + DB_USER: ${ISKYLIMS_DB_USER:?ISKYLIMS_DB_USER is required} + DB_PASSWORD: ${ISKYLIMS_DB_PASSWORD:?ISKYLIMS_DB_PASSWORD is required} + DJANGO_SECRET_KEY: ${ISKYLIMS_DJANGO_SECRET_KEY:?ISKYLIMS_DJANGO_SECRET_KEY is required} + DJANGO_ALLOWED_HOSTS: ${ISKYLIMS_DJANGO_ALLOWED_HOSTS:?ISKYLIMS_DJANGO_ALLOWED_HOSTS is required} depends_on: - relecov-iskylims-db: + iskylims-db: condition: service_healthy volumes: - - relecov-iskylims_test_documents:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/documents:z - - relecov-iskylims_test_static:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/static:z + - iskylims_test_documents:${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required}/documents:z + - iskylims_test_static:${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required}/static:z healthcheck: test: ["CMD", "python", "-c", "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ['APP_PORT'] + '/health/', timeout=3)"] interval: 10s timeout: 5s retries: 12 ports: - - "0.0.0.0:${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required}:${RELECOV_ISKYLIMS_APP_PORT:?RELECOV_ISKYLIMS_APP_PORT is required}" + - "0.0.0.0:${ISKYLIMS_APP_PORT:?ISKYLIMS_APP_PORT is required}:${ISKYLIMS_APP_PORT:?ISKYLIMS_APP_PORT is required}" networks: [deployment_net] - # END BU-ISCIII SERVICE: relecov-iskylims + # END BU-ISCIII SERVICE: iskylims # BEGIN BU-ISCIII ADDON: apache - relecov-iskylims-apache: + iskylims-apache: image: registry.access.redhat.com/ubi9/httpd-24:latest restart: unless-stopped depends_on: - relecov-iskylims: + iskylims: condition: service_healthy ports: - "${APACHE_BIND_HOST:?APACHE_BIND_HOST is required}:${APACHE_PORT:?APACHE_PORT is required}:8080" @@ -69,13 +69,13 @@ services: # Disposable log volume avoids writing test logs into production paths. - apache_test_logs:/var/log/httpd:z # GENERATED MULTI-APP MOUNTS: disposable test static/document volumes. - - relecov-iskylims_test_static:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/static:ro,z - - relecov-iskylims_test_documents:${RELECOV_ISKYLIMS_INSTALL_PATH:?RELECOV_ISKYLIMS_INSTALL_PATH is required}/documents:ro,z + - iskylims_test_static:${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required}/static:ro,z + - iskylims_test_documents:${ISKYLIMS_INSTALL_PATH:?ISKYLIMS_INSTALL_PATH is required}/documents:ro,z # OPTIONAL APP MOUNTS belong in ADDONS.apache.MOUNTS. networks: [deployment_net] # END BU-ISCIII ADDON: apache # BEGIN BU-ISCIII ADDON: samba (test only) - relecov-iskylims-samba: + iskylims-samba: image: docker.io/dperson/samba:latest restart: unless-stopped command: '-S -s "ngs_data;/mnt/test_ngs_data;yes;yes;no;${SAMBA_USER:?SAMBA_USER is required};none;none;ngs data samba share" -u "${SAMBA_USER:?SAMBA_USER is required};${SAMBA_PASSWORD:?SAMBA_PASSWORD is required}" -p' @@ -83,27 +83,27 @@ services: - samba_test_data:/mnt:z networks: [deployment_net] # END BU-ISCIII ADDON: samba - relecov-iskylims-db: + iskylims-db: image: docker.io/library/mysql:8.0 environment: - MYSQL_DATABASE: ${RELECOV_ISKYLIMS_DB_NAME:?RELECOV_ISKYLIMS_DB_NAME is required} - MYSQL_USER: ${RELECOV_ISKYLIMS_DB_USER:?RELECOV_ISKYLIMS_DB_USER is required} - MYSQL_PASSWORD: ${RELECOV_ISKYLIMS_DB_PASSWORD:?RELECOV_ISKYLIMS_DB_PASSWORD is required} - MYSQL_ROOT_PASSWORD: ${RELECOV_ISKYLIMS_DB_ROOT_PASSWORD:?RELECOV_ISKYLIMS_DB_ROOT_PASSWORD is required} + MYSQL_DATABASE: ${ISKYLIMS_DB_NAME:?ISKYLIMS_DB_NAME is required} + MYSQL_USER: ${ISKYLIMS_DB_USER:?ISKYLIMS_DB_USER is required} + MYSQL_PASSWORD: ${ISKYLIMS_DB_PASSWORD:?ISKYLIMS_DB_PASSWORD is required} + MYSQL_ROOT_PASSWORD: ${ISKYLIMS_DB_ROOT_PASSWORD:?ISKYLIMS_DB_ROOT_PASSWORD is required} healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"] interval: 5s timeout: 5s retries: 20 - volumes: [relecov-iskylims_test_db:/var/lib/mysql] + volumes: [iskylims_test_db:/var/lib/mysql] networks: [deployment_net] networks: deployment_net: volumes: - relecov-iskylims_test_db: - relecov-iskylims_test_documents: - relecov-iskylims_test_static: + iskylims_test_db: + iskylims_test_documents: + iskylims_test_static: apache_test_logs: samba_test_data: diff --git a/scripts/container_start.sh b/scripts/container_start.sh index b2b66df6..ac648ec5 100755 --- a/scripts/container_start.sh +++ b/scripts/container_start.sh @@ -2,10 +2,10 @@ set -euo pipefail # Shared RELECOV Platform/iSkyLIMS runtime pattern, parameterized per project. -APP_DIR="${APP_INSTALL_PATH:-${INSTALL_PATH:-/opt/relecov-iskylims}}" +APP_DIR="${APP_INSTALL_PATH:-${INSTALL_PATH:-/opt/iskylims}}" CRON_DIR="${APP_DIR}/cron" TMP_DIR="${APP_DIR}/tmp" -CRON_FILE="${CRON_DIR}/relecov-iskylims" +CRON_FILE="${CRON_DIR}/iskylims" CRON_LOG="${TMP_DIR}/supercronic.log" CRON_DISABLED_FILE="${CRON_DIR}/disabled" APP_MODE="${APP_MODE:-prod}" diff --git a/scripts/smoke_test.sh b/scripts/smoke_test.sh index 26e3fb26..db01c099 100755 --- a/scripts/smoke_test.sh +++ b/scripts/smoke_test.sh @@ -9,7 +9,7 @@ repo_root="$(cd "$script_dir/.." && pwd)" # shellcheck disable=SC1091 source "$repo_root/deployment/lib/container/common.sh" -install_services=(relecov-iskylims) +install_services=(iskylims) engine="docker"; mode="production"; compose_file=""; env_file="" while (($#)); do case "$1" in @@ -35,11 +35,11 @@ fi compose_run() { compose_with_env_exec -f "$compose_file" "$@"; } fail() { echo "FAIL: $*" >&2; exit 1; } compose_run config >/dev/null - container_id="$(resolve_service_container relecov-iskylims)" - [ -n "$container_id" ] || fail "Service relecov-iskylims has no container" - ensure_service_running relecov-iskylims "$container_id" >/dev/null + container_id="$(resolve_service_container iskylims)" + [ -n "$container_id" ] || fail "Service iskylims has no container" + ensure_service_running iskylims "$container_id" >/dev/null engine_exec exec "$container_id" bash -lc 'cd "$INSTALL_PATH" && source virtualenv/bin/activate && python manage.py check && ! python manage.py showmigrations --plan | grep -F '"'"'[ ]'"'"'' - echo "PASS: relecov-iskylims Django checks and migrations" + echo "PASS: iskylims Django checks and migrations" check_url() { local service="$1" url="$2" curl --fail --silent --show-error --location --max-time 20 --output /dev/null "$url" \ From 6242c1d04839edf8ec4727a2ed193a9612421805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sara=20Monz=C3=B3n?= Date: Tue, 22 Sep 2026 13:02:13 +0200 Subject: [PATCH 55/55] add skip flake --- conf/template_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/template_settings.py b/conf/template_settings.py index 6c2a5f7f..faba011c 100644 --- a/conf/template_settings.py +++ b/conf/template_settings.py @@ -12,7 +12,7 @@ # The renderer replaces this complete line and preserves the generated secret # during upgrades. Never put a real production secret in this repository. SECRET_KEY = "PLACEHOLDER" -DEBUG = djangodebug +DEBUG = djangodebug # noqa: F821 - replaced by the deployment renderer ALLOWED_HOSTS = [ host.strip() for host in "djangoallowedhosts".split(",") if host.strip() ] @@ -93,7 +93,7 @@ "PORT": "djangoport", "NAME": "djangodbname", "HOST": os.getenv("DB_HOST", "djangohost"), - "CONN_MAX_AGE": dbconnmaxage, + "CONN_MAX_AGE": dbconnmaxage, # noqa: F821 - deployment placeholder "TEST": { "NAME": "iSkyLIMS_test", }, @@ -139,7 +139,7 @@ EMAIL_PORT = "emailport" EMAIL_HOST_USER = "emailhostuser" EMAIL_HOST_PASSWORD = "emailhostpassword" -EMAIL_USE_TLS = emailhosttls +EMAIL_USE_TLS = emailhosttls # noqa: F821 - replaced by the deployment renderer ALLOWED_EMAIL_DOMAINS = ["isciii.es", "externos.isciii.es"] # django-crontab writes into the persistent application log directory. Add new