diff --git a/.dockerignore b/.dockerignore index 33957286..88f752a0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ virtual-env .vscode db.sqlite -postgres_data \ No newline at end of file +postgres_data diff --git a/.gitignore b/.gitignore index 8813a2d2..6a185cea 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ dist/ .python-version .mypy_cache/ +env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..686e319b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +# Stage 1 - Install Python Requirement and Response +FROM python:3.7-slim as builder + +WORKDIR /src + +RUN apt-get update && apt-get install -y gcc libpq-dev + +RUN pip install uwsgi + +COPY ./response/ /src/response/ +COPY ./setup.py /src/ +COPY ./README.md /src/ +COPY ./MANIFEST.in /src/ +COPY ./LICENSE /src/ + +RUN pip install . + +# Stage 2 - Install/Obtain supercronic for cron +FROM python:3.7-slim as supercronic + +RUN apt-get update && apt-get install -y curl + +ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.1.11/supercronic-linux-amd64 \ + SUPERCRONIC=supercronic-linux-amd64 \ + SUPERCRONIC_SHA1SUM=a2e2d47078a8dafc5949491e5ea7267cc721d67c + +RUN curl -fsSLO "$SUPERCRONIC_URL" \ + && echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \ + && chmod +x "$SUPERCRONIC" \ + && mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \ + && ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic + +# Stage 3 - FINAL - Put the pieces together +FROM python:3.7-slim + +WORKDIR /app +ENTRYPOINT ["/app/entrypoint.sh"] + +RUN apt-get update && apt-get install -y wget netcat postgresql-client && apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY --from=supercronic /usr/local/bin/supercronic /usr/local/bin/supercronic +COPY --from=builder /usr/local/lib/python3.7/site-packages/ /usr/local/lib/python3.7/site-packages/ +COPY --from=builder /usr/local/bin/ /usr/local/bin/ + +COPY ./app/ /app/ +COPY ./entrypoint.sh /app/entrypoint.sh +COPY ./crontab /app/crontab + +RUN mkdir -p /app/static && chown -R nobody /app/static + +USER nobody \ No newline at end of file diff --git a/demo/demo/__init__.py b/app/__init__.py similarity index 100% rename from demo/demo/__init__.py rename to app/__init__.py diff --git a/demo/manage.py b/app/manage.py similarity index 88% rename from demo/manage.py rename to app/manage.py index c98d68ed..6af94a0b 100755 --- a/demo/manage.py +++ b/app/manage.py @@ -5,7 +5,7 @@ def main(): - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.prod") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/demo/demo/settings/base.py b/app/settings/base.py similarity index 97% rename from demo/demo/settings/base.py rename to app/settings/base.py index ebeb8deb..da7eeaa2 100644 --- a/demo/demo/settings/base.py +++ b/app/settings/base.py @@ -1,5 +1,5 @@ """ -Django settings for demo project. +Django settings for response project. Generated by 'django-admin startproject' using Django 2.2.3. @@ -22,7 +22,6 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ @@ -47,7 +46,7 @@ "after_response", "rest_framework", "bootstrap4", - "response.apps.ResponseConfig", + "response.apps.ResponseConfig" ] MIDDLEWARE = [ @@ -60,7 +59,7 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "demo.urls" +ROOT_URLCONF = "urls" TEMPLATES = [ { @@ -78,7 +77,7 @@ } ] -WSGI_APPLICATION = "demo.wsgi.application" +WSGI_APPLICATION = "wsgi.application" # Database @@ -125,7 +124,6 @@ STATIC_URL = "/static/" STATIC_ROOT = "static" - # Django Rest Framework # https://www.django-rest-framework.org/ diff --git a/demo/demo/settings/prod.py b/app/settings/prod.py similarity index 97% rename from demo/demo/settings/prod.py rename to app/settings/prod.py index a27184d3..a86d638b 100644 --- a/demo/demo/settings/prod.py +++ b/app/settings/prod.py @@ -60,3 +60,5 @@ INCIDENT_REPORT_CHANNEL_ID = os.getenv( "INCIDENT_REPORT_CHANNEL_ID" ) or SLACK_CLIENT.get_channel_id(INCIDENT_REPORT_CHANNEL_NAME) + +SECRET_KEY = os.getenv("SECRET_KEY") diff --git a/demo/demo/urls.py b/app/urls.py similarity index 79% rename from demo/demo/urls.py rename to app/urls.py index d201422e..ff0b704c 100644 --- a/demo/demo/urls.py +++ b/app/urls.py @@ -1,4 +1,4 @@ -"""demo URL Configuration +"""response app URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/2.2/topics/http/urls/ @@ -15,10 +15,12 @@ """ from django.contrib import admin from django.urls import include, path +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), path("slack/", include("response.slack.urls")), path("core/", include("response.core.urls")), - path("", include("response.ui.urls")), -] + path("", include("response.ui.urls")) +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/demo/demo/wsgi.py b/app/wsgi.py similarity index 73% rename from demo/demo/wsgi.py rename to app/wsgi.py index f3d69374..5d0b4623 100644 --- a/demo/demo/wsgi.py +++ b/app/wsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for demo project. +WSGI config for response app project. It exposes the WSGI callable as a module-level variable named ``application``. @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings.dev") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.prod") application = get_wsgi_application() diff --git a/crontab b/crontab new file mode 100644 index 00000000..87ffe888 --- /dev/null +++ b/crontab @@ -0,0 +1 @@ +* * * * * wget -qO- $RESPONSE_URL/slack/cron_minute \ No newline at end of file diff --git a/demo/Dockerfile.cron b/demo/Dockerfile.cron deleted file mode 100644 index d94bb359..00000000 --- a/demo/Dockerfile.cron +++ /dev/null @@ -1,7 +0,0 @@ -FROM alpine - -# Run every minute -RUN echo '* * * * * wget -qO- response:8000/slack/cron_minute' > /etc/crontabs/root - -# Run crond in the foreground -CMD ["crond", "-f"] \ No newline at end of file diff --git a/demo/Dockerfile.response b/demo/Dockerfile.response deleted file mode 100644 index 2fff54a1..00000000 --- a/demo/Dockerfile.response +++ /dev/null @@ -1,14 +0,0 @@ -FROM python:3.7-slim - -RUN apt-get update && apt-get install -y --no-install-recommends \ - netcat \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app -COPY requirements.txt /app -RUN pip install -r requirements.txt - -COPY . /app/ - -ENTRYPOINT ["python", "manage.py"] -CMD ["runserver", "0.0.0.0:8000"] diff --git a/demo/README.md b/demo/README.md index 5c91876a..d0913254 100644 --- a/demo/README.md +++ b/demo/README.md @@ -1,25 +1,12 @@ -# Response Demo App +# Response -This is an example Django project using the django-incident-response package that you can use to test drive Response locally. You'll need access to be able to add and configure apps in a Slack workspace of your choosing - you can sign up for a free account, if necessary. - -All commands should be run from this directory (`demo`). - ---- - -# Quick Start - -The following steps explain how to create a Slack app, run Response locally, and configure everything to develop and test locally. - -Broadly speaking, this sets things up as below: -

- -

+The easiest way to get started is with docker! ## 1. Create a Slack App Follow [these instructions](../docs/slack_app_create.md) to create a new Slack App. -## 2. Configure the demo app +## 2. Configure Response! The demo app is configured using environment variables in a `.env` file. Create your own: ``` @@ -34,47 +21,23 @@ and update the variables in it: | `INCIDENT_CHANNEL_NAME` | When an incident is declared, a 'headline' post is sent to a central channel.

The default channel is `incidents` - change `INCIDENT_CHANNEL_NAME` if you want them to be sent somewhere else (note: do not include the #). | | `INCIDENT_BOT_NAME` | We want to invite the Bot to all Incident Channels, so need to know its ID. You can find/configure this in the App Home section of the Slack App.

The default bot name is `incident` - change the `INCIDENT_BOT_NAME` if your app uses something different.

⚠️ If your chosen username has ever been used on your Slack workspace, Slack will silently change the underlying username and won't show you the actual name in use anywhere. The easiest way to find the exact name you need to use is to make the API call directly [here](https://api.slack.com/methods/users.list/test), using your bot token from above, and searching the response for you APP ID, which is shown in the Basic Info page. | -## 3. Run Response - -From the root of the Response directory run: - -``` -docker-compose up -``` - -This starts the following containers: - -- response: the main Response app -- postgres: the DB used by the app to store incident data -- cron: a container running cron, configured to hit an endpoint in Response every minute -- ngrok: ngrok in a container, providing a public URL pointed at Response. - +## 3. Start Postgres -You can tail the logs of all containers with: -``` -docker-compose logs -f +```bash +docker-compose up -d db ``` -Ngrok establishes a new, random, URL any time it starts. You'll need this to complete the Slack app setup, so look for an entry like this and make note of the https://abc123.ngrok.io address - this is your public URL. +## 4. Run Response -``` -ngrok | The ngrok tunnel is active -ngrok | https://6bb315c8.ngrok.io ---> response:8000 -``` - -If everything has started successfully, you should see logs that look like this: - -``` -response | Django version 2.1.7, using settings 'response.settings.dev' -response | Starting development server at http://0.0.0.0:8000/ -response | Quit the server with CONTROL-C. +```bash +docker run -it --rm --env-file .env --ports 8000:8000 response ``` -## 4. Complete the Slack App Setup +## 5. Complete the Slack App Setup Head back to the Slack web UI and complete the configuration of your app, as [described here](../docs/slack_app_config.md). -## 5. Test it's working! +## 6. Test it's working! In Slack, start an incident with `/incident Something's happened`. You should see a post in your incidents channel! diff --git a/demo/demo/settings/dev.py b/demo/demo/settings/dev.py deleted file mode 100644 index 7937371b..00000000 --- a/demo/demo/settings/dev.py +++ /dev/null @@ -1,63 +0,0 @@ -import os - -from .base import * # noqa: F401, F403 -from .base import SLACK_CLIENT, get_env_var - -SITE_URL = "http://localhost:8000" - -if os.environ.get("POSTGRES"): - DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "HOST": os.getenv("DB_HOST", "db"), - "PORT": os.getenv("DB_PORT", "5432"), - "USER": os.getenv("DB_USER", "postgres"), - "NAME": os.getenv("DB_NAME", "postgres"), - } - } - - -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "simple": { - "format": " {levelname:5s} - {module:10.15s} - {message}", - "style": "{", - } - }, - "handlers": { - "console": { - "level": "INFO", - "class": "logging.StreamHandler", - "formatter": "simple", - } - }, - "loggers": { - "": { - "handlers": ["console"], - "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), - "propagate": False, - } - }, -} - -RESPONSE_LOGIN_REQUIRED = False - -SLACK_TOKEN = get_env_var("SLACK_TOKEN") -SLACK_SIGNING_SECRET = get_env_var("SLACK_SIGNING_SECRET") -INCIDENT_CHANNEL_NAME = get_env_var("INCIDENT_CHANNEL_NAME") -INCIDENT_REPORT_CHANNEL_NAME = get_env_var("INCIDENT_REPORT_CHANNEL_NAME") -INCIDENT_BOT_NAME = get_env_var("INCIDENT_BOT_NAME") - -SLACK_API_MOCK = os.getenv("SLACK_API_MOCK", None) - -INCIDENT_BOT_ID = os.getenv("INCIDENT_BOT_ID") or SLACK_CLIENT.get_user_id( - INCIDENT_BOT_NAME -) -INCIDENT_CHANNEL_ID = os.getenv("INCIDENT_CHANNEL_ID") or SLACK_CLIENT.get_channel_id( - INCIDENT_CHANNEL_NAME -) -INCIDENT_REPORT_CHANNEL_ID = os.getenv( - "INCIDENT_REPORT_CHANNEL_ID" -) or SLACK_CLIENT.get_channel_id(INCIDENT_REPORT_CHANNEL_NAME) diff --git a/demo/docker-compose.yaml b/demo/docker-compose.yaml index 5db0755d..fb38f652 100644 --- a/demo/docker-compose.yaml +++ b/demo/docker-compose.yaml @@ -1,55 +1,27 @@ -version: '3' -services: +version: "2" +services: response: - build: - context: . - dockerfile: Dockerfile.response image: response - container_name: response - entrypoint: bash - command: /app/startup.sh + env_file: .env ports: - - "8000:8000" + - 8000:8000 depends_on: - db - environment: - POSTGRES: "true" - env_file: .env - volumes: - - ./:/app - - ../:/response - - pypd:/app/pypd - stdin_open: true - tty: true cron: - build: - context: . - dockerfile: Dockerfile.cron - image: cron - container_name: cron + image: response + command: cron + environment: + RESPONSE_URL: http://response:8000 depends_on: - response - tty: true db: - image: "postgres:11.2" - container_name: postgres - volumes: - - postgres_data:/var/lib/postgresql/data/ - - ngrok: - image: gtriggiano/ngrok-tunnel - container_name: ngrok - environment: - TARGET_HOST: "response" - TARGET_PORT: 8000 + image: postgres:11.2 ports: - - "4040:4040" - depends_on: - - response - -volumes: - postgres_data: - pypd: + - 4456:5432 + environment: + POSTGRES_PASSWORD: response + POSTGRES_USER: response + POSTGRES_DB: response diff --git a/demo/env.example b/demo/env.example index b8aa9d7b..5e1e0dbf 100644 --- a/demo/env.example +++ b/demo/env.example @@ -2,5 +2,11 @@ SLACK_TOKEN= SLACK_SIGNING_SECRET= INCIDENT_CHANNEL_NAME=incidents INCIDENT_BOT_NAME=incident - -DJANGO_SETTINGS_MODULE=demo.settings.dev +DB_HOST= +DB_NAME= +DB_USER= +DB_PORT= +DB_SSL_MODE=disable +DB_PASSWORD= +SITE_URL= +INCIDENT_REPORT_CHANNEL_NAME=incidents diff --git a/demo/env.prod.example b/demo/env.prod.example deleted file mode 100644 index 52a9b80f..00000000 --- a/demo/env.prod.example +++ /dev/null @@ -1,12 +0,0 @@ -SLACK_TOKEN= -SLACK_SIGNING_SECRET= -INCIDENT_CHANNEL_NAME=incidents -INCIDENT_BOT_NAME=incident -DB_HOST= -DB_NAME= -DB_USER= -DB_PORT= -DB_SSL_MODE= -DB_PASSWORD= -SITE_URL= -DJANGO_SETTINGS_MODULE=demo.settings.prod diff --git a/demo/requirements.txt b/demo/requirements.txt deleted file mode 100644 index 7b178a73..00000000 --- a/demo/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Django==2.2.13 -psycopg2-binary==2.8.2 diff --git a/demo/startup.sh b/entrypoint.sh old mode 100644 new mode 100755 similarity index 66% rename from demo/startup.sh rename to entrypoint.sh index 71e76d8e..548bbb1e --- a/demo/startup.sh +++ b/entrypoint.sh @@ -1,6 +1,10 @@ -#! /bin/bash +#!/bin/bash -pip install -e /response +set -e + +if [ "$1" == "cron" ]; then + exec supercronic /app/crontab +fi wait_for_db() { @@ -32,5 +36,7 @@ python3 manage.py migrate --noinput echo "[INFO] Creating Admin User" create_admin_user -echo "[INFO] Starting Response Dev Server" -python3 manage.py runserver 0.0.0.0:8000 +echo "[INFO] Starting Response Server" +python3 manage.py collectstatic --noinput + +exec uwsgi --http 0.0.0.0:8000 --module wsgi --master --processes 4 --threads 2 --static-map /static=/app/static/ diff --git a/setup.py b/setup.py index 04dddc39..8f597f31 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ "python-slugify>=1.2.6", "slackclient>=1.3,<2", "statuspageio>=0.0.1", + "psycopg2>=2.8.6" ] # allow setup.py to be run from any path @@ -35,7 +36,7 @@ version=VERSION, long_description=long_description, long_description_content_type="text/markdown", - packages=find_packages(exclude="demo"), + packages=find_packages(exclude="response-app"), install_requires=INSTALL_REQUIRES, package_dir={"response": "response"}, python_requires=">3.6",