Skip to content

Commit

Permalink
Mijn 6927 env agnostisch (#44)
Browse files Browse the repository at this point in the history
* Add Azure pipeline file
* Adapt to multiple environments
* Add dependabot workflow
  • Loading branch information
timvanoostrom authored Oct 11, 2023
1 parent d0d5677 commit b4223d8
Show file tree
Hide file tree
Showing 18 changed files with 404 additions and 243 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Opdrachten Team Dependabot

on:
schedule: # Run the script every day at 6am UTC
- cron: "0 6 * * *"
workflow_dispatch:

jobs:
dependabot:
name: Templates
uses: amsterdam/github-workflows/.github/workflows/dependabot.yml@v1
secrets: inherit
with:
check_diff: true
50 changes: 42 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
FROM python:3.11-bookworm as base

ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=off
ENV TZ=Europe/Amsterdam
ENV PYTHONUNBUFFERED=1
ENV PIP_NO_CACHE_DIR=off

WORKDIR /api

RUN apt-get update \
&& apt-get dist-upgrade -y \
&& apt-get autoremove -y \
&& apt-get install --no-install-recommends -y \
&& apt-get install -y --no-install-recommends \
nano \
&& rm -rf /var/lib/apt/lists/* /var/cache/debconf/*-old \
openssh-server \
&& pip install --upgrade pip \
&& pip install uwsgi

Expand All @@ -20,10 +21,43 @@ RUN pip install -r requirements.txt

COPY ./scripts /api/scripts
COPY ./app /api/app
COPY ./uwsgi.ini /api
COPY ./test.sh /api
COPY .flake8 /api


FROM base as tests

COPY conf/test.sh /api/
COPY .flake8 /api/

RUN chmod u+x /api/test.sh

CMD uwsgi --uid www-data --gid www-data --ini /api/uwsgi.ini
ENTRYPOINT [ "/bin/sh", "/api/test.sh"]

FROM base as publish

# ssh ( see also: https://github.com/Azure-Samples/docker-django-webapp-linux )
ENV SSH_PASSWD "root:Docker!"

EXPOSE 8000
ENV PORT 8000

ARG MA_OTAP_ENV
ENV MA_OTAP_ENV=$MA_OTAP_ENV

ARG MA_BUILD_ID
ENV MA_BUILD_ID=$MA_BUILD_ID

ARG MA_GIT_SHA
ENV MA_GIT_SHA=$MA_GIT_SHA

COPY conf/uwsgi.ini /api/
COPY conf/docker-entrypoint.sh /api/
COPY conf/sshd_config /etc/ssh/

RUN chmod u+x /api/docker-entrypoint.sh \
&& echo "$SSH_PASSWD" | chpasswd

ENTRYPOINT [ "/bin/sh", "/api/docker-entrypoint.sh"]

FROM publish as publish-final

COPY /files /app/files
46 changes: 20 additions & 26 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,42 @@ def retagAndPush(String imageName, String currentTag, String newTag)
String BRANCH = "${env.BRANCH_NAME}"
String IMAGE_NAME = "mijnams/wmoned"
String IMAGE_TAG = "${IMAGE_NAME}:${env.BUILD_NUMBER}"
String IMAGE_TEST = "${IMAGE_NAME}:test-${env.BUILD_NUMBER}"
String CMDB_ID = "app_mijn-wmoned"

node {
stage("Checkout") {
checkout scm
}

stage("Build image") {
docker.withRegistry(DOCKER_REGISTRY_HOST, "docker_registry_auth") {
def image = docker.build(IMAGE_TAG)
image.push()
}
}
}

// Skipping tests for the test branch
if (BRANCH != "test-acc") {
node {
// Skipping tests for the test branch
if (BRANCH != "test-acc") {
stage("Test") {
docker.withRegistry(DOCKER_REGISTRY_HOST, "docker_registry_auth") {
docker.image(IMAGE_TAG).pull()
sh "docker run --rm ${IMAGE_TAG} /api/test.sh"
sh "docker build -t ${IMAGE_TEST} " +
"--target=tests " +
"--shm-size 1G " +
"."
sh "docker run --rm ${IMAGE_TEST}"
}
}
}
}

if (BRANCH == "test-acc" || BRANCH == "main") {
node {
stage("Build image") {
docker.withRegistry(DOCKER_REGISTRY_HOST, "docker_registry_auth") {
def image = docker.build(IMAGE_TAG, "--target=publish .")
image.push()
}
}

if (BRANCH == "test-acc" || BRANCH == "main") {
stage("Push acceptance image") {
docker.withRegistry(DOCKER_REGISTRY_HOST, "docker_registry_auth") {
docker.image(IMAGE_TAG).pull()
retagAndPush(IMAGE_NAME, env.BUILD_NUMBER, "acceptance")
}
}
}

node {
stage("Deploy to ACC") {
build job: "Subtask_Openstack_Playbook",
parameters: [
Expand All @@ -58,23 +56,19 @@ if (BRANCH == "test-acc" || BRANCH == "main") {
]
}
}
}

if (BRANCH == "production-release") {
stage("Waiting for approval") {
input "Deploy to Production?"
}
if (BRANCH == "production-release") {
stage("Waiting for approval") {
input "Deploy to Production?"
}

node {
stage("Push production image") {
docker.withRegistry(DOCKER_REGISTRY_HOST, "docker_registry_auth") {
docker.image(IMAGE_TAG).pull()
retagAndPush(IMAGE_NAME, env.BUILD_NUMBER, "production")
}
}
}

node {
stage("Deploy") {
build job: "Subtask_Openstack_Playbook",
parameters: [
Expand Down
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This Makefile is based on the Makefile defined in the Python Best Practices repository:
# https://git.data.amsterdam.nl/Datapunt/python-best-practices/blob/master/dependency_management/

PYTHON = python3

pip-tools:
pip install pip-tools

requirements: pip-tools ## Upgrade requirements (in requirements-root.txt) to latest versions and compile requirements.txt
pip-compile --upgrade --output-file requirements.txt requirements-root.txt

diff:
@python3 ./scripts/diff.py
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

### Updating dependencies

Direct dependencies are specified in `requirements-root.txt`. These should not have pinned a version (except when needed)
Direct dependencies are specified in `requirements-root.txt`. These packages usually do not have pinned versions so we can update versions easily.

- `pip install -r requirements-root.txt`
- `pip freeze > requirements.txt`
- `make requirements`
- `pip install -r requirements.txt`
7 changes: 6 additions & 1 deletion app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from flask_httpauth import HTTPTokenAuth
import jwt

from app.config import VERIFY_JWT_SIGNATURE

auth = HTTPTokenAuth(scheme="Bearer")

PROFILE_TYPE_PRIVATE = "private"
Expand Down Expand Up @@ -118,7 +120,10 @@ def get_verified_token_data(token):


def get_user_profile_from_token(token):
token_data = get_verified_token_data(token)
if VERIFY_JWT_SIGNATURE:
token_data = get_verified_token_data(token)
else:
token_data = jwt.api_jwt.decode(token, options={"verify_signature": False})

profile_type = get_profile_type(token_data)
profile_id = get_profile_id(token_data)
Expand Down
12 changes: 8 additions & 4 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@
# Environment determination
IS_PRODUCTION = SENTRY_ENV == "production"
IS_ACCEPTANCE = SENTRY_ENV == "acceptance"
IS_AP = IS_PRODUCTION or IS_ACCEPTANCE
IS_DEV = os.getenv("FLASK_ENV") == "development" and not IS_AP
IS_DEV = SENTRY_ENV == "development"
IS_TEST = SENTRY_ENV == "test"

IS_TAP = IS_PRODUCTION or IS_ACCEPTANCE or IS_TEST
IS_AP = IS_ACCEPTANCE or IS_PRODUCTION
IS_OT = IS_DEV or IS_TEST

# App constants
ENABLE_OPENAPI_VALIDATION = os.getenv("ENABLE_OPENAPI_VALIDATION", not IS_AP)
VERIFY_JWT_SIGNATURE = os.getenv("VERIFY_JWT_SIGNATURE", IS_AP)

# ZORGNED specific config
ZORGNED_API_REQUEST_TIMEOUT_SECONDS = 30
ZORGNED_GEMEENTE_CODE = "0363"
ZORGNED_API_TOKEN = os.getenv("WMO_NED_API_TOKEN")
ZORGNED_API_TOKEN = os.getenv("ZORGNED_API_TOKEN", os.getenv("WMO_NED_API_TOKEN"))
ZORGNED_API_URL = os.getenv("ZORGNED_API_URL")
ZORGNED_DOCUMENT_ATTACHMENTS_ACTIVE = not IS_PRODUCTION

Expand Down
50 changes: 1 addition & 49 deletions app/helpers.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,9 @@
import os
from datetime import date, datetime
from functools import wraps

import yaml
from cryptography.fernet import Fernet
from flask import g, request
from flask.helpers import make_response
from openapi_core import create_spec
from openapi_core.contrib.flask import FlaskOpenAPIRequest, FlaskOpenAPIResponse
from openapi_core.validation.request.validators import RequestValidator
from openapi_core.validation.response.validators import ResponseValidator
from yaml import load

from app.config import (
BASE_PATH,
ENABLE_OPENAPI_VALIDATION,
WMONED_FERNET_ENCRYPTION_KEY,
)

openapi_spec = None


def get_openapi_spec():
global openapi_spec
if not openapi_spec:
with open(os.path.join(BASE_PATH, "openapi.yml"), "r") as spec_file:
spec_dict = load(spec_file, Loader=yaml.Loader)
openapi_spec = create_spec(spec_dict)

return openapi_spec


def validate_openapi(function):
@wraps(function)
def validate(*args, **kwargs):
if ENABLE_OPENAPI_VALIDATION:
spec = get_openapi_spec()
openapi_request = FlaskOpenAPIRequest(request)
validator = RequestValidator(spec)
result = validator.validate(openapi_request)
result.raise_for_errors()

response = function(*args, **kwargs)

if ENABLE_OPENAPI_VALIDATION:
openapi_response = FlaskOpenAPIResponse(response)
validator = ResponseValidator(spec)
result = validator.validate(openapi_request, openapi_response)
result.raise_for_errors()

return response

return validate
from app.config import WMONED_FERNET_ENCRYPTION_KEY


def success_response_json(response_content):
Expand Down
23 changes: 12 additions & 11 deletions app/server.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import logging

import sentry_sdk
import os
from flask import Flask, make_response
from requests.exceptions import HTTPError
from sentry_sdk.integrations.flask import FlaskIntegration

import app.zorgned_service as zorgned
from app import auth
from app.config import IS_DEV, SENTRY_DSN, UpdatedJSONProvider
from app.helpers import (
decrypt,
error_response_json,
success_response_json,
validate_openapi,
)
from app.config import IS_OT, SENTRY_DSN, UpdatedJSONProvider
from app.helpers import decrypt, error_response_json, success_response_json

app = Flask(__name__)
app.json = UpdatedJSONProvider(app)
Expand All @@ -26,7 +22,6 @@

@app.route("/wmoned/voorzieningen", methods=["GET"])
@auth.login_required
@validate_openapi
def get_voorzieningen():
user = auth.get_current_user()
voorzieningen = zorgned.get_voorzieningen(user["id"])
Expand All @@ -35,7 +30,6 @@ def get_voorzieningen():

@app.route("/wmoned/document/<string:doc_id_encrypted>", methods=["GET"])
@auth.login_required
@validate_openapi
def get_document(doc_id_encrypted):
user = auth.get_current_user()
doc_id = decrypt(doc_id_encrypted)
Expand All @@ -47,9 +41,16 @@ def get_document(doc_id_encrypted):
return new_response


@app.route("/")
@app.route("/status/health")
def health_check():
return success_response_json("OK")
return success_response_json(
{
"gitSha": os.getenv("MA_GIT_SHA", -1),
"buildId": os.getenv("MA_BUILD_ID", -1),
"otapEnv": os.getenv("MA_OTAP_ENV", None),
}
)


@app.errorhandler(Exception)
Expand All @@ -62,7 +63,7 @@ def handle_error(error):

logging.exception(error, extra={"error_message_original": error_message_original})

if IS_DEV: # pragma: no cover
if IS_OT: # pragma: no cover
msg_auth_exception = error_message_original
msg_request_http_error = error_message_original
msg_server_error = error_message_original
Expand Down
Loading

0 comments on commit b4223d8

Please sign in to comment.