9 changed files with 255 additions and 0 deletions
@ -0,0 +1,26 @@
|
||||
FROM alpine/git AS install |
||||
|
||||
RUN echo "unknow" > /git-version.txt |
||||
|
||||
# If the --dirty flag is left out, only the .git directory has to be copied |
||||
COPY . /git/ |
||||
|
||||
RUN find . -type d -name .git -exec git describe --always --dirty > /git-version.txt \; |
||||
|
||||
|
||||
FROM python:3.8 |
||||
|
||||
EXPOSE 8080 |
||||
|
||||
COPY test.sh / |
||||
|
||||
COPY OAS3.yml / |
||||
|
||||
COPY requirements.txt / |
||||
RUN pip install -r requirements.txt |
||||
|
||||
COPY *.py / |
||||
|
||||
COPY --from=install /git-version.txt / |
||||
|
||||
CMD ["python", "-u", "./app.py"] |
@ -0,0 +1,93 @@
|
||||
#!/usr/bin/python3 |
||||
from abc import ABCMeta |
||||
|
||||
import tornado.web |
||||
|
||||
import os |
||||
import subprocess |
||||
from datetime import datetime |
||||
import isodate |
||||
|
||||
import json |
||||
|
||||
import util |
||||
|
||||
|
||||
startup_timestamp = datetime.now() |
||||
|
||||
|
||||
class HealthHandler(tornado.web.RequestHandler, metaclass=ABCMeta): |
||||
# noinspection PyAttributeOutsideInit |
||||
def initialize(self): |
||||
self.git_version = self._load_git_version() |
||||
|
||||
@staticmethod |
||||
def _load_git_version(): |
||||
v = None |
||||
|
||||
# try file git-version.txt first |
||||
gitversion_file = "git-version.txt" |
||||
if os.path.exists(gitversion_file): |
||||
with open(gitversion_file) as f: |
||||
v = f.readline().strip() |
||||
|
||||
# if not available, try git |
||||
if v is None: |
||||
try: |
||||
v = subprocess.check_output(["git", "describe", "--always", "--dirty"], |
||||
cwd=os.path.dirname(__file__)).strip().decode() |
||||
except subprocess.CalledProcessError as e: |
||||
print("Checking git version lead to non-null return code ", e.returncode) |
||||
|
||||
return v |
||||
|
||||
def get(self): |
||||
health = dict() |
||||
health['api-version'] = 'v0' |
||||
|
||||
if self.git_version is not None: |
||||
health['git-version'] = self.git_version |
||||
|
||||
health['timestamp'] = isodate.datetime_isoformat(datetime.now()) |
||||
health['uptime'] = isodate.duration_isoformat(datetime.now() - startup_timestamp) |
||||
|
||||
self.set_header("Content-Type", "application/json") |
||||
self.write(json.dumps(health, indent=4)) |
||||
self.set_status(200) |
||||
|
||||
|
||||
class Oas3Handler(tornado.web.RequestHandler, metaclass=ABCMeta): |
||||
def get(self): |
||||
self.set_header("Content-Type", "text/plain") |
||||
# This is the proposed content type, |
||||
# but browsers like Firefox try to download instead of display the content |
||||
# self.set_header("Content-Type", "text/vnd.yml") |
||||
with open('OAS3.yml', 'r') as f: |
||||
oas3 = f.read() |
||||
self.write(oas3) |
||||
self.finish() |
||||
|
||||
|
||||
def make_app(_auth_provider=None): |
||||
version_path = r"/v[0-9]" |
||||
return tornado.web.Application([ |
||||
(version_path + r"/health", HealthHandler), |
||||
(version_path + r"/oas3", Oas3Handler), |
||||
]) |
||||
|
||||
|
||||
def main(): |
||||
port = util.load_env('PORT', 8080) |
||||
|
||||
# Setup |
||||
|
||||
util.run_tornado_server(make_app(auth_provider), |
||||
server_port=port) |
||||
|
||||
# Teardown |
||||
|
||||
print("Server stopped") |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main() |
@ -0,0 +1,16 @@
|
||||
# Do not forget to create the .env file (see template) |
||||
# before using this container! |
||||
|
||||
version: '2' |
||||
|
||||
services: |
||||
entities_validation_service: |
||||
restart: always |
||||
build: . |
||||
env_file: |
||||
- .env |
||||
environment: |
||||
PORT: 8080 |
||||
ports: |
||||
- $PORT:8080 |
||||
|
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/python3 |
||||
|
||||
from app import make_app |
||||
import util |
||||
|
||||
import unittest |
||||
import tornado.testing |
||||
import json |
||||
|
||||
util.platform_setup() |
||||
|
||||
|
||||
class TestBaseAPI(tornado.testing.AsyncHTTPTestCase): |
||||
"""Example test case""" |
||||
def get_app(self): |
||||
return make_app() |
||||
|
||||
def test_health_endpoint(self): |
||||
response = self.fetch('/v0/health', |
||||
method='GET') |
||||
self.assertEqual(200, response.code, "GET /health must be available") |
||||
|
||||
health = json.loads(response.body.decode()) |
||||
|
||||
self.assertIn('api-version', health, msg="api-version is not provided by health endpoint") |
||||
self.assertEqual("v0", health['api-version'], msg="API version should be v0") |
||||
self.assertIn('git-version', health, msg="git-version is not provided by health endpoint") |
||||
self.assertIn('timestamp', health, msg="timestamp is not provided by health endpoint") |
||||
self.assertIn('uptime', health, msg="uptime is not provided by health endpoint") |
||||
|
||||
def test_oas3(self): |
||||
response = self.fetch('/v0/oas3', |
||||
method='GET') |
||||
self.assertEqual(200, response.code, "GET /oas3 must be available") |
||||
|
||||
# check contents against local OAS3.yml |
||||
with open('OAS3.yml') as oas3f: |
||||
self.assertEqual(response.body.decode(), oas3f.read(), "OAS3 content differs from spec file!") |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
unittest.main() |
@ -0,0 +1,67 @@
|
||||
import os |
||||
import signal |
||||
import platform |
||||
import asyncio |
||||
|
||||
import tornado.ioloop |
||||
import tornado.netutil |
||||
import tornado.httpserver |
||||
|
||||
|
||||
def load_env(key, default): |
||||
if key in os.environ: |
||||
return os.environ[key] |
||||
else: |
||||
return default |
||||
|
||||
|
||||
signal_received = False |
||||
|
||||
|
||||
def platform_setup(): |
||||
"""Platform-specific setup, especially for asyncio.""" |
||||
if platform.system() == 'Windows': |
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) |
||||
|
||||
|
||||
def run_tornado_server(app, server_port=8080): |
||||
platform_setup() |
||||
|
||||
sockets = tornado.netutil.bind_sockets(server_port, '') |
||||
server = tornado.httpserver.HTTPServer(app) |
||||
server.add_sockets(sockets) |
||||
|
||||
port = None |
||||
|
||||
for s in sockets: |
||||
print('Listening on %s, port %d' % s.getsockname()[:2]) |
||||
if port is None: |
||||
port = s.getsockname()[1] |
||||
|
||||
ioloop = tornado.ioloop.IOLoop.instance() |
||||
|
||||
def register_signal(sig, _frame): |
||||
# noinspection PyGlobalUndefined |
||||
global signal_received |
||||
print("%s received, stopping server" % sig) |
||||
server.stop() # no more requests are accepted |
||||
signal_received = True |
||||
|
||||
def stop_on_signal(): |
||||
# noinspection PyGlobalUndefined |
||||
global signal_received |
||||
if signal_received: |
||||
ioloop.stop() |
||||
print("IOLoop stopped") |
||||
|
||||
tornado.ioloop.PeriodicCallback(stop_on_signal, 1000).start() |
||||
signal.signal(signal.SIGTERM, register_signal) |
||||
print("Starting server") |
||||
|
||||
global signal_received |
||||
while not signal_received: |
||||
try: |
||||
ioloop.start() |
||||
except KeyboardInterrupt: |
||||
print("Keyboard interrupt") |
||||
register_signal(signal.SIGTERM, None) |
Loading…
Reference in new issue