Skip to content

Commit

Permalink
Merge pull request #74 from geoadmin/bug-docker
Browse files Browse the repository at this point in the history
Fix docker image missing dependencies
  • Loading branch information
ltshb authored Sep 17, 2024
2 parents ec5b061 + 969f3cd commit fb1daa8
Show file tree
Hide file tree
Showing 8 changed files with 967 additions and 790 deletions.
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

0 comments on commit fb1daa8

Please sign in to comment.