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

Add: initial version of a web-frontend for the Master Server API #3

Merged
merged 1 commit into from Sep 6, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .Dockerignore
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
/.env
/node_modules
9 changes: 9 additions & 0 deletions .github/workflows/testing.yml
Expand Up @@ -7,6 +7,15 @@ on:
pull_request:

jobs:
docker:
name: Docker build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Build
run: docker build .

flake8:
name: Flake8
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
/.env
/node_modules
4 changes: 4 additions & 0 deletions .pyup.yml
@@ -0,0 +1,4 @@
# autogenerated pyup.io config file
# see https://pyup.io/docs/configuration/ for all available options

schedule: every month
1 change: 1 addition & 0 deletions .version
@@ -0,0 +1 @@
dev
32 changes: 32 additions & 0 deletions Dockerfile
@@ -0,0 +1,32 @@
FROM python:3.8-slim

ARG BUILD_DATE=""
ARG BUILD_VERSION="dev"

LABEL maintainer="truebrain@openttd.org"
LABEL org.label-schema.schema-version="1.0"
LABEL org.label-schema.build-date=${BUILD_DATE}
LABEL org.label-schema.version=${BUILD_VERSION}

WORKDIR /code

COPY requirements.txt \
LICENSE \
README.md \
.version \
/code/
# Needed for Sentry to know what version we are running
RUN echo "${BUILD_VERSION}" > /code/.version

RUN pip --no-cache-dir install -r requirements.txt

# Validate that what was installed was what was expected
RUN pip freeze 2>/dev/null > requirements.installed \
&& diff -u --strip-trailing-cr requirements.txt requirements.installed 1>&2 \
|| ( echo "!! ERROR !! requirements.txt defined different packages or versions for installation" \
&& exit 1 ) 1>&2

COPY webclient /code/webclient

ENTRYPOINT ["python", "-m", "webclient"]
CMD ["--api-url", "https://api.master.staging.openttd.org", "run", "-p", "80", "-h", "0.0.0.0"]
339 changes: 339 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions Makefile
@@ -0,0 +1,13 @@
.PHONY: run venv flake8

run: .env/pyvenv.cfg
FLASK_ENV=development .env/bin/python3 -m webclient --api-url "http://localhost:8080" run

venv: .env/pyvenv.cfg

.env/pyvenv.cfg: requirements.txt
python3 -m venv .env
.env/bin/pip install -r requirements.txt

flake8:
python3 -m flake8 webclient
41 changes: 41 additions & 0 deletions README.md
@@ -0,0 +1,41 @@
# Master Server web front-end

[![GitHub License](https://img.shields.io/github/license/OpenTTD/master-server-web)](https://github.com/OpenTTD/master-server-web/blob/master/LICENSE)
[![GitHub Tag](https://img.shields.io/github/v/tag/OpenTTD/master-server-web?include_prereleases&label=stable)](https://github.com/OpenTTD/master-server-web/releases)
[![GitHub commits since latest release](https://img.shields.io/github/commits-since/OpenTTD/master-server-web/latest/master)](https://github.com/OpenTTD/master-server-web/commits/master)

[![GitHub Workflow Status (Testing)](https://img.shields.io/github/workflow/status/OpenTTD/master-server-web/Testing/master?label=master)](https://github.com/OpenTTD/master-server-web/actions?query=workflow%3ATesting)
[![GitHub Workflow Status (Publish Image)](https://img.shields.io/github/workflow/status/OpenTTD/master-server-web/Publish%20image?label=publish)](https://github.com/OpenTTD/master-server-web/actions?query=workflow%3A%22Publish+image%22)
[![GitHub Workflow Status (Deployments)](https://img.shields.io/github/workflow/status/OpenTTD/master-server-web/Deployment?label=deployment)](https://github.com/OpenTTD/master-server-web/actions?query=workflow%3A%22Deployment%22)

[![GitHub deployments (Staging)](https://img.shields.io/github/deployments/OpenTTD/master-server-web/staging?label=staging)](https://github.com/OpenTTD/master-server-web/deployments)
[![GitHub deployments (Production)](https://img.shields.io/github/deployments/OpenTTD/master-server-web/production?label=production)](https://github.com/OpenTTD/master-server-web/deployments)

This is a front-end for the Master Server Public Server listing.
It works together with [master-server](https://github.com/OpenTTD/master-server), which serves the HTTP API.

## Development

This front-end is written in Python 3.7 with Flask.

## Usage

To start it, you are advised to first create a virtualenv:

```bash
python3 -m venv .env
.env/bin/pip install -r requirements.txt
```

After this, you can run the flask application by running:

```bash
make run
```

### Running via docker

```bash
docker build -t openttd/master-server-web:local .
docker run --rm -p 127.0.0.1:5000:80 openttd/master-server-web:local
```
3 changes: 3 additions & 0 deletions requirements.base
@@ -0,0 +1,3 @@
Flask
requests
sentry_sdk
12 changes: 12 additions & 0 deletions requirements.txt
@@ -0,0 +1,12 @@
certifi==2020.6.20
chardet==3.0.4
click==7.1.2
Flask==1.1.2
idna==2.10
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
requests==2.24.0
sentry-sdk==0.17.3
urllib3==1.25.10
Werkzeug==1.0.1
Empty file added webclient/__init__.py
Empty file.
41 changes: 41 additions & 0 deletions webclient/__main__.py
@@ -0,0 +1,41 @@
import click
import flask
import logging

from werkzeug import serving

from . import pages # noqa
from .app import app
from .click import click_additional_options
from .helpers import click_urls
from .sentry import click_sentry


# Patch the werkzeug logger to only log errors
def log_request(self, code="-", size="-"):
if str(code).startswith(("2", "3")):
return
original_log_request(self, code, size)


original_log_request = serving.WSGIRequestHandler.log_request
serving.WSGIRequestHandler.log_request = log_request


@click_additional_options
def click_logging():
logging.basicConfig(
format="%(asctime)s %(levelname)-8s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", level=logging.INFO
)


@click.group(cls=flask.cli.FlaskGroup, create_app=lambda: app)
@click_logging
@click_sentry
@click_urls
def cli():
pass


if __name__ == "__main__":
cli(auto_envvar_prefix="WEBCLIENT")
55 changes: 55 additions & 0 deletions webclient/api.py
@@ -0,0 +1,55 @@
import flask
import requests
import urllib

from .app import app
from .helpers import (
api_host,
redirect,
)

_client_id = None


def api_call(method, path, params=None, json=None, session=None, return_errors=False):
url = api_host() + "/" + "/".join(urllib.parse.quote(p, safe="") for p in path)
headers = None
if session and session.api_token:
headers = {"Authorization": "Bearer " + session.api_token}

error_response = redirect("error", message="API call failed; sorry for the inconvenience")
try:
r = method(url, params=params, headers=headers, json=json)

success = r.status_code in (200, 201, 204)
if not success:
app.logger.warning("API failed: {} {}".format(r.status_code, r.text))

if success:
result = None
try:
result = r.json()
except Exception:
result = None
if return_errors:
return (result, None)
else:
return result
elif r.status_code == 404:
if return_errors:
return (None, "Data not found")
error_response = redirect("error", message="Data not found")
elif return_errors:
error = str(r.json().get("errors", "API call failed"))
return (None, error)
except Exception:
pass

if return_errors:
return (None, "API call failed")
else:
flask.abort(error_response)


def api_get(*args, **kwargs):
return api_call(requests.get, *args, **kwargs)
3 changes: 3 additions & 0 deletions webclient/app.py
@@ -0,0 +1,3 @@
import flask

app = flask.Flask("webclient")
22 changes: 22 additions & 0 deletions webclient/click.py
@@ -0,0 +1,22 @@
def click_additional_options(additional_func):
def decorator(func):
additional_params = []
for param in getattr(additional_func, "__click_params__", []):
additional_params.append(param.name)

def inner_decorator(**kwargs):
additional_kwargs = {param: kwargs[param] for param in additional_params}
additional_func(**additional_kwargs)

# Remove the kwargs that are consumed by the additional_func
[kwargs.pop(kwarg) for kwarg in additional_kwargs]

func(**kwargs)

inner_decorator.__click_params__ = getattr(func, "__click_params__", []) + getattr(
additional_func, "__click_params__", []
)
inner_decorator.__doc__ = func.__doc__
return inner_decorator

return decorator
43 changes: 43 additions & 0 deletions webclient/helpers.py
@@ -0,0 +1,43 @@
import click
import datetime
import flask

from .click import click_additional_options

_api_url = None


@click_additional_options
@click.option(
"--api-url",
help="Master Server API URL.",
default="https://api.master.openttd.org",
show_default=True,
metavar="URL",
)
def click_urls(api_url):
global _api_url
_api_url = api_url


def template(*args, **kwargs):
messages = kwargs.setdefault("messages", [])
if "message" in kwargs:
messages.append(kwargs["message"])
if "message" in flask.request.args:
messages.append(flask.request.args["message"])
kwargs["globals"] = {
"copyright_year": datetime.datetime.utcnow().year,
}

response = flask.make_response(flask.render_template(*args, **kwargs))
response.headers["Content-Security-Policy"] = "default-src 'self'"
return response


def api_host():
return _api_url


def redirect(*args, **kwargs):
return flask.redirect(flask.url_for(*args, **kwargs))
2 changes: 2 additions & 0 deletions webclient/pages/__init__.py
@@ -0,0 +1,2 @@
from . import static # noqa
from . import servers # noqa