├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── deploy.sh ├── docker-compose.yml ├── docker ├── celery-flower │ ├── Dockerfile │ ├── celery-flower.yaml │ └── entry.sh ├── celery-worker │ ├── Dockerfile │ ├── celery-worker.yaml │ └── entry.sh ├── celery │ ├── celerybeat │ ├── celeryd │ └── config │ │ └── celeryd ├── metrics-server.yaml ├── rest-api │ ├── Dockerfile │ ├── entry.sh │ └── rest-api.yaml └── webapp │ ├── Dockerfile │ └── webapp.yaml ├── findmyhitman ├── findmyhitman │ ├── __init__.py │ ├── asgi.py │ ├── celery.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── hitman_rest_api │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── channels.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tasks.py │ ├── tests.py │ └── views.py ├── manage.py └── oidc.key ├── hitman-webapp ├── .gitignore ├── README.md ├── index.ts ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── MainRouter.tsx │ ├── authentication │ │ ├── auth-context.tsx │ │ ├── auth-provider.tsx │ │ ├── auth-state.ts │ │ ├── auth.tsx │ │ └── reducer.ts │ ├── celery-task-progress-bar │ │ └── progress-bar.tsx │ ├── index.css │ ├── index.tsx │ ├── login │ │ └── login.tsx │ ├── logo.svg │ ├── navbar │ │ └── navbar.tsx │ ├── react-app-env.d.ts │ ├── register │ │ └── register.tsx │ ├── reportWebVitals.ts │ ├── services │ │ └── hitman.ts │ └── setupTests.ts └── tsconfig.json └── requirements.txt /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ '**' ] 6 | pull_request: 7 | branches: [ '**' ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | push_docker_image_to_github_packages: 12 | name: Build and push docker images 13 | runs-on: ubuntu-latest 14 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Permissions 18 | run: chmod +x ./deploy.sh 19 | - name: Deploy 20 | run: ./deploy.sh 21 | env: 22 | CR_PAT: ${{ secrets.CR_PAT }} 23 | KUBECTL_CONFIG: ${{ secrets.KUBECTL_CONFIG }} 24 | BUILD_NUMBER: ${{ github.run_number }} 25 | REACT_ENV_PROD: ${{ secrets.REACT_ENV_PROD }} 26 | deploy_to_kubernetes_cluster: 27 | name: Deploy to Kubernetes Cluster 28 | needs: push_docker_image_to_github_packages 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout Repo 32 | uses: actions/checkout@v2 33 | - name: Set Kubernetes Context 34 | uses: azure/k8s-set-context@v1 35 | with: 36 | method: kubeconfig 37 | kubeconfig: ${{ secrets.KUBECTL_CONFIG }} 38 | - name: Deploy to Cluster 39 | env: 40 | BUILD_NUMBER: ${{ github.run_number }} 41 | run: | 42 | export BUILD_TAG=hitman-prod-$BUILD_NUMBER 43 | sed -i "s/VERSION/${BUILD_TAG}/g" ./docker/rest-api/rest-api.yaml 44 | sed -i "s/VERSION/${BUILD_TAG}/g" ./docker/celery-worker/celery-worker.yaml 45 | sed -i "s/VERSION/${BUILD_TAG}/g" ./docker/celery-flower/celery-flower.yaml 46 | sed -i "s/VERSION/${BUILD_TAG}/g" ./docker/webapp/webapp.yaml 47 | kubectl apply -f ./docker/rest-api/rest-api.yaml 48 | kubectl apply -f ./docker/celery-worker/celery-worker.yaml 49 | kubectl apply -f ./docker/celery-flower/celery-flower.yaml 50 | kubectl apply -f ./docker/webapp/webapp.yaml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dump.rdb 3 | *.log 4 | .DS_Store 5 | .idea/ 6 | static/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-celery 2 | 3 | If you want a production ready template to deploy to kubernetes checkout out here: https://github.com/doherty-labs/django-rest-api 4 | 5 | Getting started 6 | 7 | docker-compose contains all required containers. 8 | 9 | - Start the postgres container and pgadmin container first. 10 | - Access pgadmin after starting via http://localhost:5050/browser/ password is changeme. 11 | - Create a database user called hitman with a password hitman. Superuser access required. 12 | - Create a database called hitman_db and grant hitman user permission. 13 | - After doing so run the rest-api container once hitman_db database has been setup and running. 14 | - Visit http://localhost:8000/hitmen/start-job to start a job, monitor celery logs to watch processing. 15 | - Visit http://localhost:5555/ for celery flower. 16 | - `celery -A findmyhitman worker --beat --loglevel=DEBUG --scheduler django_celery_beat.schedulers:DatabaseScheduler` 17 | - `docker run -dp 3000:3000 hitman/webapp` 18 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | ENV_TYPE=prod 5 | BUILD_TAG=hitman-$ENV_TYPE-$BUILD_NUMBER 6 | REGISTRY=ghcr.io/john-doherty01 7 | 8 | echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin 9 | 10 | docker build -f ./docker/webapp/Dockerfile . -t hitman/webapp --build-arg REACT_ENV_PROD="$REACT_ENV_PROD" 11 | docker tag hitman/webapp $REGISTRY/hitman-webapp:$BUILD_TAG 12 | docker push $REGISTRY/hitman-webapp:$BUILD_TAG 13 | 14 | docker build -f ./docker/rest-api/Dockerfile . -t hitman/rest-api 15 | docker tag hitman/rest-api $REGISTRY/hitman-rest-api:$BUILD_TAG 16 | docker push $REGISTRY/hitman-rest-api:$BUILD_TAG 17 | 18 | docker build -f ./docker/celery-worker/Dockerfile . -t hitman/celery-worker 19 | docker tag hitman/celery-worker $REGISTRY/hitman-celery-worker:$BUILD_TAG 20 | docker push $REGISTRY/hitman-celery-worker:$BUILD_TAG 21 | 22 | docker build -f ./docker/celery-flower/Dockerfile . -t hitman/celery-flower 23 | docker tag hitman/celery-flower $REGISTRY/hitman-celery-flower:$BUILD_TAG 24 | docker push $REGISTRY/hitman-celery-flower:$BUILD_TAG 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | flower: 4 | build: 5 | context: . 6 | dockerfile: docker/celery-flower/Dockerfile 7 | environment: 8 | DATABASE_NAME: hitman_db 9 | DATABASE_USER: hitman 10 | DATABASE_PASSWORD: hitman 11 | DATABASE_HOST: postgres 12 | DATABASE_PORT: 5432 13 | REDIS_URL: redis://redis:6379/0 14 | depends_on: 15 | - postgres 16 | - redis 17 | networks: 18 | - postgres 19 | - redis 20 | - celery-worker 21 | ports: 22 | - "5555:5555" 23 | rest-api: 24 | restart: always 25 | build: 26 | context: . 27 | dockerfile: docker/rest-api/Dockerfile 28 | depends_on: 29 | - celery-worker 30 | - flower 31 | volumes: 32 | - .:/usr/src/app 33 | environment: 34 | DATABASE_NAME: hitman_db 35 | DATABASE_USER: hitman 36 | DATABASE_PASSWORD: hitman 37 | DATABASE_HOST: postgres 38 | DATABASE_PORT: 5432 39 | REDIS_URL: redis://redis:6379/0 40 | networks: 41 | - postgres 42 | - redis 43 | ports: 44 | - "8000:8000" 45 | celery-worker: 46 | restart: always 47 | build: 48 | context: . 49 | dockerfile: docker/celery-worker/Dockerfile 50 | volumes: 51 | - .:/usr/src/app 52 | depends_on: 53 | - postgres 54 | - redis 55 | - pgadmin 56 | environment: 57 | DATABASE_NAME: hitman_db 58 | DATABASE_USER: hitman 59 | DATABASE_PASSWORD: hitman 60 | DATABASE_HOST: postgres 61 | DATABASE_PORT: 5432 62 | REDIS_URL: redis://redis:6379/0 63 | networks: 64 | - postgres 65 | - redis 66 | - celery-worker 67 | redis: 68 | image: "redis:alpine" 69 | networks: 70 | - redis 71 | postgres: 72 | container_name: postgres_container 73 | image: postgres 74 | environment: 75 | POSTGRES_USER: ${POSTGRES_USER:-postgres} 76 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme} 77 | PGDATA: /data/postgres 78 | volumes: 79 | - postgres:/data/postgres 80 | ports: 81 | - "5432:5432" 82 | networks: 83 | - postgres 84 | restart: unless-stopped 85 | pgadmin: 86 | container_name: pgadmin_container 87 | image: dpage/pgadmin4 88 | environment: 89 | PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} 90 | PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} 91 | PGADMIN_CONFIG_SERVER_MODE: 'False' 92 | volumes: 93 | - pgadmin:/var/lib/pgadmin 94 | ports: 95 | - "${PGADMIN_PORT:-5050}:80" 96 | networks: 97 | - postgres 98 | restart: unless-stopped 99 | depends_on: 100 | - postgres 101 | 102 | networks: 103 | postgres: 104 | driver: bridge 105 | redis: 106 | driver: bridge 107 | celery-worker: 108 | driver: bridge 109 | 110 | volumes: 111 | postgres: 112 | pgadmin: -------------------------------------------------------------------------------- /docker/celery-flower/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.1 2 | EXPOSE 5555 3 | 4 | ENV DEBIAN_FRONTEND noninteractive 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | ENV LC_ALL C.UTF-8 8 | ENV LANG C.UTF-8 9 | 10 | RUN apt-get update && apt-get -y upgrade 11 | RUN apt-get install -y gcc 12 | RUN apt-get install -y g++ 13 | RUN apt-get install -y build-essential 14 | RUN apt-get install -y wget 15 | 16 | USER root 17 | RUN useradd -ms /bin/bash celery 18 | RUN usermod -a -G celery celery 19 | RUN usermod -a -G celery root 20 | 21 | WORKDIR /usr/src/app 22 | COPY ./ ./ 23 | 24 | RUN pip install -r ./requirements.txt 25 | 26 | 27 | COPY ./docker/celery/celeryd /etc/init.d/ 28 | COPY ./docker/celery/celerybeat /etc/init.d/ 29 | COPY ./docker/celery/config/celeryd /etc/default/ 30 | RUN ["chmod", "755", "/etc/init.d/celeryd"] 31 | RUN ["chmod", "755", "/etc/init.d/celerybeat"] 32 | RUN ["chmod", "640", "/etc/default/celeryd"] 33 | 34 | 35 | RUN ["chmod", "+x", "./docker/celery-flower/entry.sh"] 36 | ENTRYPOINT ["./docker/celery-flower/entry.sh"] 37 | -------------------------------------------------------------------------------- /docker/celery-flower/celery-flower.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hitman-celery-flower-deployment 5 | namespace: default 6 | labels: 7 | app: hitman-celery-flower 8 | spec: 9 | replicas: 1 10 | strategy: 11 | type: RollingUpdate 12 | rollingUpdate: 13 | maxSurge: 1 14 | maxUnavailable: 0 15 | selector: 16 | matchLabels: 17 | app: hitman-celery-flower 18 | template: 19 | metadata: 20 | labels: 21 | app: hitman-celery-flower 22 | spec: 23 | terminationGracePeriodSeconds: 3800 24 | imagePullSecrets: 25 | - name: dockerconfigjson-github-com 26 | containers: 27 | - name: hitman-celery-flower-image 28 | image: ghcr.io/john-doherty01/hitman-celery-flower:VERSION 29 | imagePullPolicy: IfNotPresent 30 | resources: 31 | requests: 32 | cpu: "100m" 33 | memory: "100M" 34 | limits: 35 | cpu: "150m" 36 | memory: "200M" 37 | ports: 38 | - containerPort: 5555 39 | readinessProbe: 40 | httpGet: 41 | path: / 42 | port: 5555 43 | initialDelaySeconds: 30 44 | periodSeconds: 20 45 | livenessProbe: 46 | httpGet: 47 | path: / 48 | port: 5555 49 | initialDelaySeconds: 15 50 | periodSeconds: 120 51 | env: 52 | - name: DATABASE_NAME 53 | valueFrom: 54 | secretKeyRef: 55 | name: hitmansecret 56 | key: database_name 57 | optional: false 58 | - name: DATABASE_USER 59 | valueFrom: 60 | secretKeyRef: 61 | name: hitmansecret 62 | key: database_user 63 | optional: false 64 | - name: DATABASE_PASSWORD 65 | valueFrom: 66 | secretKeyRef: 67 | name: hitmansecret 68 | key: database_password 69 | optional: false 70 | - name: DATABASE_HOST 71 | valueFrom: 72 | secretKeyRef: 73 | name: hitmansecret 74 | key: database_host 75 | optional: false 76 | - name: DATABASE_PORT 77 | valueFrom: 78 | secretKeyRef: 79 | name: hitmansecret 80 | key: database_port 81 | optional: false 82 | - name: ALLOWED_HOST 83 | valueFrom: 84 | secretKeyRef: 85 | name: hitmansecret 86 | key: allowed_host 87 | optional: false 88 | - name: CORS_ALLOWED_ORIGINS 89 | valueFrom: 90 | secretKeyRef: 91 | name: hitmansecret 92 | key: cors_allowed_origins 93 | optional: false 94 | - name: CELERY_BROKER_URL 95 | valueFrom: 96 | secretKeyRef: 97 | name: hitmansecret 98 | key: celery_broker_url 99 | optional: false 100 | - name: CELERY_RESULT_URL 101 | valueFrom: 102 | secretKeyRef: 103 | name: hitmansecret 104 | key: celery_result_url 105 | optional: false 106 | - name: CHANNELS_URLS 107 | valueFrom: 108 | secretKeyRef: 109 | name: hitmansecret 110 | key: channels_url 111 | optional: false 112 | 113 | --- 114 | apiVersion: v1 115 | kind: Service 116 | metadata: 117 | name: hitman-celery-flower-load-balancer 118 | spec: 119 | selector: 120 | app: hitman-celery-flower 121 | ports: 122 | - port: 80 123 | name: http 124 | targetPort: 5555 125 | - port: 443 126 | name: https 127 | targetPort: 5555 128 | type: LoadBalancer 129 | -------------------------------------------------------------------------------- /docker/celery-flower/entry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | cd ./findmyhitman/ 5 | python manage.py migrate 6 | python manage.py collectstatic --clear --noinput 7 | 8 | echo "Starting flower" 9 | exec celery -A findmyhitman flower --address=0.0.0.0 --port=5555 10 | -------------------------------------------------------------------------------- /docker/celery-worker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.1 2 | 3 | ENV DEBIAN_FRONTEND noninteractive 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | ENV PYTHONUNBUFFERED 1 6 | ENV LC_ALL C.UTF-8 7 | ENV LANG C.UTF-8 8 | 9 | RUN apt-get update && apt-get -y upgrade 10 | RUN apt-get install -y gcc 11 | RUN apt-get install -y g++ 12 | RUN apt-get install -y build-essential 13 | RUN apt-get install -y wget 14 | 15 | USER root 16 | RUN useradd -ms /bin/bash celery 17 | RUN usermod -a -G celery celery 18 | RUN usermod -a -G celery root 19 | 20 | WORKDIR /usr/src/app 21 | COPY ./ ./ 22 | 23 | RUN pip install -r ./requirements.txt 24 | 25 | COPY ./docker/celery/celeryd /etc/init.d/ 26 | COPY ./docker/celery/celerybeat /etc/init.d/ 27 | COPY ./docker/celery/config/celeryd /etc/default/ 28 | RUN ["chmod", "755", "/etc/init.d/celeryd"] 29 | RUN ["chmod", "755", "/etc/init.d/celerybeat"] 30 | RUN ["chmod", "640", "/etc/default/celeryd"] 31 | 32 | RUN ["chmod", "+x", "./docker/celery-worker/entry.sh"] 33 | ENTRYPOINT ["./docker/celery-worker/entry.sh"] 34 | -------------------------------------------------------------------------------- /docker/celery-worker/celery-worker.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hitman-celery-worker-deployment 5 | namespace: default 6 | labels: 7 | app: hitman-celery-worker 8 | spec: 9 | replicas: 1 10 | strategy: 11 | type: RollingUpdate 12 | rollingUpdate: 13 | maxSurge: 1 14 | maxUnavailable: 0 15 | selector: 16 | matchLabels: 17 | app: hitman-celery-worker 18 | template: 19 | metadata: 20 | labels: 21 | app: hitman-celery-worker 22 | spec: 23 | terminationGracePeriodSeconds: 86400 24 | imagePullSecrets: 25 | - name: dockerconfigjson-github-com 26 | containers: 27 | - name: hitman-celery-worker-docker-image 28 | image: ghcr.io/john-doherty01/hitman-celery-worker:VERSION 29 | imagePullPolicy: IfNotPresent 30 | resources: 31 | requests: 32 | cpu: "100m" 33 | memory: "500M" 34 | limits: 35 | cpu: "500m" 36 | memory: "1000M" 37 | readinessProbe: 38 | exec: 39 | command: 40 | [ 41 | "python", 42 | "-c", 43 | '"import os;from celery.app.control import Inspect;from api import celery_app;exit(0 if os.environ[''HOSTNAME''] in '',''.join(Inspect(app=celery_app).stats().keys()) else 1)"', 44 | ] 45 | initialDelaySeconds: 120 46 | periodSeconds: 300 47 | livenessProbe: 48 | exec: 49 | command: 50 | [ 51 | "python", 52 | "-c", 53 | '"import os;from celery.app.control import Inspect;from api import celery_app;exit(0 if os.environ[''HOSTNAME''] in '',''.join(Inspect(app=celery_app).stats().keys()) else 1)"', 54 | ] 55 | initialDelaySeconds: 120 56 | periodSeconds: 300 57 | env: 58 | - name: DATABASE_NAME 59 | valueFrom: 60 | secretKeyRef: 61 | name: hitmansecret 62 | key: database_name 63 | optional: false 64 | - name: DATABASE_USER 65 | valueFrom: 66 | secretKeyRef: 67 | name: hitmansecret 68 | key: database_user 69 | optional: false 70 | - name: DATABASE_PASSWORD 71 | valueFrom: 72 | secretKeyRef: 73 | name: hitmansecret 74 | key: database_password 75 | optional: false 76 | - name: DATABASE_HOST 77 | valueFrom: 78 | secretKeyRef: 79 | name: hitmansecret 80 | key: database_host 81 | optional: false 82 | - name: DATABASE_PORT 83 | valueFrom: 84 | secretKeyRef: 85 | name: hitmansecret 86 | key: database_port 87 | optional: false 88 | - name: ALLOWED_HOST 89 | valueFrom: 90 | secretKeyRef: 91 | name: hitmansecret 92 | key: allowed_host 93 | optional: false 94 | - name: CORS_ALLOWED_ORIGINS 95 | valueFrom: 96 | secretKeyRef: 97 | name: hitmansecret 98 | key: cors_allowed_origins 99 | optional: false 100 | - name: CELERY_BROKER_URL 101 | valueFrom: 102 | secretKeyRef: 103 | name: hitmansecret 104 | key: celery_broker_url 105 | optional: false 106 | - name: CELERY_RESULT_URL 107 | valueFrom: 108 | secretKeyRef: 109 | name: hitmansecret 110 | key: celery_result_url 111 | optional: false 112 | - name: CHANNELS_URLS 113 | valueFrom: 114 | secretKeyRef: 115 | name: hitmansecret 116 | key: channels_url 117 | optional: false 118 | --- 119 | apiVersion: autoscaling/v1 120 | kind: HorizontalPodAutoscaler 121 | metadata: 122 | name: celery-autoscaler 123 | spec: 124 | scaleTargetRef: 125 | apiVersion: apps/v1 126 | kind: Deployment 127 | name: hitman-celery-worker-deployment 128 | minReplicas: 1 129 | maxReplicas: 10 130 | targetCPUUtilizationPercentage: 50 131 | -------------------------------------------------------------------------------- /docker/celery-worker/entry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # Run tests 5 | cd ./findmyhitman/ 6 | python manage.py migrate 7 | python manage.py collectstatic --clear --noinput 8 | 9 | echo "Starting celery worker" 10 | exec celery -A findmyhitman worker --beat -Q celery --loglevel=info --concurrency=1 --max-memory-per-child=25000 --scheduler django_celery_beat.schedulers:DatabaseScheduler 11 | -------------------------------------------------------------------------------- /docker/celery/celerybeat: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # ========================================================= 3 | # celerybeat - Starts the Celery periodic task scheduler. 4 | # ========================================================= 5 | # 6 | # :Usage: /etc/init.d/celerybeat {start|stop|force-reload|restart|try-restart|status} 7 | # :Configuration file: /etc/default/celerybeat or /etc/default/celeryd 8 | # 9 | # See http://docs.celeryproject.org/en/latest/userguide/daemonizing.html#generic-init-scripts 10 | 11 | ### BEGIN INIT INFO 12 | # Provides: celerybeat 13 | # Required-Start: $network $local_fs $remote_fs 14 | # Required-Stop: $network $local_fs $remote_fs 15 | # Default-Start: 2 3 4 5 16 | # Default-Stop: 0 1 6 17 | # Short-Description: celery periodic task scheduler 18 | ### END INIT INFO 19 | 20 | # Cannot use set -e/bash -e since the kill -0 command will abort 21 | # abnormally in the absence of a valid process ID. 22 | #set -e 23 | VERSION=10.1 24 | echo "celery init v${VERSION}." 25 | 26 | if [ $(id -u) -ne 0 ]; then 27 | echo "Error: This program can only be used by the root user." 28 | echo " Unpriviliged users must use 'celery beat --detach'" 29 | exit 1 30 | fi 31 | 32 | origin_is_runlevel_dir () { 33 | set +e 34 | dirname $0 | grep -q "/etc/rc.\.d" 35 | echo $? 36 | } 37 | 38 | # Can be a runlevel symlink (e.g., S02celeryd) 39 | if [ $(origin_is_runlevel_dir) -eq 0 ]; then 40 | SCRIPT_FILE=$(readlink "$0") 41 | else 42 | SCRIPT_FILE="$0" 43 | fi 44 | SCRIPT_NAME="$(basename "$SCRIPT_FILE")" 45 | 46 | # /etc/init.d/celerybeat: start and stop the celery periodic task scheduler daemon. 47 | 48 | # Make sure executable configuration script is owned by root 49 | _config_sanity() { 50 | local path="$1" 51 | local owner=$(ls -ld "$path" | awk '{print $3}') 52 | local iwgrp=$(ls -ld "$path" | cut -b 6) 53 | local iwoth=$(ls -ld "$path" | cut -b 9) 54 | 55 | if [ "$(id -u $owner)" != "0" ]; then 56 | echo "Error: Config script '$path' must be owned by root!" 57 | echo 58 | echo "Resolution:" 59 | echo "Review the file carefully, and make sure it hasn't been " 60 | echo "modified with mailicious intent. When sure the " 61 | echo "script is safe to execute with superuser privileges " 62 | echo "you can change ownership of the script:" 63 | echo " $ sudo chown root '$path'" 64 | exit 1 65 | fi 66 | 67 | if [ "$iwoth" != "-" ]; then # S_IWOTH 68 | echo "Error: Config script '$path' cannot be writable by others!" 69 | echo 70 | echo "Resolution:" 71 | echo "Review the file carefully, and make sure it hasn't been " 72 | echo "modified with malicious intent. When sure the " 73 | echo "script is safe to execute with superuser privileges " 74 | echo "you can change the scripts permissions:" 75 | echo " $ sudo chmod 640 '$path'" 76 | exit 1 77 | fi 78 | if [ "$iwgrp" != "-" ]; then # S_IWGRP 79 | echo "Error: Config script '$path' cannot be writable by group!" 80 | echo 81 | echo "Resolution:" 82 | echo "Review the file carefully, and make sure it hasn't been " 83 | echo "modified with malicious intent. When sure the " 84 | echo "script is safe to execute with superuser privileges " 85 | echo "you can change the scripts permissions:" 86 | echo " $ sudo chmod 640 '$path'" 87 | exit 1 88 | fi 89 | } 90 | 91 | scripts="" 92 | 93 | if test -f /etc/default/celeryd; then 94 | scripts="/etc/default/celeryd" 95 | _config_sanity /etc/default/celeryd 96 | . /etc/default/celeryd 97 | fi 98 | 99 | EXTRA_CONFIG="/etc/default/${SCRIPT_NAME}" 100 | if test -f "$EXTRA_CONFIG"; then 101 | scripts="$scripts, $EXTRA_CONFIG" 102 | _config_sanity "$EXTRA_CONFIG" 103 | . "$EXTRA_CONFIG" 104 | fi 105 | 106 | echo "Using configuration: $scripts" 107 | 108 | CELERY_BIN=${CELERY_BIN:-"celery"} 109 | DEFAULT_USER="celery" 110 | DEFAULT_PID_FILE="/var/run/celery/beat.pid" 111 | DEFAULT_LOG_FILE="/var/log/celery/beat.log" 112 | DEFAULT_LOG_LEVEL="INFO" 113 | DEFAULT_CELERYBEAT="$CELERY_BIN" 114 | 115 | CELERYBEAT=${CELERYBEAT:-$DEFAULT_CELERYBEAT} 116 | CELERYBEAT_LOG_LEVEL=${CELERYBEAT_LOG_LEVEL:-${CELERYBEAT_LOGLEVEL:-$DEFAULT_LOG_LEVEL}} 117 | 118 | CELERYBEAT_SU=${CELERYBEAT_SU:-"su"} 119 | CELERYBEAT_SU_ARGS=${CELERYBEAT_SU_ARGS:-""} 120 | 121 | # Sets --app argument for CELERY_BIN 122 | CELERY_APP_ARG="" 123 | if [ ! -z "$CELERY_APP" ]; then 124 | CELERY_APP_ARG="--app=$CELERY_APP" 125 | fi 126 | 127 | CELERYBEAT_USER=${CELERYBEAT_USER:-${CELERYD_USER:-$DEFAULT_USER}} 128 | 129 | # Set CELERY_CREATE_DIRS to always create log/pid dirs. 130 | CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0} 131 | CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS 132 | CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS 133 | if [ -z "$CELERYBEAT_PID_FILE" ]; then 134 | CELERYBEAT_PID_FILE="$DEFAULT_PID_FILE" 135 | CELERY_CREATE_RUNDIR=1 136 | fi 137 | if [ -z "$CELERYBEAT_LOG_FILE" ]; then 138 | CELERYBEAT_LOG_FILE="$DEFAULT_LOG_FILE" 139 | CELERY_CREATE_LOGDIR=1 140 | fi 141 | 142 | export CELERY_LOADER 143 | 144 | if [ -n "$2" ]; then 145 | CELERYBEAT_OPTS="$CELERYBEAT_OPTS $2" 146 | fi 147 | 148 | CELERYBEAT_LOG_DIR=`dirname $CELERYBEAT_LOG_FILE` 149 | CELERYBEAT_PID_DIR=`dirname $CELERYBEAT_PID_FILE` 150 | 151 | # Extra start-stop-daemon options, like user/group. 152 | 153 | CELERYBEAT_CHDIR=${CELERYBEAT_CHDIR:-$CELERYD_CHDIR} 154 | if [ -n "$CELERYBEAT_CHDIR" ]; then 155 | DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYBEAT_CHDIR" 156 | fi 157 | 158 | 159 | export PATH="${PATH:+$PATH:}/usr/sbin:/sbin" 160 | 161 | check_dev_null() { 162 | if [ ! -c /dev/null ]; then 163 | echo "/dev/null is not a character device!" 164 | exit 75 # EX_TEMPFAIL 165 | fi 166 | } 167 | 168 | maybe_die() { 169 | if [ $? -ne 0 ]; then 170 | echo "Exiting: $*" 171 | exit 77 # EX_NOPERM 172 | fi 173 | } 174 | 175 | create_default_dir() { 176 | if [ ! -d "$1" ]; then 177 | echo "- Creating default directory: '$1'" 178 | mkdir -p "$1" 179 | maybe_die "Couldn't create directory $1" 180 | echo "- Changing permissions of '$1' to 02755" 181 | chmod 02755 "$1" 182 | maybe_die "Couldn't change permissions for $1" 183 | if [ -n "$CELERYBEAT_USER" ]; then 184 | echo "- Changing owner of '$1' to '$CELERYBEAT_USER'" 185 | chown "$CELERYBEAT_USER" "$1" 186 | maybe_die "Couldn't change owner of $1" 187 | fi 188 | if [ -n "$CELERYBEAT_GROUP" ]; then 189 | echo "- Changing group of '$1' to '$CELERYBEAT_GROUP'" 190 | chgrp "$CELERYBEAT_GROUP" "$1" 191 | maybe_die "Couldn't change group of $1" 192 | fi 193 | fi 194 | } 195 | 196 | check_paths() { 197 | if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then 198 | create_default_dir "$CELERYBEAT_LOG_DIR" 199 | fi 200 | if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then 201 | create_default_dir "$CELERYBEAT_PID_DIR" 202 | fi 203 | } 204 | 205 | 206 | create_paths () { 207 | create_default_dir "$CELERYBEAT_LOG_DIR" 208 | create_default_dir "$CELERYBEAT_PID_DIR" 209 | } 210 | 211 | is_running() { 212 | pid=$1 213 | ps $pid > /dev/null 2>&1 214 | } 215 | 216 | wait_pid () { 217 | pid=$1 218 | forever=1 219 | i=0 220 | while [ $forever -gt 0 ]; do 221 | if ! is_running $pid; then 222 | echo "OK" 223 | forever=0 224 | else 225 | kill -TERM "$pid" 226 | i=$((i + 1)) 227 | if [ $i -gt 60 ]; then 228 | echo "ERROR" 229 | echo "Timed out while stopping (30s)" 230 | forever=0 231 | else 232 | sleep 0.5 233 | fi 234 | fi 235 | done 236 | } 237 | 238 | 239 | stop_beat () { 240 | echo -n "Stopping ${SCRIPT_NAME}... " 241 | if [ -f "$CELERYBEAT_PID_FILE" ]; then 242 | wait_pid $(cat "$CELERYBEAT_PID_FILE") 243 | else 244 | echo "NOT RUNNING" 245 | fi 246 | } 247 | 248 | _chuid () { 249 | ${CELERYBEAT_SU} ${CELERYBEAT_SU_ARGS} \ 250 | "$CELERYBEAT_USER" -c "$CELERYBEAT $*" 251 | } 252 | 253 | start_beat () { 254 | echo "Starting ${SCRIPT_NAME}..." 255 | _chuid $CELERY_APP_ARG $DAEMON_OPTS beat --detach \ 256 | --pidfile="$CELERYBEAT_PID_FILE" \ 257 | --logfile="$CELERYBEAT_LOG_FILE" \ 258 | --loglevel="$CELERYBEAT_LOG_LEVEL" \ 259 | $CELERYBEAT_OPTS 260 | } 261 | 262 | 263 | check_status () { 264 | local failed= 265 | local pid_file=$CELERYBEAT_PID_FILE 266 | if [ ! -e $pid_file ]; then 267 | echo "${SCRIPT_NAME} is down: no pid file found" 268 | failed=true 269 | elif [ ! -r $pid_file ]; then 270 | echo "${SCRIPT_NAME} is in unknown state, user cannot read pid file." 271 | failed=true 272 | else 273 | local pid=`cat "$pid_file"` 274 | local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'` 275 | if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then 276 | echo "${SCRIPT_NAME}: bad pid file ($pid_file)" 277 | failed=true 278 | else 279 | local failed= 280 | kill -0 $pid 2> /dev/null || failed=true 281 | if [ "$failed" ]; then 282 | echo "${SCRIPT_NAME} (pid $pid) is down, but pid file exists!" 283 | failed=true 284 | else 285 | echo "${SCRIPT_NAME} (pid $pid) is up..." 286 | fi 287 | fi 288 | fi 289 | 290 | [ "$failed" ] && exit 1 || exit 0 291 | } 292 | 293 | 294 | case "$1" in 295 | start) 296 | check_dev_null 297 | check_paths 298 | start_beat 299 | ;; 300 | stop) 301 | check_paths 302 | stop_beat 303 | ;; 304 | reload|force-reload) 305 | echo "Use start+stop" 306 | ;; 307 | status) 308 | check_status 309 | ;; 310 | restart) 311 | echo "Restarting celery periodic task scheduler" 312 | check_paths 313 | stop_beat && check_dev_null && start_beat 314 | ;; 315 | create-paths) 316 | check_dev_null 317 | create_paths 318 | ;; 319 | check-paths) 320 | check_dev_null 321 | check_paths 322 | ;; 323 | *) 324 | echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|create-paths|status}" 325 | exit 64 # EX_USAGE 326 | ;; 327 | esac 328 | 329 | exit 0 330 | -------------------------------------------------------------------------------- /docker/celery/celeryd: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # ============================================ 3 | # celeryd - Starts the Celery worker daemon. 4 | # ============================================ 5 | # 6 | # :Usage: /etc/init.d/celeryd {start|stop|force-reload|restart|try-restart|status} 7 | # :Configuration file: /etc/default/celeryd (or /usr/local/etc/celeryd on BSD) 8 | # 9 | # See http://docs.celeryproject.org/en/latest/userguide/daemonizing.html#generic-init-scripts 10 | 11 | 12 | ### BEGIN INIT INFO 13 | # Provides: celeryd 14 | # Required-Start: $network $local_fs $remote_fs 15 | # Required-Stop: $network $local_fs $remote_fs 16 | # Default-Start: 2 3 4 5 17 | # Default-Stop: 0 1 6 18 | # Short-Description: celery task worker daemon 19 | ### END INIT INFO 20 | # 21 | # 22 | # To implement separate init-scripts, copy this script and give it a different 23 | # name. That is, if your new application named "little-worker" needs an init, 24 | # you should use: 25 | # 26 | # cp /etc/init.d/celeryd /etc/init.d/little-worker 27 | # 28 | # You can then configure this by manipulating /etc/default/little-worker. 29 | # 30 | VERSION=10.1 31 | echo "celery init v${VERSION}." 32 | if [ $(id -u) -ne 0 ]; then 33 | echo "Error: This program can only be used by the root user." 34 | echo " Unprivileged users must use the 'celery multi' utility, " 35 | echo " or 'celery worker --detach'." 36 | exit 1 37 | fi 38 | 39 | origin_is_runlevel_dir () { 40 | set +e 41 | dirname $0 | grep -q "/etc/rc.\.d" 42 | echo $? 43 | } 44 | 45 | # Can be a runlevel symlink (e.g., S02celeryd) 46 | if [ $(origin_is_runlevel_dir) -eq 0 ]; then 47 | SCRIPT_FILE=$(readlink "$0") 48 | else 49 | SCRIPT_FILE="$0" 50 | fi 51 | SCRIPT_NAME="$(basename "$SCRIPT_FILE")" 52 | 53 | DEFAULT_USER="celery" 54 | DEFAULT_PID_FILE="/var/run/celery/%n.pid" 55 | DEFAULT_LOG_FILE="/var/log/celery/%n%I.log" 56 | DEFAULT_LOG_LEVEL="INFO" 57 | DEFAULT_NODES="celery" 58 | DEFAULT_CELERYD="-m celery worker --detach" 59 | 60 | if [ -d "/etc/default" ]; then 61 | CELERY_CONFIG_DIR="/etc/default" 62 | else 63 | CELERY_CONFIG_DIR="/usr/local/etc" 64 | fi 65 | 66 | CELERY_DEFAULTS=${CELERY_DEFAULTS:-"$CELERY_CONFIG_DIR/${SCRIPT_NAME}"} 67 | 68 | # Make sure executable configuration script is owned by root 69 | _config_sanity() { 70 | local path="$1" 71 | local owner=$(ls -ld "$path" | awk '{print $3}') 72 | local iwgrp=$(ls -ld "$path" | cut -b 6) 73 | local iwoth=$(ls -ld "$path" | cut -b 9) 74 | 75 | if [ "$(id -u $owner)" != "0" ]; then 76 | echo "Error: Config script '$path' must be owned by root!" 77 | echo 78 | echo "Resolution:" 79 | echo "Review the file carefully, and make sure it hasn't been " 80 | echo "modified with mailicious intent. When sure the " 81 | echo "script is safe to execute with superuser privileges " 82 | echo "you can change ownership of the script:" 83 | echo " $ sudo chown root '$path'" 84 | exit 1 85 | fi 86 | 87 | if [ "$iwoth" != "-" ]; then # S_IWOTH 88 | echo "Error: Config script '$path' cannot be writable by others!" 89 | echo 90 | echo "Resolution:" 91 | echo "Review the file carefully, and make sure it hasn't been " 92 | echo "modified with malicious intent. When sure the " 93 | echo "script is safe to execute with superuser privileges " 94 | echo "you can change the scripts permissions:" 95 | echo " $ sudo chmod 640 '$path'" 96 | exit 1 97 | fi 98 | if [ "$iwgrp" != "-" ]; then # S_IWGRP 99 | echo "Error: Config script '$path' cannot be writable by group!" 100 | echo 101 | echo "Resolution:" 102 | echo "Review the file carefully, and make sure it hasn't been " 103 | echo "modified with malicious intent. When sure the " 104 | echo "script is safe to execute with superuser privileges " 105 | echo "you can change the scripts permissions:" 106 | echo " $ sudo chmod 640 '$path'" 107 | exit 1 108 | fi 109 | } 110 | 111 | if [ -f "$CELERY_DEFAULTS" ]; then 112 | _config_sanity "$CELERY_DEFAULTS" 113 | echo "Using config script: $CELERY_DEFAULTS" 114 | . "$CELERY_DEFAULTS" 115 | fi 116 | 117 | # Sets --app argument for CELERY_BIN 118 | CELERY_APP_ARG="" 119 | if [ ! -z "$CELERY_APP" ]; then 120 | CELERY_APP_ARG="--app=$CELERY_APP" 121 | fi 122 | 123 | # Options to su 124 | # can be used to enable login shell (CELERYD_SU_ARGS="-l"), 125 | # or even to use start-stop-daemon instead of su. 126 | CELERYD_SU=${CELERY_SU:-"su"} 127 | CELERYD_SU_ARGS=${CELERYD_SU_ARGS:-""} 128 | 129 | CELERYD_USER=${CELERYD_USER:-$DEFAULT_USER} 130 | 131 | # Set CELERY_CREATE_DIRS to always create log/pid dirs. 132 | CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0} 133 | CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS 134 | CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS 135 | if [ -z "$CELERYD_PID_FILE" ]; then 136 | CELERYD_PID_FILE="$DEFAULT_PID_FILE" 137 | CELERY_CREATE_RUNDIR=1 138 | fi 139 | if [ -z "$CELERYD_LOG_FILE" ]; then 140 | CELERYD_LOG_FILE="$DEFAULT_LOG_FILE" 141 | CELERY_CREATE_LOGDIR=1 142 | fi 143 | 144 | CELERYD_LOG_LEVEL=${CELERYD_LOG_LEVEL:-${CELERYD_LOGLEVEL:-$DEFAULT_LOG_LEVEL}} 145 | CELERY_BIN=${CELERY_BIN:-"celery"} 146 | CELERYD_MULTI=${CELERYD_MULTI:-"$CELERY_BIN multi"} 147 | CELERYD_NODES=${CELERYD_NODES:-$DEFAULT_NODES} 148 | 149 | export CELERY_LOADER 150 | 151 | if [ -n "$2" ]; then 152 | CELERYD_OPTS="$CELERYD_OPTS $2" 153 | fi 154 | 155 | CELERYD_LOG_DIR=`dirname $CELERYD_LOG_FILE` 156 | CELERYD_PID_DIR=`dirname $CELERYD_PID_FILE` 157 | 158 | # Extra start-stop-daemon options, like user/group. 159 | if [ -n "$CELERYD_CHDIR" ]; then 160 | DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYD_CHDIR" 161 | fi 162 | 163 | 164 | check_dev_null() { 165 | if [ ! -c /dev/null ]; then 166 | echo "/dev/null is not a character device!" 167 | exit 75 # EX_TEMPFAIL 168 | fi 169 | } 170 | 171 | 172 | maybe_die() { 173 | if [ $? -ne 0 ]; then 174 | echo "Exiting: $* (errno $?)" 175 | exit 77 # EX_NOPERM 176 | fi 177 | } 178 | 179 | create_default_dir() { 180 | if [ ! -d "$1" ]; then 181 | echo "- Creating default directory: '$1'" 182 | mkdir -p "$1" 183 | maybe_die "Couldn't create directory $1" 184 | echo "- Changing permissions of '$1' to 02755" 185 | chmod 02755 "$1" 186 | maybe_die "Couldn't change permissions for $1" 187 | if [ -n "$CELERYD_USER" ]; then 188 | echo "- Changing owner of '$1' to '$CELERYD_USER'" 189 | chown "$CELERYD_USER" "$1" 190 | maybe_die "Couldn't change owner of $1" 191 | fi 192 | if [ -n "$CELERYD_GROUP" ]; then 193 | echo "- Changing group of '$1' to '$CELERYD_GROUP'" 194 | chgrp "$CELERYD_GROUP" "$1" 195 | maybe_die "Couldn't change group of $1" 196 | fi 197 | fi 198 | } 199 | 200 | 201 | check_paths() { 202 | if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then 203 | create_default_dir "$CELERYD_LOG_DIR" 204 | fi 205 | if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then 206 | create_default_dir "$CELERYD_PID_DIR" 207 | fi 208 | } 209 | 210 | create_paths() { 211 | create_default_dir "$CELERYD_LOG_DIR" 212 | create_default_dir "$CELERYD_PID_DIR" 213 | } 214 | 215 | export PATH="${PATH:+$PATH:}/usr/sbin:/sbin" 216 | 217 | 218 | _get_pidfiles () { 219 | # note: multi < 3.1.14 output to stderr, not stdout, hence the redirect. 220 | ${CELERYD_MULTI} expand "${CELERYD_PID_FILE}" ${CELERYD_NODES} 2>&1 221 | } 222 | 223 | 224 | _get_pids() { 225 | found_pids=0 226 | my_exitcode=0 227 | 228 | for pidfile in $(_get_pidfiles); do 229 | local pid=`cat "$pidfile"` 230 | local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'` 231 | if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then 232 | echo "bad pid file ($pidfile)" 233 | one_failed=true 234 | my_exitcode=1 235 | else 236 | found_pids=1 237 | echo "$pid" 238 | fi 239 | 240 | if [ $found_pids -eq 0 ]; then 241 | echo "${SCRIPT_NAME}: All nodes down" 242 | exit $my_exitcode 243 | fi 244 | done 245 | } 246 | 247 | 248 | _chuid () { 249 | ${CELERYD_SU} ${CELERYD_SU_ARGS} "$CELERYD_USER" -c "$CELERYD_MULTI $*" 250 | } 251 | 252 | 253 | start_workers () { 254 | if [ ! -z "$CELERYD_ULIMIT" ]; then 255 | ulimit $CELERYD_ULIMIT 256 | fi 257 | _chuid $* start $CELERYD_NODES $DAEMON_OPTS \ 258 | --pidfile="$CELERYD_PID_FILE" \ 259 | --logfile="$CELERYD_LOG_FILE" \ 260 | --loglevel="$CELERYD_LOG_LEVEL" \ 261 | $CELERY_APP_ARG \ 262 | $CELERYD_OPTS 263 | } 264 | 265 | 266 | dryrun () { 267 | (C_FAKEFORK=1 start_workers --verbose) 268 | } 269 | 270 | 271 | stop_workers () { 272 | _chuid stopwait $CELERYD_NODES $DAEMON_OPTS --pidfile="$CELERYD_PID_FILE" 273 | } 274 | 275 | 276 | restart_workers () { 277 | _chuid restart $CELERYD_NODES $DAEMON_OPTS \ 278 | --pidfile="$CELERYD_PID_FILE" \ 279 | --logfile="$CELERYD_LOG_FILE" \ 280 | --loglevel="$CELERYD_LOG_LEVEL" \ 281 | $CELERY_APP_ARG \ 282 | $CELERYD_OPTS 283 | } 284 | 285 | 286 | kill_workers() { 287 | _chuid kill $CELERYD_NODES $DAEMON_OPTS --pidfile="$CELERYD_PID_FILE" 288 | } 289 | 290 | 291 | restart_workers_graceful () { 292 | echo "WARNING: Use with caution in production" 293 | echo "The workers will attempt to restart, but they may not be able to." 294 | local worker_pids= 295 | worker_pids=`_get_pids` 296 | [ "$one_failed" ] && exit 1 297 | 298 | for worker_pid in $worker_pids; do 299 | local failed= 300 | kill -HUP $worker_pid 2> /dev/null || failed=true 301 | if [ "$failed" ]; then 302 | echo "${SCRIPT_NAME} worker (pid $worker_pid) could not be restarted" 303 | one_failed=true 304 | else 305 | echo "${SCRIPT_NAME} worker (pid $worker_pid) received SIGHUP" 306 | fi 307 | done 308 | 309 | [ "$one_failed" ] && exit 1 || exit 0 310 | } 311 | 312 | 313 | check_status () { 314 | my_exitcode=0 315 | found_pids=0 316 | 317 | local one_failed= 318 | for pidfile in $(_get_pidfiles); do 319 | if [ ! -r $pidfile ]; then 320 | echo "${SCRIPT_NAME} down: no pidfiles found" 321 | one_failed=true 322 | break 323 | fi 324 | 325 | local node=`basename "$pidfile" .pid` 326 | local pid=`cat "$pidfile"` 327 | local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'` 328 | if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then 329 | echo "bad pid file ($pidfile)" 330 | one_failed=true 331 | else 332 | local failed= 333 | kill -0 $pid 2> /dev/null || failed=true 334 | if [ "$failed" ]; then 335 | echo "${SCRIPT_NAME} (node $node) (pid $pid) is down, but pidfile exists!" 336 | one_failed=true 337 | else 338 | echo "${SCRIPT_NAME} (node $node) (pid $pid) is up..." 339 | fi 340 | fi 341 | done 342 | 343 | [ "$one_failed" ] && exit 1 || exit 0 344 | } 345 | 346 | 347 | case "$1" in 348 | start) 349 | check_dev_null 350 | check_paths 351 | start_workers 352 | ;; 353 | 354 | stop) 355 | check_dev_null 356 | check_paths 357 | stop_workers 358 | ;; 359 | 360 | reload|force-reload) 361 | echo "Use restart" 362 | ;; 363 | 364 | status) 365 | check_status 366 | ;; 367 | 368 | restart) 369 | check_dev_null 370 | check_paths 371 | restart_workers 372 | ;; 373 | 374 | graceful) 375 | check_dev_null 376 | restart_workers_graceful 377 | ;; 378 | 379 | kill) 380 | check_dev_null 381 | kill_workers 382 | ;; 383 | 384 | dryrun) 385 | check_dev_null 386 | dryrun 387 | ;; 388 | 389 | try-restart) 390 | check_dev_null 391 | check_paths 392 | restart_workers 393 | ;; 394 | 395 | create-paths) 396 | check_dev_null 397 | create_paths 398 | ;; 399 | 400 | check-paths) 401 | check_dev_null 402 | check_paths 403 | ;; 404 | 405 | *) 406 | echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|graceful|kill|dryrun|create-paths}" 407 | exit 64 # EX_USAGE 408 | ;; 409 | esac 410 | 411 | exit 0 412 | -------------------------------------------------------------------------------- /docker/celery/config/celeryd: -------------------------------------------------------------------------------- 1 | CELERYD_NODES="worker" 2 | 3 | # App instance to use 4 | # comment out this line if you don't use an app 5 | CELERY_APP="findmyhitman" 6 | CELERY_BIN="/usr/local/bin/celery" 7 | 8 | # Where to chdir at start. 9 | CELERYD_CHDIR="/usr/src/app/findmyhitman" 10 | CELERYBEAT_CHDIR="/usr/src/app/findmyhitman" 11 | 12 | # Extra command-line arguments to the worker 13 | # CELERYD_OPTS="-Q:celery" 14 | CELERYBEAT_OPTS="--scheduler django_celery_beat.schedulers:DatabaseScheduler" 15 | 16 | # Set logging level to DEBUG 17 | CELERYD_LOG_LEVEL="DEBUG" 18 | 19 | # %n will be replaced with the first part of the nodename. 20 | CELERYD_LOG_FILE="/usr/src/app/logs/celery/%n%I.log" 21 | CELERYD_PID_FILE="/usr/src/app/logs/celery/pid/%n.pid" 22 | 23 | CELERYD_USER="root" 24 | CELERYD_GROUP="celery" 25 | 26 | CELERY_CREATE_DIRS=1 27 | -------------------------------------------------------------------------------- /docker/metrics-server.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | k8s-app: metrics-server 6 | name: metrics-server 7 | namespace: kube-system 8 | --- 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | kind: ClusterRole 11 | metadata: 12 | labels: 13 | k8s-app: metrics-server 14 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 15 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 16 | rbac.authorization.k8s.io/aggregate-to-view: "true" 17 | name: system:aggregated-metrics-reader 18 | rules: 19 | - apiGroups: 20 | - metrics.k8s.io 21 | resources: 22 | - pods 23 | - nodes 24 | verbs: 25 | - get 26 | - list 27 | - watch 28 | --- 29 | apiVersion: rbac.authorization.k8s.io/v1 30 | kind: ClusterRole 31 | metadata: 32 | labels: 33 | k8s-app: metrics-server 34 | name: system:metrics-server 35 | rules: 36 | - apiGroups: 37 | - "" 38 | resources: 39 | - nodes/metrics 40 | verbs: 41 | - get 42 | - apiGroups: 43 | - "" 44 | resources: 45 | - pods 46 | - nodes 47 | verbs: 48 | - get 49 | - list 50 | - watch 51 | --- 52 | apiVersion: rbac.authorization.k8s.io/v1 53 | kind: RoleBinding 54 | metadata: 55 | labels: 56 | k8s-app: metrics-server 57 | name: metrics-server-auth-reader 58 | namespace: kube-system 59 | roleRef: 60 | apiGroup: rbac.authorization.k8s.io 61 | kind: Role 62 | name: extension-apiserver-authentication-reader 63 | subjects: 64 | - kind: ServiceAccount 65 | name: metrics-server 66 | namespace: kube-system 67 | --- 68 | apiVersion: rbac.authorization.k8s.io/v1 69 | kind: ClusterRoleBinding 70 | metadata: 71 | labels: 72 | k8s-app: metrics-server 73 | name: metrics-server:system:auth-delegator 74 | roleRef: 75 | apiGroup: rbac.authorization.k8s.io 76 | kind: ClusterRole 77 | name: system:auth-delegator 78 | subjects: 79 | - kind: ServiceAccount 80 | name: metrics-server 81 | namespace: kube-system 82 | --- 83 | apiVersion: rbac.authorization.k8s.io/v1 84 | kind: ClusterRoleBinding 85 | metadata: 86 | labels: 87 | k8s-app: metrics-server 88 | name: system:metrics-server 89 | roleRef: 90 | apiGroup: rbac.authorization.k8s.io 91 | kind: ClusterRole 92 | name: system:metrics-server 93 | subjects: 94 | - kind: ServiceAccount 95 | name: metrics-server 96 | namespace: kube-system 97 | --- 98 | apiVersion: v1 99 | kind: Service 100 | metadata: 101 | labels: 102 | k8s-app: metrics-server 103 | name: metrics-server 104 | namespace: kube-system 105 | spec: 106 | ports: 107 | - name: https 108 | port: 443 109 | protocol: TCP 110 | targetPort: https 111 | selector: 112 | k8s-app: metrics-server 113 | --- 114 | apiVersion: apps/v1 115 | kind: Deployment 116 | metadata: 117 | labels: 118 | k8s-app: metrics-server 119 | name: metrics-server 120 | namespace: kube-system 121 | spec: 122 | selector: 123 | matchLabels: 124 | k8s-app: metrics-server 125 | strategy: 126 | rollingUpdate: 127 | maxUnavailable: 0 128 | template: 129 | metadata: 130 | labels: 131 | k8s-app: metrics-server 132 | spec: 133 | containers: 134 | - args: 135 | - --cert-dir=/tmp 136 | - --secure-port=4443 137 | - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname 138 | - --kubelet-use-node-status-port 139 | - --metric-resolution=15s 140 | - --kubelet-insecure-tls 141 | image: k8s.gcr.io/metrics-server/metrics-server:v0.6.1 142 | imagePullPolicy: IfNotPresent 143 | livenessProbe: 144 | failureThreshold: 3 145 | httpGet: 146 | path: /livez 147 | port: https 148 | scheme: HTTPS 149 | periodSeconds: 10 150 | name: metrics-server 151 | ports: 152 | - containerPort: 4443 153 | name: https 154 | protocol: TCP 155 | readinessProbe: 156 | failureThreshold: 3 157 | httpGet: 158 | path: /readyz 159 | port: https 160 | scheme: HTTPS 161 | initialDelaySeconds: 20 162 | periodSeconds: 10 163 | resources: 164 | requests: 165 | cpu: 100m 166 | memory: 200Mi 167 | securityContext: 168 | allowPrivilegeEscalation: false 169 | readOnlyRootFilesystem: true 170 | runAsNonRoot: true 171 | runAsUser: 1000 172 | volumeMounts: 173 | - mountPath: /tmp 174 | name: tmp-dir 175 | nodeSelector: 176 | kubernetes.io/os: linux 177 | priorityClassName: system-cluster-critical 178 | serviceAccountName: metrics-server 179 | volumes: 180 | - emptyDir: {} 181 | name: tmp-dir 182 | --- 183 | apiVersion: apiregistration.k8s.io/v1 184 | kind: APIService 185 | metadata: 186 | labels: 187 | k8s-app: metrics-server 188 | name: v1beta1.metrics.k8s.io 189 | spec: 190 | group: metrics.k8s.io 191 | groupPriorityMinimum: 100 192 | insecureSkipTLSVerify: true 193 | service: 194 | name: metrics-server 195 | namespace: kube-system 196 | version: v1beta1 197 | versionPriority: 100 198 | -------------------------------------------------------------------------------- /docker/rest-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.1 2 | 3 | EXPOSE 8000 4 | 5 | ENV DEBIAN_FRONTEND noninteractive 6 | ENV PYTHONDONTWRITEBYTECODE 1 7 | ENV PYTHONUNBUFFERED 1 8 | ENV LC_ALL C.UTF-8 9 | ENV LANG C.UTF-8 10 | 11 | RUN apt-get update && apt-get -y upgrade 12 | RUN apt-get install -y gcc 13 | RUN apt-get install -y g++ 14 | RUN apt-get install -y build-essential 15 | RUN apt-get install -y wget 16 | 17 | WORKDIR /usr/src/app 18 | COPY ./ ./ 19 | 20 | RUN pip install -r ./requirements.txt 21 | 22 | RUN ["chmod", "+x", "./docker/rest-api/entry.sh"] 23 | ENTRYPOINT ["./docker/rest-api/entry.sh"] 24 | -------------------------------------------------------------------------------- /docker/rest-api/entry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # Run tests 5 | cd ./findmyhitman/ 6 | python manage.py migrate 7 | rm -rf /usr/src/app/logs/ 8 | mkdir /usr/src/app/logs/ 9 | touch /usr/src/app/logs/gunicorn.log 10 | touch /usr/src/app/logs/access.log 11 | 12 | exec python manage.py runserver 0.0.0.0:8000 13 | -------------------------------------------------------------------------------- /docker/rest-api/rest-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hitman-rest-api-deployment 5 | namespace: default 6 | labels: 7 | app: hitman-rest-api 8 | spec: 9 | replicas: 2 10 | strategy: 11 | type: RollingUpdate 12 | rollingUpdate: 13 | maxSurge: 1 14 | maxUnavailable: 0 15 | selector: 16 | matchLabels: 17 | app: hitman-rest-api 18 | template: 19 | metadata: 20 | labels: 21 | app: hitman-rest-api 22 | annotations: 23 | cluster-autoscaler.kubernetes.io/safe-to-evict: "false" 24 | spec: 25 | terminationGracePeriodSeconds: 300 26 | imagePullSecrets: 27 | - name: dockerconfigjson-github-com 28 | containers: 29 | - name: hitman-rest-api-docker-image 30 | image: ghcr.io/john-doherty01/hitman-rest-api:VERSION 31 | imagePullPolicy: IfNotPresent 32 | resources: 33 | requests: 34 | cpu: "250m" 35 | memory: "100M" 36 | limits: 37 | cpu: "500m" 38 | memory: "1000M" 39 | ports: 40 | - containerPort: 8000 41 | readinessProbe: 42 | httpGet: 43 | path: /admin/login/?next=/admin/ 44 | port: 8000 45 | initialDelaySeconds: 30 46 | periodSeconds: 20 47 | livenessProbe: 48 | httpGet: 49 | path: /admin/login/?next=/admin/ 50 | port: 8000 51 | initialDelaySeconds: 15 52 | periodSeconds: 120 53 | env: 54 | - name: DATABASE_NAME 55 | valueFrom: 56 | secretKeyRef: 57 | name: hitmansecret 58 | key: database_name 59 | optional: false 60 | - name: DATABASE_USER 61 | valueFrom: 62 | secretKeyRef: 63 | name: hitmansecret 64 | key: database_user 65 | optional: false 66 | - name: DATABASE_PASSWORD 67 | valueFrom: 68 | secretKeyRef: 69 | name: hitmansecret 70 | key: database_password 71 | optional: false 72 | - name: DATABASE_HOST 73 | valueFrom: 74 | secretKeyRef: 75 | name: hitmansecret 76 | key: database_host 77 | optional: false 78 | - name: DATABASE_PORT 79 | valueFrom: 80 | secretKeyRef: 81 | name: hitmansecret 82 | key: database_port 83 | optional: false 84 | - name: ALLOWED_HOST 85 | valueFrom: 86 | secretKeyRef: 87 | name: hitmansecret 88 | key: allowed_host 89 | optional: false 90 | - name: CORS_ALLOWED_ORIGINS 91 | valueFrom: 92 | secretKeyRef: 93 | name: hitmansecret 94 | key: cors_allowed_origins 95 | optional: false 96 | - name: CELERY_BROKER_URL 97 | valueFrom: 98 | secretKeyRef: 99 | name: hitmansecret 100 | key: celery_broker_url 101 | optional: false 102 | - name: CELERY_RESULT_URL 103 | valueFrom: 104 | secretKeyRef: 105 | name: hitmansecret 106 | key: celery_result_url 107 | optional: false 108 | - name: CHANNELS_URLS 109 | valueFrom: 110 | secretKeyRef: 111 | name: hitmansecret 112 | key: channels_url 113 | optional: false 114 | 115 | --- 116 | apiVersion: v1 117 | kind: Service 118 | metadata: 119 | name: hitman-rest-api-load-balancer 120 | spec: 121 | selector: 122 | app: hitman-rest-api 123 | ports: 124 | - port: 80 125 | name: http 126 | targetPort: 8000 127 | - port: 443 128 | name: https 129 | targetPort: 8000 130 | type: LoadBalancer 131 | -------------------------------------------------------------------------------- /docker/webapp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17.5-alpine 2 | ARG REACT_ENV_PROD 3 | EXPOSE 3000 4 | 5 | WORKDIR /usr/src/app 6 | ENV PATH /usr/src/app/hitman-webapp/node_modules/.bin:$PATH 7 | COPY ./ ./ 8 | WORKDIR /usr/src/app/hitman-webapp/ 9 | RUN npm install 10 | RUN touch .env 11 | RUN echo "$REACT_ENV_PROD" > .env 12 | RUN npm run build 13 | 14 | CMD ["node", "index.ts"] -------------------------------------------------------------------------------- /docker/webapp/webapp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hitman-webapp-deployment 5 | namespace: default 6 | labels: 7 | app: hitman-webapp 8 | spec: 9 | replicas: 1 10 | strategy: 11 | type: RollingUpdate 12 | rollingUpdate: 13 | maxSurge: 1 14 | maxUnavailable: 0 15 | selector: 16 | matchLabels: 17 | app: hitman-webapp 18 | template: 19 | metadata: 20 | labels: 21 | app: hitman-webapp 22 | annotations: 23 | cluster-autoscaler.kubernetes.io/safe-to-evict: "false" 24 | spec: 25 | terminationGracePeriodSeconds: 300 26 | imagePullSecrets: 27 | - name: dockerconfigjson-github-com 28 | containers: 29 | - name: hitman-webapp-docker-image 30 | image: ghcr.io/john-doherty01/hitman-webapp:VERSION 31 | imagePullPolicy: IfNotPresent 32 | resources: 33 | requests: 34 | cpu: "250m" 35 | memory: "100M" 36 | limits: 37 | cpu: "500m" 38 | memory: "1000M" 39 | ports: 40 | - containerPort: 3000 41 | readinessProbe: 42 | httpGet: 43 | path: / 44 | port: 3000 45 | initialDelaySeconds: 30 46 | periodSeconds: 20 47 | livenessProbe: 48 | httpGet: 49 | path: / 50 | port: 3000 51 | initialDelaySeconds: 15 52 | periodSeconds: 120 53 | 54 | --- 55 | 56 | apiVersion: v1 57 | kind: Service 58 | metadata: 59 | name: hitman-webapp-load-balancer 60 | spec: 61 | selector: 62 | app: hitman-webapp 63 | ports: 64 | - port: 80 65 | name: http 66 | targetPort: 3000 67 | - port: 443 68 | name: https 69 | targetPort: 3000 70 | type: LoadBalancer -------------------------------------------------------------------------------- /findmyhitman/findmyhitman/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .celery import app as celery_app 6 | 7 | __all__ = ("celery_app",) 8 | -------------------------------------------------------------------------------- /findmyhitman/findmyhitman/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for findmyhitman project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from channels.routing import ProtocolTypeRouter, URLRouter 13 | from django.core.asgi import get_asgi_application 14 | from channels.auth import AuthMiddlewareStack 15 | from .urls import websocket_urlpatterns 16 | 17 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "findmyhitman.settings") 18 | 19 | application = ProtocolTypeRouter( 20 | { 21 | "http": get_asgi_application(), 22 | "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)), 23 | } 24 | ) 25 | -------------------------------------------------------------------------------- /findmyhitman/findmyhitman/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | import os 3 | from celery import Celery 4 | from datetime import timedelta 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "findmyhitman.settings") 7 | app = Celery("hitman_rest_api") 8 | app.config_from_object("django.conf:settings", namespace="CELERY") 9 | app.autodiscover_tasks() 10 | 11 | app.conf.result_chord_join_timeout = 900 12 | app.conf.result_chord_retry_interval = 5 13 | app.conf.result_expires = timedelta(days=3) 14 | 15 | 16 | @app.task(bind=True) 17 | def debug_task(self): 18 | print("Request: {0!r}".format(self.request)) 19 | -------------------------------------------------------------------------------- /findmyhitman/findmyhitman/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for findmyhitman project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = "django-insecure-i*d^)wbvs5b5*j=n3i4^(sa($v2#nb($08d22a=8jwne#lrjch" 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [os.environ.get("ALLOWED_HOST", "*")] 28 | 29 | CORS_ALLOWED_ORIGINS = [ 30 | os.environ.get("CORS_ALLOWED_ORIGINS", "http://localhost:3000") 31 | ] 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | "channels", 37 | "django.contrib.admin", 38 | "django.contrib.auth", 39 | "django.contrib.contenttypes", 40 | "django.contrib.sessions", 41 | "django.contrib.messages", 42 | "django.contrib.staticfiles", 43 | "oauth2_provider", 44 | "rest_framework", 45 | "django_celery_beat", 46 | "corsheaders", 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | "django.middleware.security.SecurityMiddleware", 51 | "django.contrib.sessions.middleware.SessionMiddleware", 52 | "corsheaders.middleware.CorsMiddleware", 53 | "django.middleware.common.CommonMiddleware", 54 | "django.middleware.csrf.CsrfViewMiddleware", 55 | "django.contrib.auth.middleware.AuthenticationMiddleware", 56 | "django.contrib.messages.middleware.MessageMiddleware", 57 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 58 | ] 59 | 60 | ROOT_URLCONF = "findmyhitman.urls" 61 | 62 | TEMPLATES = [ 63 | { 64 | "BACKEND": "django.template.backends.django.DjangoTemplates", 65 | "DIRS": [], 66 | "APP_DIRS": True, 67 | "OPTIONS": { 68 | "context_processors": [ 69 | "django.template.context_processors.debug", 70 | "django.template.context_processors.request", 71 | "django.contrib.auth.context_processors.auth", 72 | "django.contrib.messages.context_processors.messages", 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = "findmyhitman.wsgi.application" 79 | ASGI_APPLICATION = "findmyhitman.asgi.application" 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 83 | 84 | DATABASES = { 85 | "default": { 86 | "ENGINE": "django.db.backends.postgresql", 87 | "NAME": os.environ.get("DATABASE_NAME", "hitman_db"), 88 | "USER": os.environ.get("DATABASE_USER", "hitman"), 89 | "PASSWORD": os.environ.get("DATABASE_PASSWORD", "hitman"), 90 | "HOST": os.environ.get("DATABASE_HOST", "localhost"), 91 | "PORT": os.environ.get("DATABASE_PORT", "5432"), 92 | } 93 | } 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 101 | }, 102 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, 103 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, 104 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, 105 | ] 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 109 | 110 | LANGUAGE_CODE = "en-us" 111 | 112 | TIME_ZONE = "UTC" 113 | 114 | USE_I18N = True 115 | 116 | USE_TZ = True 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 120 | 121 | STATIC_URL = "static/" 122 | PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) 123 | STATIC_ROOT = os.path.join(PROJECT_ROOT, "static") 124 | 125 | 126 | # Default primary key field type 127 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 128 | 129 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 130 | 131 | CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0") 132 | CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_URL", "redis://localhost:6379/0",) 133 | CELERY_ACCEPT_CONTENT = ["json"] 134 | CELERY_TASK_SERIALIZER = "json" 135 | CELERY_TIMEZONE = "UTC" 136 | CELERY_TASK_REJECT_ON_WORKER_LOST = True 137 | CELERY_TASK_TRACK_STARTED = True 138 | CELERY_ACKS_LATE = True 139 | CELERY_WORKER_SEND_TASK_EVENTS = True 140 | CELERY_TASK_SEND_SENT_EVENT = True 141 | 142 | CHANNEL_LAYERS = { 143 | "default": { 144 | "BACKEND": "channels_redis.core.RedisChannelLayer", 145 | "CONFIG": { 146 | "hosts": [(os.environ.get("CHANNELS_URLS", "redis://localhost:6379/0"))], 147 | }, 148 | }, 149 | } 150 | 151 | OIDC_KEY = Path(str(BASE_DIR) + "/oidc.key").read_text() 152 | 153 | OAUTH2_PROVIDER = { 154 | "OIDC_ENABLED": True, 155 | "OIDC_RSA_PRIVATE_KEY": OIDC_KEY, 156 | "SCOPES": {"all": "all scopes"}, 157 | } 158 | 159 | REST_FRAMEWORK = { 160 | "DEFAULT_AUTHENTICATION_CLASSES": [ 161 | "oauth2_provider.contrib.rest_framework.OAuth2Authentication", 162 | ] 163 | } 164 | 165 | LOGIN_URL = "/admin/login/" 166 | -------------------------------------------------------------------------------- /findmyhitman/findmyhitman/urls.py: -------------------------------------------------------------------------------- 1 | """findmyhitman URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | from hitman_rest_api.channels import TaskProgressConsumer 20 | from hitman_rest_api.views import GetHitmen, StartNewHitJob, ScheduleNewHitJob, CreateUserView 21 | 22 | urlpatterns = [ 23 | path("admin/", admin.site.urls), 24 | path("api-auth/", include("rest_framework.urls")), 25 | path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")), 26 | path("user/create", CreateUserView.as_view()), 27 | path("hitmen/all", GetHitmen.as_view()), 28 | path("hitmen/start-job", StartNewHitJob.as_view()), 29 | path("hitmen/schedule", ScheduleNewHitJob.as_view()), 30 | ] 31 | 32 | websocket_urlpatterns = [ 33 | path("task/progress//", TaskProgressConsumer.as_asgi()), 34 | ] 35 | -------------------------------------------------------------------------------- /findmyhitman/findmyhitman/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for findmyhitman project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "findmyhitman.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /findmyhitman/hitman_rest_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-Doherty01/docker-celery/50e909aab1df6cb594a3b5c68104a83e56d77d62/findmyhitman/hitman_rest_api/__init__.py -------------------------------------------------------------------------------- /findmyhitman/hitman_rest_api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /findmyhitman/hitman_rest_api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HitmanRestApiConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "hitman_rest_api" 7 | -------------------------------------------------------------------------------- /findmyhitman/hitman_rest_api/channels.py: -------------------------------------------------------------------------------- 1 | from asgiref.sync import async_to_sync 2 | from channels.generic.websocket import JsonWebsocketConsumer 3 | 4 | 5 | class TaskProgressConsumer(JsonWebsocketConsumer): 6 | def celery_task_update(self, event): 7 | message = event["message"] 8 | self.send_json(message) 9 | 10 | def connect(self): 11 | super().connect() 12 | taskID = self.scope.get("url_route").get("kwargs").get("taskID") 13 | async_to_sync(self.channel_layer.group_add)(taskID, self.channel_name) 14 | 15 | def receive(self, text_data=None, bytes_data=None, **kwargs): 16 | self.send(text_data="Hello world!") 17 | 18 | def disconnect(self, close_code): 19 | self.close() 20 | -------------------------------------------------------------------------------- /findmyhitman/hitman_rest_api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-Doherty01/docker-celery/50e909aab1df6cb594a3b5c68104a83e56d77d62/findmyhitman/hitman_rest_api/migrations/__init__.py -------------------------------------------------------------------------------- /findmyhitman/hitman_rest_api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /findmyhitman/hitman_rest_api/tasks.py: -------------------------------------------------------------------------------- 1 | import random 2 | from time import sleep 3 | 4 | from asgiref.sync import async_to_sync 5 | from celery import shared_task 6 | from channels.layers import get_channel_layer 7 | 8 | 9 | @shared_task(bind=True) 10 | def start_new_hit_job(self, target_name): 11 | channel_layer = get_channel_layer() 12 | task_id = self.request.id 13 | async_to_sync(channel_layer.group_send)( 14 | task_id, 15 | { 16 | "type": "celery_task_update", 17 | "message": {"progress": 0.1, "status": "Processing"}, 18 | }, 19 | ) 20 | random_number = random.randint(1, 10) 21 | sleep(random_number) 22 | async_to_sync(channel_layer.group_send)( 23 | task_id, 24 | { 25 | "type": "celery_task_update", 26 | "message": {"progress": 0.2, "status": "Processing"}, 27 | }, 28 | ) 29 | random_number = random.randint(1, 10) 30 | sleep(random_number) 31 | async_to_sync(channel_layer.group_send)( 32 | task_id, 33 | { 34 | "type": "celery_task_update", 35 | "message": {"progress": 0.3, "status": "Processing"}, 36 | }, 37 | ) 38 | 39 | random_number = random.randint(1, 10) 40 | sleep(random_number) 41 | async_to_sync(channel_layer.group_send)( 42 | task_id, 43 | { 44 | "type": "celery_task_update", 45 | "message": {"progress": 0.4, "status": "Processing"}, 46 | }, 47 | ) 48 | 49 | random_number = random.randint(1, 10) 50 | sleep(random_number) 51 | async_to_sync(channel_layer.group_send)( 52 | task_id, 53 | { 54 | "type": "celery_task_update", 55 | "message": {"progress": 1, "status": "Complete"}, 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /findmyhitman/hitman_rest_api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /findmyhitman/hitman_rest_api/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from rest_framework import permissions, status, serializers 4 | from rest_framework.generics import CreateAPIView 5 | from rest_framework.response import Response 6 | from rest_framework.views import APIView 7 | from hitman_rest_api.tasks import start_new_hit_job 8 | from django_celery_beat.models import PeriodicTask, CrontabSchedule 9 | from dateutil import parser 10 | from django.contrib.auth import get_user_model # If used custom user model 11 | 12 | UserModel = get_user_model() 13 | 14 | 15 | class UserSerializer(serializers.ModelSerializer): 16 | 17 | password = serializers.CharField(write_only=True) 18 | 19 | def create(self, validated_data): 20 | user = UserModel.objects.create_user( 21 | username=validated_data["username"], password=validated_data["password"], 22 | first_name=validated_data["first_name"], last_name=validated_data["last_name"], 23 | ) 24 | 25 | return user 26 | 27 | class Meta: 28 | model = UserModel 29 | fields = ( 30 | "id", 31 | "username", 32 | "password", 33 | "first_name", 34 | "last_name" 35 | ) 36 | 37 | 38 | class StringListField(serializers.ListField): 39 | day = serializers.CharField(allow_blank=False, min_length=2, max_length=400) 40 | 41 | 42 | class ScheduleJobSerializer(serializers.Serializer): 43 | target_name = serializers.CharField(allow_blank=False, min_length=2, max_length=400) 44 | days_of_week = StringListField() 45 | schedule_time = serializers.DateTimeField() 46 | 47 | class Meta: 48 | fields = ("target_name", "schedule_time", "days_of_week") 49 | 50 | 51 | class HitJobSerializer(serializers.Serializer): 52 | target_name = serializers.CharField(allow_blank=False, min_length=2, max_length=400) 53 | 54 | class Meta: 55 | fields = "target_name" 56 | 57 | 58 | class GetHitmen(APIView): 59 | permission_classes = [permissions.IsAuthenticated] 60 | 61 | def get(self, request): 62 | return Response( 63 | data={"names": ["bill", "bob", "keanu", "logan"]}, status=status.HTTP_200_OK 64 | ) 65 | 66 | 67 | class StartNewHitJob(APIView): 68 | permission_classes = [permissions.AllowAny] 69 | serializer_class = HitJobSerializer 70 | 71 | def post(self, request): 72 | name = request.data.get("target_name") 73 | new_celery_task = start_new_hit_job.delay(name) 74 | return Response( 75 | data={ 76 | "result": f"Job created for {name}", 77 | "celery_task_id": new_celery_task.id, 78 | }, 79 | status=status.HTTP_200_OK, 80 | ) 81 | 82 | 83 | class ScheduleNewHitJob(APIView): 84 | permission_classes = [permissions.AllowAny] 85 | serializer_class = ScheduleJobSerializer 86 | 87 | def post(self, request): 88 | name = request.data.get("target_name") 89 | schedule_time = parser.parse(request.data.get("schedule_time")) 90 | schedule, _ = CrontabSchedule.objects.get_or_create( 91 | day_of_week=",".join(request.data.get("days_of_week")), 92 | minute=schedule_time.minute, 93 | hour=schedule_time.hour, 94 | ) 95 | 96 | new_celery_task = PeriodicTask.objects.update_or_create( 97 | name=f"Schedule hit job for {name}", 98 | defaults={ 99 | "task": "hitman_rest_api.tasks.start_new_hit_job", 100 | "args": json.dumps([name]), 101 | "crontab": schedule, 102 | }, 103 | ) 104 | 105 | return Response( 106 | data={"result": f"Task scheduled for execution"}, status=status.HTTP_200_OK, 107 | ) 108 | 109 | 110 | class CreateUserView(CreateAPIView): 111 | model = get_user_model() 112 | permission_classes = [permissions.AllowAny] 113 | serializer_class = UserSerializer 114 | -------------------------------------------------------------------------------- /findmyhitman/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "findmyhitman.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /findmyhitman/oidc.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKgIBAAKCAgEAz9/FOAcd3RVJZomybgczWCGE3CCyl4CCVpGQ63VSHcZoa6b4 3 | Mso7IXuHd3wzgM6O+zJXqb9vRb4xki+8syvOpXmTqQGMKnOjvzbGOgr+iAC1rJfW 4 | eNXVIJMExHJ4xfOW/mARE7zWc0f3lOsEgXys7BMJl6LhG6YNZCvAOhfQ8+WmX8D3 5 | 7vBLtK5C7K3VpmZGpK5u2PfOkHkMY5Coslq8TzsS8MaNO0G/53OYzZPDxPjR2Ghc 6 | /bElKPdZppnsopVtW84cudXDLKlxHERv9fji14bMtmQqVk+bCmXbruDQgyFPoqxk 7 | 6SB1ykY/rghGKYMSvL6Ol+eoqZKSfwO3TcR9uT1Y6obRW6p5lBjPNohc/0ezpng7 8 | NcGrR5FXgnelD+C4E0S+I+PIuhaha4FWMyGkE66vWA2YyRG5PPFTLXnVuI/k9bcn 9 | NlVTH6Q2Vyjbt8N1H/7By6QYB4t4Z6u8BOr4ErArp9fRddWJGimE9VnddalRLI1f 10 | y+ZdCVxQ6UEMdiFdJoYO5S9TglJ0OjFYRM/KJji5JYcPBn55p5yTR/BEtjH+3DL6 11 | /D7mTE+odTuFgP12Cx22BaWrdf4pOxlyqJf7CukqkK3UmWYYcPnyKBcpAVBeyHt/ 12 | M5W9lHXnAeTRAOlLW97peYKV2U0m/l0tAsGyyk86moo+b4Y3RfjpKFzPvJkCAwEA 13 | AQKCAgA2tU3SUQ0rAk8vDU5IZty8dRXiWTinZsrmvajGTzotW+pfarInq9GkHj6o 14 | 06c6XyKCOd2Ybyl50XR6ZNXk0cKxCi4ybJXXFlgiGuhQnlQetxEZO1zWsHBHAmpe 15 | yUG/1zwF9oKID0rhTC98od2ttoJMHzaHEfbsr+cWVe1hXKNBjU8cJE0cNWG3S6WR 16 | ZK61/HTbIo1JzO4wV6tLCOddCURGFeYGODszPTqkT2gHsrvzOwj/S3gtzFFbVr3h 17 | euEsaOc/Ih+tSPT07CM4rUpKEhahYni+B96cmJbVRblrbP9zg3DR/00PjdtyQMrB 18 | oqvUgQqNYhllITLibhDgHLpBXVU40dH8pxv9nfwrTg6XOpa+p862lkvgPnMTyi3k 19 | dEhD5Y4VXJpvSRGoLdB1rdGp0AKFmS5+eUea6siWc5+tOweFym2uyW4Ns+OMZGsY 20 | CVxA2BgQR6F93XE2Y6mKWjYKJZbek1lWK2aBryGGRt0A9MI0DvUDqoKjeu+ZnukG 21 | o0yDuMPustQ9iEnswD7EazKcX0fpMpl17MvufKlfdwN0F6akwTD90/IziN5wJ67y 22 | pcKmM/JqKo8Df/acigYOrhaVJcgje/ltlDjuq9m+hQgUe766VlsNBUWfE0bMFY9B 23 | X43UcEg7odQbaebTofO73yGup6ZkOHv9gkkomr3rXGh/PGjQAQKCAQEA7Zzi7esR 24 | eFuBfMhwH+S9bri8MmlkES1ZyN3g8eXzzl/sfe7JeJ+ZYOqhQzYWZ5JfAzQYzHV1 25 | iZ3rbqK8peHac8/UMhX1jeik2SxDNCynq4IM9+BzvbphOaQpAH63NlKnlvIOtcmd 26 | 0hn6MCdI1emfJiWl6MW2lPvcn5iCkPt1wMgDWimcrp/azU8m+MtsJCgpChTeWuzr 27 | O3J9AYROFuYu84I7LXv3h2+PO2F53zKc0C5oesVUB6vrWlE0XgsPM3sxG9BZ94TR 28 | CjHTEYCPXQnhxFQiXh+BwP1GqNpax+5wQ1n9jUFKynTDz1ZHZ7W1m7BKlw+eJBjv 29 | 5FrEXvtL6PdDMQKCAQEA3/XCW0PJGQmDBY6IhyJkENOEju0IJ+S5Gp38NNDMjJ2R 30 | 9R74DRsZHlAl7yyV0K8R0xROVmuxxkYLv6E6wwVtwRPOiOn2qEm0JO6VUIYQvdBH 31 | Rrw3JNX+WeeCiBZK67nHkBVcRMBoFAKEii2WZ2iLsKGDU0qO8SfgDeXqESXLlD0p 32 | Ag5cgJdR4KPRh4aPQxRTtIoHb4yQ7Dt4BHRzgofnEYS3vvcSFqmpg+eZsBMR6XiV 33 | YfkIyU5vjuhIc5vLyqeb/0Sy8oi78G+5eTTmFnPP6rcBxqfStVEsm7kyDe+RJrRl 34 | xNZLPvuZZfVRg4NyDmy2WxLpbSv8fLfYIIVvnYWl6QKCAQEAoW1wjoqg98/TDN9v 35 | rXtNZyeCVA1dun3A/cIfzLeZB6ALnxLXNWfYYAuHO45Hcl3yxs5h+qXiEHiFNkjB 36 | OUFTlFKhqtVYWeyv8gssLQoZb1+PR+fpOgChhPOOF5P/DJzi1p3keIRdABw96PEH 37 | fqyVPulc6eNIw7bIkHSgX7c2rJ42CM80bz6S++DGbUhmU7olB4BeAA/tUBnb62fj 38 | VZNUWoMjiScrZ4vXF1SF46kS+CxPNvlQuVWHM3jvxuRkk2kiV+8Y8Bv/mJl49+8L 39 | dv37r9Io9yEYmfB4TMbr3eCTzG4a4Bh3o0voq7fGEyNUGk2On0ow+f0nHVC9EHpR 40 | fBhgQQKCAQEArBF1sgjyiHn2YF6SJsEVgJgCUI1sprnkPb8D9tA1WBP4AAw7KFaZ 41 | heCuRIRM1niriTEgvuUfQOawQBrvusaA/Mbq9+ZJzCJUdcuYjLV0R2GuYXpaI8MG 42 | GKz2bPgzt8iUuu6uZWQukrEcg81isTBxotN0wpmWGVI5onPy0hnnv4z95MaD64hX 43 | L/CtGyMXDsKfTSZ/cATfk6BdJY8S8EbaEWuWgPHyw9a2ltFqEw2TxX0PyKY9Se0p 44 | aPKuN6SLfrgn8ogltJG5U5GDPsyVuspgBEV259oo2YX0b/BptKtLKiGPzxuV1F4a 45 | DWvdpN1o1AxSTsbI8UfKRUg6lUHv72avgQKCAQEA0EjRERXE3WgmgIt04O93HgBf 46 | woprIJoL0tSkm/MgL7/jr8JQKdygcK/UZ+gj/1eAWZW963GB9L47MASAZJJ10hly 47 | Z3ke/LuZ3qHyvWL7nwpRyHuZxrRJCpf76tl71kG1pD4jbPHTfQMdVSZOTw3jjDHr 48 | 13KfE7aDzELuBQt+yhh/HcCtmkzQVcJPfsfolJN4lxhFiDiDBbnl/Zj+rdFl2GG7 49 | AORYVXVh10wOKRl+kF4XiuI7wGk2+126DIl3rRaqHeBG0pfd3qfDVMnl4aNpM/2Z 50 | 0wa2ea2yTGLohM1XEYcNQBM2QmArblOPQjHmk3FmJDcQTdEg6uzQDhUL3kRKAw== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /hitman-webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /hitman-webapp/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | -------------------------------------------------------------------------------- /hitman-webapp/index.ts: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const app = express(); 4 | 5 | app.use(express.static(path.join(__dirname, 'build'))); 6 | 7 | app.get('/*', function (req, res) { 8 | res.sendFile(path.join(__dirname, 'build', 'index.html')); 9 | }); 10 | 11 | app.listen(3000); -------------------------------------------------------------------------------- /hitman-webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hitman-webapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:3000", 6 | "homepage": ".", 7 | "dependencies": { 8 | "@emotion/core": "^11.0.0", 9 | "@emotion/react": "^11.8.1", 10 | "@emotion/styled": "^11.8.1", 11 | "@fontsource/roboto": "^4.5.3", 12 | "@mui/icons-material": "^5.4.2", 13 | "@mui/lab": "^5.0.0-alpha.70", 14 | "@mui/material": "^5.4.2", 15 | "@mui/styled-engine-sc": "^5.4.2", 16 | "@testing-library/jest-dom": "^5.16.2", 17 | "@testing-library/react": "^12.1.3", 18 | "@testing-library/user-event": "^13.5.0", 19 | "@types/jest": "^27.4.0", 20 | "@types/node": "^16.11.25", 21 | "@types/react": "^17.0.39", 22 | "@types/react-dom": "^17.0.11", 23 | "axios": "^0.26.0", 24 | "date-fns": "^2.28.0", 25 | "react": "^16.14.0", 26 | "react-dom": "^16.14.0", 27 | "react-router-dom": "^6.2.2", 28 | "react-scripts": "5.0.0", 29 | "styled-components": "^5.3.3", 30 | "typescript": "^4.5.5", 31 | "web-vitals": "^2.1.4", 32 | "express": "^4.17.3" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": [ 42 | "react-app", 43 | "react-app/jest" 44 | ] 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /hitman-webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-Doherty01/docker-celery/50e909aab1df6cb594a3b5c68104a83e56d77d62/hitman-webapp/public/favicon.ico -------------------------------------------------------------------------------- /hitman-webapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /hitman-webapp/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-Doherty01/docker-celery/50e909aab1df6cb594a3b5c68104a83e56d77d62/hitman-webapp/public/logo192.png -------------------------------------------------------------------------------- /hitman-webapp/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-Doherty01/docker-celery/50e909aab1df6cb594a3b5c68104a83e56d77d62/hitman-webapp/public/logo512.png -------------------------------------------------------------------------------- /hitman-webapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /hitman-webapp/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /hitman-webapp/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /hitman-webapp/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import App from "./App"; 4 | 5 | test("renders learn react link", () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /hitman-webapp/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import "@fontsource/roboto/300.css"; 3 | import "@fontsource/roboto/400.css"; 4 | import "@fontsource/roboto/500.css"; 5 | import "@fontsource/roboto/700.css"; 6 | import { 7 | CeleryTaskProgressBar, 8 | StartCeleryTaskButton, 9 | } from "./celery-task-progress-bar/progress-bar"; 10 | import React, { ReactElement, useMemo, useState } from "react"; 11 | import { 12 | Box, 13 | Button, 14 | Card, 15 | CardActions, 16 | CardContent, 17 | FormControl, 18 | InputLabel, 19 | MenuItem, 20 | OutlinedInput, 21 | Select, 22 | TextField, 23 | Theme, 24 | Typography, 25 | useTheme, 26 | } from "@mui/material"; 27 | import AdapterDateFns from "@mui/lab/AdapterDateFns"; 28 | import LocalizationProvider from "@mui/lab/LocalizationProvider"; 29 | import { TimePicker } from "@mui/lab"; 30 | import { useAuth } from "./authentication/auth-provider"; 31 | import { ApiService } from "./services/hitman"; 32 | import { HOST_URL, WEBSOCKET_URL } from "./authentication/auth"; 33 | 34 | interface CeleryTaskUpdate { 35 | progress: number; 36 | status: string; 37 | } 38 | 39 | interface ScheduleCeleryTaskProps { 40 | onSubmitCallback: (date: Date | null, dayOfWeek: string[]) => void; 41 | } 42 | 43 | const ITEM_HEIGHT = 48; 44 | const ITEM_PADDING_TOP = 8; 45 | const MenuProps = { 46 | PaperProps: { 47 | style: { 48 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, 49 | width: 250, 50 | }, 51 | }, 52 | }; 53 | 54 | function getStyles(name: string, personName: string[], theme: Theme) { 55 | return { 56 | fontWeight: 57 | personName.indexOf(name) === -1 58 | ? theme.typography.fontWeightRegular 59 | : theme.typography.fontWeightMedium, 60 | }; 61 | } 62 | 63 | function ScheduleCeleryTask({ 64 | onSubmitCallback, 65 | }: ScheduleCeleryTaskProps): ReactElement { 66 | const theme = useTheme(); 67 | const [dateTimeValue, setDateTimeValue] = useState(null); 68 | const [dayOfWeek, setDayOfWeek] = React.useState(["monday"]); 69 | 70 | const daysOfWeekArray: string[] = [ 71 | "monday", 72 | "tuesday", 73 | "wednesday", 74 | "thursday", 75 | "friday", 76 | "saturday", 77 | "sunday", 78 | ]; 79 | return ( 80 | 83 | 84 | 85 | 86 | Schedule Celery Task 87 | 88 | 89 | Setup weekly schedule. 90 | 91 | 92 | Day 93 | 118 | 119 | 120 | } 122 | ampm={false} 123 | label="Execution time" 124 | value={dateTimeValue} 125 | onChange={(newValue) => { 126 | setDateTimeValue(newValue); 127 | }} 128 | /> 129 | 130 | 131 | 132 | 140 | 141 | 142 | 143 | ); 144 | } 145 | 146 | function App() { 147 | const [progressValue, setProgressValue] = useState(0); 148 | const CreateCeleryTask = () => { 149 | fetch(HOST_URL + "hitmen/start-job", { 150 | method: "POST", 151 | headers: { 152 | Accept: "application/json", 153 | "Content-Type": "application/json", 154 | }, 155 | body: JSON.stringify({ 156 | target_name: "Keanu Reeves", 157 | }), 158 | }) 159 | .then((response) => { 160 | return response.json(); 161 | }) 162 | .then((data) => { 163 | const socket = new WebSocket( 164 | WEBSOCKET_URL + `task/progress/${data.celery_task_id}/` 165 | ); 166 | socket.onmessage = (event) => { 167 | const parsedEvent: CeleryTaskUpdate = JSON.parse(event.data); 168 | console.log(parsedEvent); 169 | setProgressValue(parsedEvent.progress * 100); 170 | }; 171 | 172 | socket.onerror = (err) => { 173 | console.log(err); 174 | }; 175 | socket.onclose = (event) => { 176 | console.log(event); 177 | }; 178 | socket.onopen = (event) => { 179 | console.log(event); 180 | }; 181 | }); 182 | }; 183 | 184 | const onSubmitScheduledTask = (newDate: Date | null, dayOfWeek: string[]) => { 185 | if (newDate !== null) { 186 | fetch(HOST_URL + "hitmen/schedule", { 187 | method: "POST", 188 | headers: { 189 | Accept: "application/json", 190 | "Content-Type": "application/json", 191 | }, 192 | body: JSON.stringify({ 193 | target_name: "Keanu Reeves", 194 | schedule_time: newDate, 195 | days_of_week: dayOfWeek, 196 | }), 197 | }) 198 | .then((response) => { 199 | return response.json(); 200 | }) 201 | .then((data) => { 202 | console.log(data); 203 | }); 204 | } 205 | }; 206 | 207 | const { getClient } = useAuth(); 208 | 209 | useMemo(() => { 210 | const service = new ApiService(getClient()); 211 | service.getHitmen().then((res) => { 212 | console.log(res.data); 213 | }); 214 | }, [getClient]); 215 | 216 | return ( 217 |
218 |
219 | 220 | 221 |
222 | 223 |
224 | ); 225 | } 226 | 227 | export default App; 228 | -------------------------------------------------------------------------------- /hitman-webapp/src/MainRouter.tsx: -------------------------------------------------------------------------------- 1 | import App from "./App"; 2 | import React, { ReactElement } from "react"; 3 | import { Routes, Route } from "react-router-dom"; 4 | import { AppNavbar } from "./navbar/navbar"; 5 | import { LoginScreen } from "./login/login"; 6 | import { RegisterScreen } from "./register/register"; 7 | import { AuthProvider } from "./authentication/auth-provider"; 8 | 9 | function MainRouter(): ReactElement { 10 | return ( 11 |
12 | 13 | 14 | 15 | } /> 16 | } /> 17 | } /> 18 | 19 | 20 |
21 | ); 22 | } 23 | 24 | export default MainRouter; 25 | -------------------------------------------------------------------------------- /hitman-webapp/src/authentication/auth-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { AuthState, initialAuthState, User } from "./auth-state"; 3 | import { AxiosInstance, AxiosResponse } from "axios"; 4 | 5 | export interface AuthContextInterface 6 | extends AuthState { 7 | createUser: ( 8 | username: string, 9 | password: string, 10 | firstName: string, 11 | lastName: string 12 | ) => void; 13 | login: (username: string, password: string) => Promise>; 14 | refreshToken: () => Promise>; 15 | getClient: () => AxiosInstance; 16 | } 17 | 18 | const stub = (): never => { 19 | throw new Error("You forgot to wrap your component in ."); 20 | }; 21 | 22 | const initialContext = { 23 | ...initialAuthState, 24 | createUser: stub, 25 | login: stub, 26 | refreshToken: stub, 27 | getClient: stub, 28 | }; 29 | 30 | const AuthContext = createContext(initialContext); 31 | 32 | export default AuthContext; 33 | -------------------------------------------------------------------------------- /hitman-webapp/src/authentication/auth-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext, useMemo, useReducer } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import AuthContext, { AuthContextInterface } from "./auth-context"; 4 | import { reducer } from "./reducer"; 5 | import { initialAuthState, User } from "./auth-state"; 6 | import axios, { AxiosInstance, AxiosResponse } from "axios"; 7 | import { 8 | TOKEN_LOCALSTORAGE_KEY, 9 | } from "./auth"; 10 | 11 | interface AuthProviderOptions { 12 | children?: React.ReactNode; 13 | } 14 | 15 | let refreshPromise: Promise> | null = null; 16 | 17 | export const AuthProvider = (opts: AuthProviderOptions): JSX.Element => { 18 | const { children } = opts; 19 | const HOST_URL = process.env.REACT_APP_API_HOST_URL || "http://localhost:8000/"; 20 | const CLIENT_ID = process.env.REACT_APP_CLIENT_ID || "FSb5TMUaS4GSdaFjj1M8kfwec2vxe7EH3eO8pdcx"; 21 | const CLIENT_SECRET = process.env.REACT_APP_CLIENT_SECRET || "0KobqEc4etm10YU3IeEdJ9k0Pv6GTuUl8UO15WnrlkkIgJ9MgVZqzCApFTWSaURudxlDj3gIUJcwkPXcORV1i1kcGrEXDXF2rxNMWtm4Hr6iuNDncp3c5g7P5BMY8mo7"; 22 | 23 | const [state, dispatch] = useReducer(reducer, initialAuthState); 24 | const navigate = useNavigate(); 25 | const refreshToken = useCallback((): Promise> => { 26 | const local = localStorage.getItem(TOKEN_LOCALSTORAGE_KEY); 27 | return new Promise>((resolve, reject) => { 28 | if (local === null) { 29 | reject("already refreshing or localstorage token not found"); 30 | return; 31 | } 32 | const token = JSON.parse(local); 33 | const url = HOST_URL + "o/token/"; 34 | const request = axios.post( 35 | url, 36 | new URLSearchParams({ 37 | refresh_token: token.refresh_token, 38 | grant_type: "refresh_token", 39 | client_id: CLIENT_ID, 40 | client_secret: CLIENT_SECRET, 41 | }), 42 | { 43 | headers: { 44 | "Content-Type": "application/x-www-form-urlencoded", 45 | }, 46 | } 47 | ); 48 | request 49 | .then((res) => { 50 | const token = JSON.stringify(res.data); 51 | localStorage.setItem(TOKEN_LOCALSTORAGE_KEY, token); 52 | resolve(res); 53 | }) 54 | .catch((err) => { 55 | navigate("/login", { replace: true }); 56 | reject(err); 57 | }); 58 | }); 59 | }, [CLIENT_ID, CLIENT_SECRET, HOST_URL, navigate]); 60 | 61 | const getClient = useCallback((): AxiosInstance => { 62 | const newInstance = axios.create(); 63 | newInstance.interceptors.request.use((config) => { 64 | const local = localStorage.getItem(TOKEN_LOCALSTORAGE_KEY); 65 | if (!config.url?.includes(HOST_URL)) config.url = HOST_URL + config.url; 66 | dispatch({ type: "LOADING", value: true }); 67 | if (local === null) return config; 68 | const token = JSON.parse(local).access_token; 69 | if (token && config.headers !== undefined) { 70 | config.headers.Authorization = `Bearer ${token}`; 71 | } 72 | return config; 73 | }); 74 | newInstance.interceptors.response.use( 75 | (response) => { 76 | dispatch({ type: "LOADING", value: false }); 77 | return response; 78 | }, 79 | async (error) => { 80 | dispatch({ type: "LOADING", value: false }); 81 | const originalRequest = error.config; 82 | if (error.response === undefined) return Promise.reject(error); 83 | if (error.response.status === 401 && !originalRequest._retry) { 84 | originalRequest._retry = true; 85 | if (refreshPromise === null) { 86 | const prom = refreshToken().then((res) => { 87 | refreshPromise = null; 88 | return res; 89 | }); 90 | refreshPromise = prom; 91 | await prom; 92 | } else { 93 | await refreshPromise; 94 | } 95 | 96 | return newInstance(originalRequest); 97 | } 98 | return Promise.reject(error); 99 | } 100 | ); 101 | return newInstance; 102 | }, [HOST_URL, refreshToken]); 103 | 104 | const login = useCallback( 105 | (username: string, password: string): Promise> => { 106 | const url = HOST_URL + "o/token/"; 107 | const request = axios.post( 108 | url, 109 | new URLSearchParams({ 110 | username: username, 111 | password: password, 112 | grant_type: "password", 113 | client_id: CLIENT_ID, 114 | client_secret: CLIENT_SECRET, 115 | }), 116 | { 117 | headers: { 118 | "Content-Type": "application/x-www-form-urlencoded", 119 | }, 120 | } 121 | ); 122 | return new Promise>((resolve, reject) => { 123 | request 124 | .then((res) => { 125 | const token = JSON.stringify(res.data); 126 | localStorage.setItem(TOKEN_LOCALSTORAGE_KEY, token); 127 | navigate("/", { replace: true }); 128 | resolve(res); 129 | }) 130 | .catch((err) => { 131 | reject(err); 132 | }); 133 | }); 134 | }, 135 | [CLIENT_ID, CLIENT_SECRET, HOST_URL, navigate] 136 | ); 137 | 138 | const createUser = useCallback( 139 | async ( 140 | username: string, 141 | password: string, 142 | firstName: string, 143 | lastName: string 144 | ) => { 145 | const url = HOST_URL + "user/create"; 146 | const request = axios.post( 147 | url, 148 | { 149 | username: username, 150 | password: password, 151 | first_name: firstName, 152 | last_name: lastName, 153 | }, 154 | { 155 | headers: { 156 | "Content-Type": "application/json", 157 | }, 158 | } 159 | ); 160 | await request; 161 | await login(username, password); 162 | }, 163 | [HOST_URL, login] 164 | ); 165 | 166 | const contextValue = useMemo(() => { 167 | return { 168 | ...state, 169 | refreshToken, 170 | login, 171 | createUser, 172 | getClient, 173 | }; 174 | }, [state, refreshToken, login, createUser, getClient]); 175 | 176 | return ( 177 | {children} 178 | ); 179 | }; 180 | 181 | export const useAuth = < 182 | TUser extends User = User 183 | >(): AuthContextInterface => 184 | useContext(AuthContext) as AuthContextInterface; 185 | -------------------------------------------------------------------------------- /hitman-webapp/src/authentication/auth-state.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | email: string; 4 | first_name: string; 5 | last_name: string; 6 | } 7 | 8 | export interface AuthState { 9 | error?: Error; 10 | isAuthenticated: boolean; 11 | isLoading: boolean; 12 | user?: TUser; 13 | } 14 | 15 | export const initialAuthState: AuthState = { 16 | isAuthenticated: false, 17 | isLoading: false, 18 | }; 19 | -------------------------------------------------------------------------------- /hitman-webapp/src/authentication/auth.tsx: -------------------------------------------------------------------------------- 1 | export const HOST_URL = 2 | process.env.REACT_APP_API_HOST_URL || "http://localhost:8000/"; 3 | export const WEBSOCKET_URL = 4 | process.env.REACT_APP_WEBSOCKET_URL || "ws://localhost:8000/"; 5 | export const TOKEN_LOCALSTORAGE_KEY = "TOKEN"; 6 | -------------------------------------------------------------------------------- /hitman-webapp/src/authentication/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AuthState } from "./auth-state"; 2 | 3 | type Action = { 4 | type: "LOADING"; 5 | value: boolean; 6 | }; 7 | 8 | export const reducer = (state: AuthState, action: Action): AuthState => { 9 | switch (action.type) { 10 | case "LOADING": 11 | return { 12 | ...state, 13 | isLoading: action.value, 14 | }; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /hitman-webapp/src/celery-task-progress-bar/progress-bar.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import LinearProgress from "@mui/material/LinearProgress"; 3 | import { Box, Typography, Button } from "@mui/material"; 4 | 5 | interface CeleryTaskProgressBarProps { 6 | progressValue: number; 7 | } 8 | 9 | interface StartCeleryTaskButtonProps { 10 | onClickCallback: () => void; 11 | } 12 | 13 | export function StartCeleryTaskButton({ 14 | onClickCallback, 15 | }: StartCeleryTaskButtonProps): ReactElement { 16 | return ( 17 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export function CeleryTaskProgressBar({ 26 | progressValue, 27 | }: CeleryTaskProgressBarProps): ReactElement { 28 | return ( 29 | 32 | 33 | 34 | 35 | 36 | {`${Math.round( 37 | progressValue 38 | )}%`} 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /hitman-webapp/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /hitman-webapp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import MainRouter from "./MainRouter"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /hitman-webapp/src/login/login.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import * as React from "react"; 3 | import Avatar from "@mui/material/Avatar"; 4 | import Button from "@mui/material/Button"; 5 | import CssBaseline from "@mui/material/CssBaseline"; 6 | import TextField from "@mui/material/TextField"; 7 | import Link from "@mui/material/Link"; 8 | import Grid from "@mui/material/Grid"; 9 | import Box from "@mui/material/Box"; 10 | import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; 11 | import Typography from "@mui/material/Typography"; 12 | import Container from "@mui/material/Container"; 13 | import { createTheme, ThemeProvider } from "@mui/material/styles"; 14 | import { useAuth } from "../authentication/auth-provider"; 15 | 16 | export function LoginScreen(): ReactElement { 17 | const theme = createTheme(); 18 | const { login } = useAuth(); 19 | 20 | const handleSubmit = (event: { 21 | preventDefault: () => void; 22 | currentTarget: HTMLFormElement | undefined; 23 | }) => { 24 | event.preventDefault(); 25 | const data = new FormData(event.currentTarget); 26 | const formData = { 27 | email: data.get("email"), 28 | password: data.get("password"), 29 | }; 30 | if (formData.email !== null && formData.password !== null) { 31 | login(formData.email.toString(), formData.password.toString()); 32 | } 33 | }; 34 | 35 | return ( 36 | 37 | 38 | 39 | 47 | 48 | 49 | 50 | 51 | Sign in 52 | 53 | 59 | 69 | 79 | 87 | 88 | 89 | 90 | {"Don't have an account? Sign Up"} 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /hitman-webapp/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hitman-webapp/src/navbar/navbar.tsx: -------------------------------------------------------------------------------- 1 | import Box from "@mui/material/Box"; 2 | import AppBar from "@mui/material/AppBar"; 3 | import Toolbar from "@mui/material/Toolbar"; 4 | import IconButton from "@mui/material/IconButton"; 5 | import MenuIcon from "@mui/icons-material/Menu"; 6 | import Typography from "@mui/material/Typography"; 7 | import { ReactElement } from "react"; 8 | 9 | interface AppNavbarProps { 10 | app_name: string; 11 | } 12 | 13 | export function AppNavbar({ app_name }: AppNavbarProps): ReactElement { 14 | return ( 15 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | {app_name} 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /hitman-webapp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /hitman-webapp/src/register/register.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import * as React from "react"; 3 | import Avatar from "@mui/material/Avatar"; 4 | import Button from "@mui/material/Button"; 5 | import CssBaseline from "@mui/material/CssBaseline"; 6 | import TextField from "@mui/material/TextField"; 7 | import Link from "@mui/material/Link"; 8 | import Grid from "@mui/material/Grid"; 9 | import Box from "@mui/material/Box"; 10 | import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; 11 | import Typography from "@mui/material/Typography"; 12 | import Container from "@mui/material/Container"; 13 | import { createTheme, ThemeProvider } from "@mui/material/styles"; 14 | import { useAuth } from "../authentication/auth-provider"; 15 | 16 | export function RegisterScreen(): ReactElement { 17 | const theme = createTheme(); 18 | const { createUser } = useAuth(); 19 | 20 | const handleSubmit = (event: { 21 | preventDefault: () => void; 22 | currentTarget: HTMLFormElement | undefined; 23 | }) => { 24 | event.preventDefault(); 25 | const data = new FormData(event.currentTarget); 26 | const formData = { 27 | firstName: data.get("firstName"), 28 | lastName: data.get("lastName"), 29 | email: data.get("email"), 30 | password: data.get("password"), 31 | }; 32 | 33 | if ( 34 | formData.firstName !== null && 35 | formData.lastName !== null && 36 | formData.email !== null && 37 | formData.password !== null 38 | ) { 39 | createUser( 40 | formData.email.toString(), 41 | formData.password.toString(), 42 | formData.firstName.toString(), 43 | formData.lastName.toString() 44 | ); 45 | } 46 | }; 47 | 48 | return ( 49 | 50 | 51 | 52 | 60 | 61 | 62 | 63 | 64 | Sign up 65 | 66 | 72 | 73 | 74 | 83 | 84 | 85 | 93 | 94 | 95 | 103 | 104 | 105 | 114 | 115 | 116 | 124 | 125 | 126 | 127 | Already have an account? Sign in 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /hitman-webapp/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /hitman-webapp/src/services/hitman.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance, AxiosResponse } from "axios"; 2 | 3 | export class ApiService { 4 | axiosAuthInstance: AxiosInstance; 5 | 6 | constructor(axiosAuthInstance: AxiosInstance) { 7 | this.axiosAuthInstance = axiosAuthInstance; 8 | } 9 | 10 | startNewHitJob(targetName: string): Promise> { 11 | const url = "hitmen/start-job"; 12 | return this.axiosAuthInstance.post( 13 | url, 14 | { 15 | target_name: targetName, 16 | }, 17 | { 18 | headers: { 19 | "Content-Type": "application/json", 20 | }, 21 | } 22 | ); 23 | } 24 | 25 | getHitmen(): Promise> { 26 | const url = "hitmen/all"; 27 | return this.axiosAuthInstance.get(url, { 28 | headers: { 29 | "Content-Type": "application/json", 30 | }, 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /hitman-webapp/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /hitman-webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2==2.9.3 2 | django==3.2.11 3 | djangorestframework==3.13.1 4 | markdown==3.3.6 5 | django-filter==21.1 6 | celery==5.2.3 7 | django_celery_beat==2.2.1 8 | channels==3.0.4 9 | redis==4.1.1 10 | flower==1.0.0 11 | pyyaml==6.0 12 | watchdog==2.1.6 13 | django-cors-headers==3.11.0 14 | channels-redis==3.3.1 15 | django-oauth-toolkit==1.7.0 16 | SQLAlchemy==1.4.35 --------------------------------------------------------------------------------