Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PB-887: Fix docker image missing dependencies #74

Merged
merged 4 commits into from
Sep 17, 2024
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
47 changes: 36 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
FROM python:3.11-slim-buster
RUN groupadd -r geoadmin && useradd -u 1000 -r -s /bin/false -g geoadmin geoadmin
FROM python:3.11-slim-buster AS base

ENV USER=geoadmin
ENV GROUP=geoadmin
ENV INSTALL_DIR=/opt/service-kml
ENV SRC_DIR=/usr/local/src/service-kml
ENV PIPENV_VENV_IN_PROJECT=1

RUN groupadd -r ${GROUP} && useradd -r -s /bin/false -g ${GROUP} ${USER} \
&& mkdir -p ${INSTALL_DIR}/app && chown ${USER}:${GROUP} ${INSTALL_DIR}/app

###########################################################
# Builder container
FROM base AS builder

# HERE : install relevant packages
RUN pip3 install pipenv \
&& pipenv --version
&& pipenv --version \
&& mkdir -p ${SRC_DIR} && chown ${USER}:${GROUP} ${SRC_DIR}

COPY Pipfile.lock ${SRC_DIR}
RUN cd ${SRC_DIR} && pipenv sync

COPY Pipfile.lock /tmp/
RUN cd /tmp && pipenv sync
COPY --chown=${USER}:${GROUP} app ${INSTALL_DIR}/app
COPY --chown=${USER}:${GROUP} wsgi.py ${INSTALL_DIR}/

WORKDIR /app
COPY --chown=geoadmin:geoadmin ./ /app/
###########################################################
# Container to use in production
FROM base AS production

# Activate virtual environnment
ENV VIRTUAL_ENV=${INSTALL_DIR}/.venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ENV PYTHONHOME=""

ARG GIT_HASH=unknown
ARG GIT_BRANCH=unknown
Expand All @@ -24,12 +44,17 @@ LABEL git.dirty="$GIT_DIRTY"
LABEL version=$VERSION
LABEL author=$AUTHOR

# Install venv and app from builder stage
COPY --from=builder ${SRC_DIR}/.venv/ ${INSTALL_DIR}/.venv/
COPY --from=builder ${INSTALL_DIR}/ ${INSTALL_DIR}/

# Overwrite the version.py from source with the actual version
RUN echo "APP_VERSION = '$VERSION'" > /app/app/version.py
RUN echo "APP_VERSION = '$VERSION'" > ${INSTALL_DIR}/app/version.py

USER geoadmin
WORKDIR ${INSTALL_DIR}
USER ${USER}

EXPOSE $HTTP_PORT
EXPOSE ${HTTP_PORT}

# Use a real WSGI server
ENTRYPOINT ["python3", "wsgi.py"]
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ dockerrun: clean_logs dockerbuild $(LOGS_DIR)
--env-file=${PWD}/${ENV_FILE} \
--env LOGS_DIR=/logs \
--env SCRIPT_NAME=$(ROUTE_PREFIX) \
--env GUNICORN_WORKER_TMP_DIR= \
--mount type=bind,source="${LOGS_DIR}",target=/logs \
$(DOCKER_IMG_LOCAL_TAG)

Expand Down Expand Up @@ -218,4 +219,4 @@ $(VOLUMES_MINIO):


$(LOGS_DIR):
mkdir -p -m=777 $(LOGS_DIR)
mkdir -p -m=777 $(LOGS_DIR)
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ logging-utilities = "~=4.0.0"
defusedxml = "~=0.7.1"
boto3 = "~=1.26.158"
botocore = "~=1.29.158"
moto = {extras = [ "s3",], version = "*"}
python-dotenv = "~=1.0.0"

[dev-packages]
yapf = "*"
nose2 = "*"
pylint = "*"
pylint-flask = "*"
moto = {extras = [ "s3", "dynamodb"], version = "~=4.1"}

[requires]
python_version = "3.11"
1,424 changes: 788 additions & 636 deletions Pipfile.lock

Large diffs are not rendered by default.

141 changes: 2 additions & 139 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,139 +1,2 @@
import logging
import re
import time

from werkzeug.exceptions import HTTPException

from flask import Flask
from flask import abort
from flask import g
from flask import request
from flask import url_for

from app.helpers.utils import get_registered_method
from app.helpers.utils import make_error_msg
from app.settings import ALLOWED_DOMAINS_PATTERN
from app.settings import CACHE_CONTROL
from app.settings import CACHE_CONTROL_4XX

logger = logging.getLogger(__name__)
logger_routes = logging.getLogger('app.routes')

# Standard Flask application initialisation

app = Flask(__name__)


def is_domain_allowed(domain):
return re.match(ALLOWED_DOMAINS_PATTERN, domain) is not None


# Add quick log of the routes used to all request.
# Important: this should be the first before_request method, to ensure
# a failure in another pre request method would stop logging.
@app.before_request
def log_route():
g.setdefault('request_started', time.time())
logger_routes.debug('%s %s', request.method, request.path)


# Add CORS Headers to all request
@app.after_request
def add_cors_header(response):
# Do not add CORS header to internal /checker endpoint.
if request.endpoint == 'checker':
return response

response.headers['Access-Control-Allow-Origin'] = request.host_url
if 'Origin' in request.headers and is_domain_allowed(request.headers['Origin']):
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
response.headers['Vary'] = 'Origin'

# Always add the allowed methods.
response.headers.set(
'Access-Control-Allow-Methods', ', '.join(get_registered_method(app, request.url_rule))
)
response.headers.set('Access-Control-Allow-Headers', '*')
return response


@app.after_request
def add_cache_control_header(response):
# For /checker route we let the frontend proxy decide how to cache it.
if request.method == 'GET' and request.endpoint != 'checker':
if response.status_code >= 400:
response.headers.set('Cache-Control', CACHE_CONTROL_4XX)
else:
response.headers.set('Cache-Control', CACHE_CONTROL)
if 'no-cache' in CACHE_CONTROL:
response.headers.set('Expire', 0)
return response


# Reject request from non allowed origins
@app.before_request
def validate_origin():
# The Origin headers is automatically set by the browser and cannot be changed by the javascript
# application. Unfortunately this header is only set if the request comes from another origin.
# Sec-Fetch-Site header is set to `same-origin` by most of the browser except by Safari !
# The best protection would be to use the Sec-Fetch-Site and Origin header, however this is
# not supported by Safari. Therefore we added a fallback to the Referer header for Safari.
sec_fetch_site = request.headers.get('Sec-Fetch-Site', None)
origin = request.headers.get('Origin', None)
referrer = request.headers.get('Referer', None)

if origin is not None:
if is_domain_allowed(origin):
return
logger.error('Origin=%s is not allowed', origin)
abort(403, 'Permission denied')

if sec_fetch_site is not None:
if sec_fetch_site in ['same-origin', 'same-site']:
return
logger.error('Sec-Fetch-Site=%s is not allowed', sec_fetch_site)
abort(403, 'Permission denied')

if referrer is not None:
if is_domain_allowed(referrer):
return
logger.error('Referer=%s is not allowed', referrer)
abort(403, 'Permission denied')

logger.error('Referer and/or Origin and/or Sec-Fetch-Site headers not set')
abort(403, 'Permission denied')


@app.after_request
def log_response(response):
logger_routes.info(
"%s %s - %s",
request.method,
request.path,
response.status,
extra={
'response':
{
"status_code": response.status_code,
"headers": dict(response.headers.items()),
"json": response.json
},
"duration": time.time() - g.get('request_started', time.time())
}
)
return response


# Register error handler to make sure that every error returns a json answer
@app.errorhandler(Exception)
def handle_exception(err):
"""Return JSON instead of HTML for HTTP errors."""
if isinstance(err, HTTPException):
logger.error(err)
return make_error_msg(err.code, err.description)

logger.exception('Unexpected exception: %s', err)
return make_error_msg(500, "Internal server error, please consult logs")


from app import routes # pylint: disable=wrong-import-position
from app import routes
from app.app import app
135 changes: 135 additions & 0 deletions app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import logging
import re
import time

from werkzeug.exceptions import HTTPException

from flask import Flask
from flask import abort
from flask import g
from flask import request

from app.helpers.utils import get_registered_method
from app.helpers.utils import make_error_msg
from app.settings import ALLOWED_DOMAINS_PATTERN
from app.settings import CACHE_CONTROL
from app.settings import CACHE_CONTROL_4XX

logger = logging.getLogger(__name__)
logger_routes = logging.getLogger('app.routes')

# Standard Flask application initialisation

app = Flask(__name__)


def is_domain_allowed(domain):
return re.match(ALLOWED_DOMAINS_PATTERN, domain) is not None


# Add quick log of the routes used to all request.
# Important: this should be the first before_request method, to ensure
# a failure in another pre request method would stop logging.
@app.before_request
def log_route():
g.setdefault('request_started', time.time())
logger_routes.debug('%s %s', request.method, request.path)


# Add CORS Headers to all request
@app.after_request
def add_cors_header(response):
# Do not add CORS header to internal /checker endpoint.
if request.endpoint == 'checker':
return response

response.headers['Access-Control-Allow-Origin'] = request.host_url
if 'Origin' in request.headers and is_domain_allowed(request.headers['Origin']):
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
response.headers['Vary'] = 'Origin'

# Always add the allowed methods.
response.headers.set(
'Access-Control-Allow-Methods', ', '.join(get_registered_method(app, request.url_rule))
)
response.headers.set('Access-Control-Allow-Headers', '*')
return response


@app.after_request
def add_cache_control_header(response):
# For /checker route we let the frontend proxy decide how to cache it.
if request.method == 'GET' and request.endpoint != 'checker':
if response.status_code >= 400:
response.headers.set('Cache-Control', CACHE_CONTROL_4XX)
else:
response.headers.set('Cache-Control', CACHE_CONTROL)
if 'no-cache' in CACHE_CONTROL:
response.headers.set('Expire', 0)
return response


# Reject request from non allowed origins
@app.before_request
def validate_origin():
# The Origin headers is automatically set by the browser and cannot be changed by the javascript
# application. Unfortunately this header is only set if the request comes from another origin.
# Sec-Fetch-Site header is set to `same-origin` by most of the browser except by Safari !
# The best protection would be to use the Sec-Fetch-Site and Origin header, however this is
# not supported by Safari. Therefore we added a fallback to the Referer header for Safari.
sec_fetch_site = request.headers.get('Sec-Fetch-Site', None)
origin = request.headers.get('Origin', None)
referrer = request.headers.get('Referer', None)

if origin is not None:
if is_domain_allowed(origin):
return
logger.error('Origin=%s is not allowed', origin)
abort(403, 'Permission denied')

if sec_fetch_site is not None:
if sec_fetch_site in ['same-origin', 'same-site']:
return
logger.error('Sec-Fetch-Site=%s is not allowed', sec_fetch_site)
abort(403, 'Permission denied')

if referrer is not None:
if is_domain_allowed(referrer):
return
logger.error('Referer=%s is not allowed', referrer)
abort(403, 'Permission denied')

logger.error('Referer and/or Origin and/or Sec-Fetch-Site headers not set')
abort(403, 'Permission denied')


@app.after_request
def log_response(response):
logger_routes.info(
"%s %s - %s",
request.method,
request.path,
response.status,
extra={
'response':
{
"status_code": response.status_code,
"headers": dict(response.headers.items()),
"json": response.json
},
"duration": time.time() - g.get('request_started', time.time())
}
)
return response


# Register error handler to make sure that every error returns a json answer
@app.errorhandler(Exception)
def handle_exception(err):
"""Return JSON instead of HTML for HTTP errors."""
if isinstance(err, HTTPException):
logger.error(err)
return make_error_msg(err.code, err.description)

logger.exception('Unexpected exception: %s', err)
return make_error_msg(500, "Internal server error, please consult logs")
2 changes: 1 addition & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from flask import make_response
from flask import request

from app import app
from app.app import app
from app.helpers.dynamodb import get_db
from app.helpers.s3 import get_storage
from app.helpers.utils import get_json_metadata
Expand Down
Loading