Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 17 additions & 19 deletions api/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@ GUNICORN_PROCESSES='2'
GUNICORN_THREADS='5'

# Database
NAMEX_DATABASE_HOST=
NAMEX_DATABASE_NAME=
NAMEX_DATABASE_PASSWORD=
NAMEX_DATABASE_PORT=
NAMEX_DATABASE_USERNAME=

DATABASE_TEST_HOST=
DATABASE_TEST_NAME=
DATABASE_TEST_PASSWORD=
DATABASE_TEST_PORT=
DATABASE_TEST_USERNAME=

# Oracle Database
NRO_USER=
NRO_PASSWORD=
NRO_DB_NAME=
ORACLE_HOST=
ORACLE_PORT=1521
# only for local db
# check dev-scripts/local-db/docker-compose.yml for exact values
# similar values are also used in https://github.com/bcgov/bcregistry-sre/blob/main/.github/workflows/backend-ci.yaml
DATABASE_USERNAME=postgres
DATABASE_PASSWORD="postgres"
DATABASE_NAME="unittesting"
DATABASE_HOST="localhost"
DATABASE_PORT="54345"
DATABASE_SCHEMA="public"
DATABASE_OWNER="postgres"

# only when conenecting to clousql db
DATABASE_INSTANCE_CONNECTION_NAME=
DATABASE_NAME=namex
DATABASE_USERNAME="...@gov.bc.ca" # your email, which needs to be added as IAM user to cloudsql instance and granted readwrite access
DATABASE_IP_TYPE=public
DATABASE_OWNER=userHQH

# APIs
SOLR_BASE_URL=
Expand Down Expand Up @@ -67,7 +66,6 @@ JWT_OIDC_CACHING_ENABLED=True
JWT_OIDC_JWKS_CACHE_TIMEOUT=300

# PUBSUB
BUSINESS_GCP_AUTH_KEY=
NAMEX_MAILER_TOPIC=
NAMEX_NR_STATE_TOPIC=

Expand Down
104 changes: 57 additions & 47 deletions api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
# platform=linux/amd64
FROM python:3.12.2
FROM python:3.12-slim

# Always ensure the latest security patches are applied
RUN apt-get update && apt-get upgrade -y && apt-get clean && rm -rf /var/lib/apt/lists/*

ARG VCS_REF="missing"
ARG BUILD_DATE="missing"

ENV VCS_REF=${VCS_REF}
ENV BUILD_DATE=${BUILD_DATE}
ARG APP_ENV
ARG UID=1000
ARG GID=1000

ENV VCS_REF=${VCS_REF} \
BUILD_DATE=${BUILD_DATE} \
APP_ENV=${APP_ENV} \
# python:
PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1 \
# pip:
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_DEFAULT_TIMEOUT=100 \
PIP_ROOT_USER_ACTION=ignore \
# poetry:
POETRY_VERSION=1.8.3 \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false \
POETRY_CACHE_DIR='/var/cache/pypoetry' \
POETRY_HOME='/usr/local' \
# app:
PYTHONPATH=/code

LABEL org.label-schema.vcs-ref=${VCS_REF} \
org.label-schema.build-date=${BUILD_DATE}

USER root

ARG APP_ENV \
# Needed for fixing permissions of files created by Docker:
UID=1000 \
GID=1000

ENV APP_ENV=${APP_ENV} \
# python:
PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1 \
# pip:
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_DEFAULT_TIMEOUT=100 \
PIP_ROOT_USER_ACTION=ignore \
# poetry:
POETRY_VERSION=1.3.2 \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false \
POETRY_CACHE_DIR='/var/cache/pypoetry' \
POETRY_HOME='/usr/local'

SHELL ["/bin/bash", "-eo", "pipefail", "-c"]

# Install system dependencies and Poetry in a single layer
RUN apt-get update && apt-get upgrade -y \
&& apt-get install --no-install-recommends -y \
bash \
Expand All @@ -47,44 +47,54 @@ RUN apt-get update && apt-get upgrade -y \
git \
libpq-dev \
wait-for-it \
&& curl -sSL 'https://install.python-poetry.org' | python - \
ca-certificates \
openssl \
&& pip install --no-cache-dir setuptools \
&& pip install --no-cache-dir "poetry==${POETRY_VERSION}" \
&& poetry --version \
&& poetry config installer.max-workers 2 \
&& poetry config installer.parallel false \
# Cleaning cache:
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/*


# Create user and workspace
WORKDIR /code

RUN groupadd -g "${GID}" -r web \
&& useradd -d '/code' -g web -l -r -u "${UID}" web \
&& chown web:web -R '/code'
&& chown web:web -R '/code' \
&& mkdir -p /var/cache/pypoetry \
&& chown web:web -R /var/cache/pypoetry

# Copy only requirements, to cache them in docker layer
COPY --chown=web:web ./poetry.lock ./pyproject.toml /code/

COPY --chown=web:web ./namex /code/namex
COPY --chown=web:web ./README.md /code

# Project initialization:
# Install dependencies with GitHub Actions optimizations
RUN --mount=type=cache,target="$POETRY_CACHE_DIR" \
echo "$APP_ENV" \
echo "Building for environment: ${APP_ENV:-development}" \
&& poetry version \
# Install deps:
&& poetry run pip install -U pip \
&& poetry config installer.max-workers 1 \
&& poetry install \
$(if [ -z ${APP_ENV+x} ] | [ "$APP_ENV" = 'production' ]; then echo '--only main'; fi) \
--no-interaction --no-ansi
$(if [ "$APP_ENV" = 'production' ] || [ -z "$APP_ENV" ]; then echo '--only main'; else echo '--with dev'; fi) \
--no-interaction --no-ansi --verbose

# Running as non-root user:
# Switch to non-root user after installations
USER web

# The following stage is only for production:
# FROM development_build AS production_build
# Copy application code after dependencies are installed
COPY --chown=web:web ./namex /code/namex
COPY --chown=web:web ./README.md /code/
COPY --chown=web:web . /code

# Set secure permissions immediately after copying to remove write permissions
# Files: read-only (644), Directories: read+execute (755), scripts: executable (755)
RUN find /code -type f -exec chmod 644 {} \; \
&& find /code -type d -exec chmod 755 {} \; \
&& chmod 755 /code/wsgi.py \
&& chmod 755 /code/update_db.sh

# Run the server
ENV PYTHONPATH=/code
EXPOSE 8080

CMD gunicorn --bind 0.0.0.0:8080 --config /code/gunicorn_config.py wsgi:app
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--config", "/code/gunicorn_config.py", "wsgi:app"]
30 changes: 15 additions & 15 deletions api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,30 @@ install: clean ## Install python virtrual environment
#################################################################################
# COMMANDS - CI #
#################################################################################
ci: pylint flake8 test ## CI flow
ci: docker-build-check ruff test ## CI flow

pylint: ## Linting with pylint
. .venv/bin/activate && pylint --rcfile=setup.cfg $(PROJECT_FOLDER_NAME)
docker-build-check: ## Check if Dockerfile builds successfully
docker build -f Dockerfile -t namex-api .

flake8: ## Linting with flake8
. .venv/bin/activate && flake8 $(PROJECT_FOLDER_NAME) tests
ruff: ## ruff linter
poetry run ruff check

lint: pylint flake8 ## run all lint type scripts
ruff-fix: ## auto fix lint issues with ruff
poetry run ruff check --fix

test: ## Unit testing
. .venv/bin/activate && pytest

mac-cov: local-test ## Run the coverage report and display in a browser window (mac)
open -a "Google Chrome" htmlcov/index.html
test: local-db ## unit testing with local db
poetry run pytest

#################################################################################
# COMMANDS - Local #
#################################################################################
run: db ## Run the project in local
. venv/bin/activate && python3.12 -m flask run -p 5000

db: ## Update the local database
. venv/bin/activate && python3.12 -m manage db upgrade
run: local-db ## Run the project in local
. .venv/bin/activate && python3.12 -m flask run -p 5000

local-db: ## Set up the local development database
docker compose -f dev-scripts/local-db/docker-compose.yml up -d
./update_db.sh

#################################################################################
# Self Documenting Commands #
Expand Down
2 changes: 1 addition & 1 deletion api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ BC Registries Names Examination, research and approval system API

## Technology Stack Used
* Python, Flask
* Postgres - SQLAlchemy, psycopg2-binary & alembic
* Postgres - SQLAlchemy, ppg8000 & alembic

## Third-Party Products/Libraries used and the the License they are covert by

Expand Down
58 changes: 36 additions & 22 deletions api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
'development': 'config.DevConfig',
'testing': 'config.TestConfig',
'production': 'config.Config',
'migration': 'config.MigrationConfig',
'default': 'config.Config',
}

Expand Down Expand Up @@ -66,15 +67,20 @@ class Config(object):
NAMEX_LD_SDK_ID = os.getenv('NAMEX_LD_SDK_ID', '')

# POSTGRESQL
DB_USER = os.getenv('NAMEX_DATABASE_USERNAME', '')
DB_PASSWORD = os.getenv('NAMEX_DATABASE_PASSWORD', '')
DB_NAME = os.getenv('NAMEX_DATABASE_NAME', '')
DB_HOST = os.getenv('NAMEX_DATABASE_HOST', '')
DB_PORT = os.getenv('NAMEX_DATABASE_PORT', '5432')
if DB_UNIX_SOCKET := os.getenv('NAMEX_DATABASE_UNIX_SOCKET', None):
SQLALCHEMY_DATABASE_URI = f'postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}@/{DB_NAME}?host={DB_UNIX_SOCKET}'
DB_USER = os.getenv('DATABASE_USERNAME', '')
DB_PASSWORD = os.getenv('DATABASE_PASSWORD', '')
DB_NAME = os.getenv('DATABASE_NAME', '')
DB_HOST = os.getenv('DATABASE_HOST', '')
DB_PORT = int(os.getenv('DATABASE_PORT', '5432'))

DB_SCHEMA = os.getenv('DATABASE_SCHEMA', 'public')
DB_IP_TYPE = os.getenv('DATABASE_IP_TYPE', 'private')
DB_OWNER = os.getenv('DATABASE_OWNER', 'postgres')

if DB_INSTANCE_CONNECTION_NAME := os.getenv('DATABASE_INSTANCE_CONNECTION_NAME', None):
SQLALCHEMY_DATABASE_URI = 'postgresql+pg8000://'
else:
SQLALCHEMY_DATABASE_URI = f'postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}'
SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}'

# KEYCLOAK & JWT_OIDC Settings
JWT_OIDC_WELL_KNOWN_CONFIG = os.getenv('JWT_OIDC_WELL_KNOWN_CONFIG')
Expand Down Expand Up @@ -120,24 +126,32 @@ class DevConfig(Config):
DISABLE_NAMEREQUEST_SOLR_UPDATES = int(os.getenv('DISABLE_NAMEREQUEST_SOLR_UPDATES', 0))


class TestConfig(Config):
"""Test config used for pytests."""
class MigrationConfig(Config): # pylint: disable=too-few-public-methods
"""Config for db migration."""

TESTING = (False,)
DEBUG = True

class TestConfig(Config): # pylint: disable=too-few-public-methods
"""In support of unit testing only. Used by the pytest suite."""

DEBUG = True
TESTING = True
# POSTGRESQL
DB_USER = os.getenv('DATABASE_TEST_USERNAME', '')
DB_PASSWORD = os.getenv('DATABASE_TEST_PASSWORD', '')
DB_NAME = os.getenv('DATABASE_TEST_NAME', '')
DB_HOST = os.getenv('DATABASE_TEST_HOST', '')
DB_PORT = os.getenv('DATABASE_TEST_PORT', '5432')

LOCAL_DEV_MODE = os.getenv('LOCAL_DEV_MODE', False)
# Set this in your .env to debug SQL Alchemy queries (for local development)
SQLALCHEMY_ECHO = 'debug' if os.getenv('DEBUG_SQL_QUERIES', False) else False
SQLALCHEMY_DATABASE_URI = 'postgresql://{user}:{password}@{host}:{port}/{name}'.format(
user=DB_USER, password=DB_PASSWORD, host=DB_HOST, port=int(DB_PORT), name=DB_NAME
)
DB_USER = os.getenv('DATABASE_TEST_USERNAME', 'postgres')
DB_PASSWORD = os.getenv('DATABASE_TEST_PASSWORD', 'postgres')
DB_NAME = os.getenv('DATABASE_TEST_NAME', 'unittesting')
DB_HOST = os.getenv('DATABASE_TEST_HOST', 'localhost')
DB_PORT = os.getenv('DATABASE_TEST_PORT', '54345')
SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}'

# Ensure SQLAlchemy is properly configured for Flask-Marshmallow compatibility
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_pre_ping': True,
'pool_recycle': 300,
}

EMAILER_TOPIC = os.getenv('NAMEX_MAILER_TOPIC', '')

DISABLE_NAMEREQUEST_SOLR_UPDATES = int(os.getenv('DISABLE_NAMEREQUEST_SOLR_UPDATES', 0))
Expand Down
21 changes: 21 additions & 0 deletions api/dev-scripts/local-db/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
services:
postgres:
container_name: namex-postgres
image: postgres:15
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: unittesting
ports:
- "54345:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d unittesting"]
interval: 10s
timeout: 5s
retries: 5
restart: always

volumes:
postgres_data:
13 changes: 6 additions & 7 deletions api/devops/vaults.gcp.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ NAME_REQUEST_SERVICE_ACCOUNT_CLIENT_ID="op://keycloak/$APP_ENV/name-request-serv
NAME_REQUEST_SERVICE_ACCOUNT_CLIENT_SECRET="op://keycloak/$APP_ENV/name-request-service-account/NAME_REQUEST_SERVICE_ACCOUNT_CLIENT_SECRET"
ENTITY_SERVICE_ACCOUNT_CLIENT_ID="op://keycloak/$APP_ENV/entity-service-account/ENTITY_SERVICE_ACCOUNT_CLIENT_ID"
ENTITY_SERVICE_ACCOUNT_CLIENT_SECRET="op://keycloak/$APP_ENV/entity-service-account/ENTITY_SERVICE_ACCOUNT_CLIENT_SECRET"
NAMEX_DATABASE_UNIX_SOCKET="op://database/$APP_ENV/namex-db-gcp/DATABASE_UNIX_SOCKET"
NAMEX_DATABASE_PORT="op://database/$APP_ENV/namex-db-gcp/DATABASE_PORT"
NAMEX_DATABASE_NAME="op://database/$APP_ENV/namex-db-gcp/DATABASE_NAME"
NAMEX_DATABASE_USERNAME="op://database/$APP_ENV/namex-db-gcp/DATABASE_USERNAME"
NAMEX_DATABASE_PASSWORD="op://database/$APP_ENV/namex-db-gcp/DATABASE_PASSWORD"
REPORT_API_URL="op://API/$APP_ENV/report-api/REPORT_API_URL"
REPORT_API_VERSION="op://API/$APP_ENV/report-api/REPORT_API_VERSION"
PAY_API_URL="op://API/$APP_ENV/pay-api/PAY_API_URL"
Expand Down Expand Up @@ -47,5 +42,9 @@ NAMEX_NR_STATE_TOPIC="op://gcp-queue/$APP_ENV/topics/NAMEX_NR_STATE_TOPIC"
NAMEX_MAILER_TOPIC="op://gcp-queue/$APP_ENV/topics/NAMEX_MAILER_TOPIC"
NOTIFY_DELIVERY_GCNOTIFY_TOPIC="op://gcp-queue/$APP_ENV/topics/NOTIFY_DELIVERY_GCNOTIFY_TOPIC"
NOTIFY_DELIVERY_SMTP_TOPIC="op://gcp-queue/$APP_ENV/topics/NOTIFY_DELIVERY_SMTP_TOPIC"
BUSINESS_GCP_AUTH_KEY="op://gcp-queue/$APP_ENV/a083gt/BUSINESS_GCP_AUTH_KEY"
VPC_CONNECTOR="op://CD/$APP_ENV/namex-api/VPC_CONNECTOR"
VPC_CONNECTOR="op://CD/$APP_ENV/namex-api/VPC_CONNECTOR"
DATABASE_USERNAME="op://database/$APP_ENV/namex-db-gcp/DATABASE_USERNAME"
DATABASE_NAME="op://database/$APP_ENV/namex-db-gcp/DATABASE_NAME"
DATABASE_INSTANCE_CONNECTION_NAME="op://database/$APP_ENV/namex-db-gcp/DATABASE_INSTANCE_CONNECTION_NAME"
DATABASE_SCHEMA="op://database/$APP_ENV/namex-db-gcp/DATABASE_SCHEMA"
DATABASE_OWNER="op://database/$APP_ENV/namex-db-gcp/DATABASE_OWNER"
6 changes: 4 additions & 2 deletions api/gunicorn_config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import os

workers = int(os.environ.get('GUNICORN_PROCESSES', '1'))
threads = int(os.environ.get('GUNICORN_THREADS', '1'))
workers = int(os.environ.get('GUNICORN_PROCESSES', '1')) # pylint: disable=invalid-name
threads = int(os.environ.get('GUNICORN_THREADS', '8')) # pylint: disable=invalid-name
timeout = int(os.environ.get('GUNICORN_TIMEOUT', '0')) # pylint: disable=invalid-name


forwarded_allow_ips = '*'
secure_scheme_headers = {'X-Forwarded-Proto': 'https'}
Loading
Loading