From 33bab4e3df70e991c6207da49245177ae2bc8cf8 Mon Sep 17 00:00:00 2001 From: kburke <209327+kburke@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:16:47 -0500 Subject: [PATCH 1/3] Move conf.d mount out of docker-compose.yml to supplemental configuration for tier, anticipating new configuration for localhost distinct from development --- docker/docker-compose.deployment.yml | 5 +++-- docker/docker-compose.development.yml | 4 +++- docker/docker-compose.yml | 2 -- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docker/docker-compose.deployment.yml b/docker/docker-compose.deployment.yml index 32c52058..83992daf 100644 --- a/docker/docker-compose.deployment.yml +++ b/docker/docker-compose.deployment.yml @@ -10,5 +10,6 @@ services: limits: cpus: '8' # 50% memory: 19.2G # 60% - - + volumes: + - # Mount conf.d on host machine to the nginx conf.d on container + - "./entity-api/nginx/conf.d:/etc/nginx/conf.d" diff --git a/docker/docker-compose.development.yml b/docker/docker-compose.development.yml index c89b1284..57e6a8f1 100644 --- a/docker/docker-compose.development.yml +++ b/docker/docker-compose.development.yml @@ -3,7 +3,7 @@ services: entity-api: build: context: ./entity-api - # Uncomment if tesitng against a specific branch of commons other than the PyPI package + # Uncomment if testing against a specific branch of commons other than the PyPI package # Will also need to use the 'git+https://github.com/hubmapconsortium/commons.git@${COMMONS_BRANCH}#egg=hubmap-commons' # in src/requirements.txt accordingly args: @@ -25,3 +25,5 @@ services: - "../BUILD:/usr/src/app/BUILD" # Mount the source code to container - "../src:/usr/src/app/src" + - # Mount conf.d on host machine to the nginx conf.d on container + - "./entity-api/nginx/conf.d:/etc/nginx/conf.d" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8b36e89d..c99b1ab4 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -27,8 +27,6 @@ services: - "../log:/usr/src/app/log" # Mount the schema yaml file - "../src/schema/provenance_schema.yaml:/usr/src/app/src/schema/provenance_schema.yaml" - # Mount conf.d on host machine to the nginx conf.d on container - - "./entity-api/nginx/conf.d:/etc/nginx/conf.d" networks: - gateway_hubmap # Send docker logs to AWS CloudWatch From 7db293c255c2bd42a076af3bdcf39c237f1b85ce Mon Sep 17 00:00:00 2001 From: kburke <209327+kburke@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:12:44 -0500 Subject: [PATCH 2/3] Fix typo and add comment --- docker/docker-compose.yml | 2 +- docker/entity-api/nginx/nginx.conf | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c99b1ab4..df022337 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -38,6 +38,6 @@ services: awslogs-stream: ${LOG_STREAM} networks: - # This is the network created by gateway to enable communicaton between multiple docker-compose projects + # This is the network created by gateway to enable communication between multiple docker-compose projects gateway_hubmap: external: true diff --git a/docker/entity-api/nginx/nginx.conf b/docker/entity-api/nginx/nginx.conf index 85bb1e4f..ff4445c0 100644 --- a/docker/entity-api/nginx/nginx.conf +++ b/docker/entity-api/nginx/nginx.conf @@ -18,6 +18,7 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; + # Extend nginx's default Combined Log Format with $http_x_forwarded_for log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; From 3246f854000bbf462a0826f50e85ac756dec0e09 Mon Sep 17 00:00:00 2001 From: kburke <209327+kburke@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:14:44 -0500 Subject: [PATCH 3/3] Introduce configuration to request authentication within Docker Network as replacement for AWS Gateway authorizer authentication --- docker/docker-compose.localhost.yml | 33 ++++ docker/docker-localhost.sh | 148 ++++++++++++++++++ .../nginx/conf.d-localhost/entity-api.conf | 82 ++++++++++ 3 files changed, 263 insertions(+) create mode 100644 docker/docker-compose.localhost.yml create mode 100755 docker/docker-localhost.sh create mode 100644 docker/entity-api/nginx/conf.d-localhost/entity-api.conf diff --git a/docker/docker-compose.localhost.yml b/docker/docker-compose.localhost.yml new file mode 100644 index 00000000..6a39942d --- /dev/null +++ b/docker/docker-compose.localhost.yml @@ -0,0 +1,33 @@ +services: + + entity-api: + build: + context: ./entity-api + args: + - COMMONS_BRANCH=${COMMONS_BRANCH:-main} + image: hubmap/entity-api:${ENTITY_API_VERSION:?err} + environment: + - DEPLOY_MODE=localhost + volumes: + # Mount VERSION and BUILD files + - "../VERSION:/usr/src/app/VERSION" + - "../BUILD:/usr/src/app/BUILD" + ## Mount source code for live development + #- "../src:/usr/src/app/src" + # Mount localhost-specific nginx config + - "${PWD}/entity-api/nginx/conf.d-localhost:/etc/nginx/conf.d" + healthcheck: + # Replaces base healthcheck - Check port 8080 inside container (nginx listening port) + test: [ "CMD", "curl", "--fail", "http://localhost:8080/status" ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + logging: + # Override CloudWatch logging - use local json-file driver for localhost + driver: json-file + options: + max-size: "10m" + max-file: "3" # Keep 3 files, rotating oldest out + networks: + - gateway_hubmap # Same network as hubmap-auth for communication diff --git a/docker/docker-localhost.sh b/docker/docker-localhost.sh new file mode 100755 index 00000000..a734c166 --- /dev/null +++ b/docker/docker-localhost.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +# Print a new line and the banner +echo +echo "==================== Entity-API ====================" + +function tier_check() { + # Get the script name and extract DEPLOY_TIER + SCRIPT_NAME=$(basename "${0}") + + # Extract deploy tier from script name (docker-*.sh pattern) + if [[ ${SCRIPT_NAME} =~ docker-(.*)\.sh ]]; then + DEPLOY_TIER="${BASH_REMATCH[1]}" + else + echo "Error: Script name doesn't match pattern 'docker-*.sh'" + exit 1 + fi + echo "Executing ${SCRIPT_NAME} to deploy in Docker on ${DEPLOY_TIER}" +} + +# Chances are localhost development is not being done on an RHEL server with +# the environment variables set. Unset HOST_UID and HOST_GID to ensure +# docker-compose defaults (1001:1001) are used. +function export_host_ids() { + if [ -n "${HOST_UID}" ] || [ -n "${HOST_GID}" ]; then + echo "WARNING: HOST_UID and HOST_GID are set in your environment but will be ignored for localhost." + echo " Localhost development uses docker-compose.yml defaults." + fi + # Unset to ensure docker-compose defaults are used + unset HOST_UID + unset HOST_GID +} + +# The `absent_or_newer` checks if the copied src at docker/some-api/src directory exists +# and if the source src directory is newer. +# If both conditions are true `absent_or_newer` writes an error message +# and causes script to exit with an error code. +function absent_or_newer() { + if [ \( -e ${1} \) -a \( ${2} -nt ${1} \) ]; then + echo "${1} is out of date" + exit -1 + fi +} + +function get_dir_of_this_script() { + # This function sets DIR to the directory in which this script itself is found. + # Thank you https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself + SCRIPT_SOURCE="${BASH_SOURCE[0]}" + while [ -h "${SCRIPT_SOURCE}" ]; do # resolve $SCRIPT_SOURCE until the file is no longer a symlink + DIR="$( cd -P "$( dirname "${SCRIPT_SOURCE}" )" >/dev/null 2>&1 && pwd )" + SCRIPT_SOURCE="$(readlink "${SCRIPT_SOURCE}")" + [[ ${SCRIPT_SOURCE} != /* ]] && SCRIPT_SOURCE="${DIR}/${SCRIPT_SOURCE}" # if $SCRIPT_SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located + done + DIR="$( cd -P "$( dirname "${SCRIPT_SOURCE}" )" >/dev/null 2>&1 && pwd )" + echo "DIR of script: ${DIR}" +} + +# Generate the build version based on git branch name and short commit hash and write into BUILD file +function generate_build_version() { + GIT_BRANCH_NAME=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p') + GIT_SHORT_COMMIT_HASH=$(git rev-parse --short HEAD) + # Clear the old BUILD version and write the new one + truncate -s 0 ../BUILD + # Note: echo to file appends newline + echo "${GIT_BRANCH_NAME}:${GIT_SHORT_COMMIT_HASH}" >> ../BUILD + # Remove the trailing newline character + truncate -s -1 ../BUILD + echo "BUILD(git branch name:short commit hash): ${GIT_BRANCH_NAME}:${GIT_SHORT_COMMIT_HASH}" +} + +# Set the version environment variable for the docker build +# Version number is from the VERSION file +# Also remove newlines and leading/trailing slashes if present in that VERSION file +function export_version() { + export ENTITY_API_VERSION=$(tr -d "\n\r" < ../VERSION | xargs) + echo "ENTITY_API_VERSION: ${ENTITY_API_VERSION}" +} + +if [[ "${1}" != "check" && "${1}" != "config" && "${1}" != "build" && "${1}" != "start" && "${1}" != "stop" && "${1}" != "down" ]]; then + echo "Unknown command '${1}', specify one of the following: check|config|build|start|stop|down" +else + # Echo this script name and the tier expected for Docker deployment + tier_check + + # Always show the script dir + get_dir_of_this_script + + # Always export and show the version + export_version + + # Unset HOST_UID/HOST_GID for localhost to use defaults + export_host_ids + + # Always show the build in case branch changed or new commits + generate_build_version + + # Print empty line + echo + + if [ "${1}" = "check" ]; then + # Bash array + config_paths=( + '../src/instance/app.cfg' + ) + + for pth in "${config_paths[@]}"; do + if [ ! -e ${pth} ]; then + echo "Missing file (relative path to DIR of script): ${pth}" + exit -1 + fi + done + + absent_or_newer entity-api/src ../src + + echo 'Checks complete, all good :)' + elif [ "${1}" = "config" ]; then + docker compose -f docker-compose.yml -f docker-compose.${DEPLOY_TIER}.yml -p entity-api config + elif [ "${1}" = "build" ]; then + # Delete the copied source code dir if exists + if [ -d "entity-api/src" ]; then + rm -rf entity-api/src + fi + + # Copy over the src folder + cp -r ../src entity-api/ + + # Delete old VERSION and BUILD files if found + if [ -f "entity-api/VERSION" ]; then + rm -rf entity-api/VERSION + fi + + if [ -f "entity-api/BUILD" ]; then + rm -rf entity-api/BUILD + fi + + # Copy over the VERSION and BUILD files + cp ../VERSION entity-api + cp ../BUILD entity-api + + docker compose -f docker-compose.yml -f docker-compose.${DEPLOY_TIER}.yml -p entity-api build --no-cache + elif [ "${1}" = "start" ]; then + docker compose -f docker-compose.yml -f docker-compose.${DEPLOY_TIER}.yml -p entity-api up -d + elif [ "${1}" = "stop" ]; then + docker compose -f docker-compose.yml -f docker-compose.${DEPLOY_TIER}.yml -p entity-api stop + elif [ "${1}" = "down" ]; then + docker compose -f docker-compose.yml -f docker-compose.${DEPLOY_TIER}.yml -p entity-api down + fi +fi \ No newline at end of file diff --git a/docker/entity-api/nginx/conf.d-localhost/entity-api.conf b/docker/entity-api/nginx/conf.d-localhost/entity-api.conf new file mode 100644 index 00000000..408fcdba --- /dev/null +++ b/docker/entity-api/nginx/conf.d-localhost/entity-api.conf @@ -0,0 +1,82 @@ +server { + # Only root can listen on ports below 1024, we use higher-numbered ports + # since nginx is running under non-root user hubmap + listen 8080; + server_name localhost; + root /usr/share/nginx/html; + + # Docker's internal DNS resolver + resolver 127.0.0.11 valid=10s; + resolver_timeout 5s; + + # We need this logging for inspecting auth requests from other internal services + # Logging to the mounted volume for outside container access + access_log /usr/src/app/log/nginx_access_entity-api.log; + error_log /usr/src/app/log/nginx_error_entity-api.log warn; + + # Set payload size limit to 10M, default is 1M. + client_max_body_size 10M; + + # Pass requests to the uWSGI server using the "uwsgi" protocol on port 5000 + location / { + # Always enable CORS + # Response to preflight requests + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, OPTIONS' always; + # These are permitted headers to be used with the actual request + add_header 'Access-Control-Allow-Headers' 'Authorization, Cache-Control, Content-Type, X-Hubmap-Application' always; + # Cache the response to this preflight request in browser for the max age 86400 seconds (= 24 hours) + add_header 'Access-Control-Max-Age' 86400 always; + # No Content + return 204; + } + + # Response to the original requests (HTTP methods are case-sensitive) with CORS enabled + if ($request_method ~ (POST|GET|PUT)) { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Authorization, Cache-Control, Content-Type, X-Hubmap-Application' always; + } + + # Capture original request details BEFORE auth_request subrequest + set $original_uri $request_uri; + set $original_method $request_method; + + # Call hubmap-auth for authorization before passing to entity-api + auth_request /api_auth; + + # Pass authorization headers from hubmap-auth response to the Flask app + auth_request_set $auth_user $upstream_http_x_hubmap_user; + auth_request_set $auth_groups $upstream_http_x_hubmap_groups; + uwsgi_param X-Hubmap-User $auth_user; + uwsgi_param X-Hubmap-Groups $auth_groups; + + include uwsgi_params; + uwsgi_pass uwsgi://localhost:5000; + } + + # Internal location for auth requests - calls hubmap-auth container + location = /api_auth { + internal; + # Use variable to enable runtime DNS resolution via Docker's internal DNS. + # This allows hubmap-auth to restart without requiring entity-api nginx reload. + set $hubmap_auth_backend "hubmap-auth:7777"; + # Call hubmap-auth via Docker network using container hostname and port 7777 + proxy_pass http://$hubmap_auth_backend/api_auth; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + # + # These headers match what hubmap-auth app.py expects. + # + # We need to hard-code the Host to the Docker service name on the + # Docker network gateway_hubmap, rather than $http_host, so that + # a value like localhost:3333 is not passed. The value must be a + # JSON Object key in the gateway repository's api_endpoints.localhost.json. + proxy_set_header Host "entity-api"; + #proxy_set_header X-Original-URI $request_uri; + #proxy_set_header X-Original-Request-Method $request_method; + proxy_set_header X-Original-URI $original_uri; + proxy_set_header X-Original-Request-Method $original_method; + } +} \ No newline at end of file