diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..23ec718 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +# ENV files +**/.env + +# Logs +**/*.log + +# Cache files +**/__pycache__ +**/*.py[cod] +**/*$py.class \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..757defc --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,53 @@ +name: Docker + +on: + push: + branches: [main] + tags: ["v*"] + +jobs: + Build-and-Push: + runs-on: ubuntu-latest + + # We want to filter out dependabot and pre-commit + # automated pushes to main + if: ${{ github.actor != 'dependabot[bot]'}} && ${{ github.actor != 'pre-commit-ci[bot]'}} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare Docker Meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository_owner }}/kanae + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=edge,branch=main + + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + with: + version: latest + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kanae-build-cache:server + cache-to: type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/kanae-build-cache:server + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/config-example.yml b/config-example.yml new file mode 100644 index 0000000..5e38efb --- /dev/null +++ b/config-example.yml @@ -0,0 +1,37 @@ +# --------------------------------- # +# Kanae's Configuration file # +# --------------------------------- # +# This holds the configuration for Kanae. This file is not settable during runtime. +# If you wish to change the values, change and save, and restart your server + +# Entries pertaining to Kanae are located here +kanae: + + # Host that the server will use. + # Set to 0.0.0.0 if running in Docker + host: 127.0.0.1 + + # Port that the server binds to. + # Defaults to 8000 + port: 8000 + + + # Prometheus exporter for Kanae. The following keys are used in order to control + # the behavior of the Prometheus exporter + prometheus: + + # Whether the Prometheus exporter is enabled or not + enabled: False + + # The host that the Prometheus exporter will bind to. By default, + # it will always be set to 127.0.0.1 + host: "127.0.0.1" + + # The port used for the Prometheus exporter. By default, + # it will always be set to 9555 + port: 9555 + +# The PostgreSQL connection URI that is used to connect to the database +# The URI must be valid, and components will need to be quoted. +# See https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING +postgres_uri: "postgresql://user:password@localhost:5432/user" \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..7bc92b4 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,39 @@ +FROM python:3.12-slim-bookworm + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + git \ + netcat-traditional \ + tini \ + && apt-get clean + +WORKDIR /kanae +COPY /server /kanae/server/ +COPY /docker/start.sh /kanae/start.sh +COPY /docker/wait-for /kanae/wait-for +COPY /requirements.txt /kanae/requirements.txt + +RUN adduser --disabled-password --gecos "" kanae \ + && chown -R kanae:kanae /kanae \ + && chmod +x /kanae/start.sh \ + && chmod +x /kanae/wait-for + +USER kanae + +ENV PATH="${PATH}:${HOME}/.local/bin" + +RUN pip install --user -r requirements.txt + +ENTRYPOINT ["/usr/bin/tini", "--"] + +CMD ["/kanae/start.sh"] + +STOPSIGNAL SIGTERM + +LABEL org.opencontainers.image.title="Kanae" +LABEL org.opencontainers.image.description="Internal backend server for ACM @ UC Merced" +LABEL org.opencontainers.image.licenses="Apache-2.0" +LABEL org.opencontainers.image.source="https://github.com/UCMercedACM/kanae" \ No newline at end of file diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 0000000..be1b3d9 --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,18 @@ +name: kanae_dev + +# For development purposes, it is recommended just to launch the server and use it's HMR feature instead +services: + database: + container_name: kanae_postgres + image: postgres:16 + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_DATABASE_NAME} + POSTGRES_USER: ${DB_USERNAME} + volumes: + - database:/var/lib/postgresql/data + ports: + - 5432:5432 + +volumes: + database: \ No newline at end of file diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml new file mode 100644 index 0000000..037b77a --- /dev/null +++ b/docker/docker-compose.prod.yml @@ -0,0 +1,58 @@ +name: kanae + +services: + kanae: + container_name: kanae + image: ghcr.io/UCMercedACM/kanae:edge + volumes: + # Do not edit the next line. If you want to change the path of the configuration file, please edit the CONFIG_LOCATION variable + - ${CONFIG_LOCATION}:/kanae/server/config.yml + ports: + - 9619:9619 + depends_on: + - database + # Safety script to fully wait until PostgreSQL is up + command: sh -c '/kanae/wait-for database:5432 -- echo "[Wait-for] PostgreSQL is fully up. Starting Kanae." && /kanae/start.sh' + restart: always + + database: + container_name: kanae_postgres + image: postgres:16 + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_DATABASE_NAME} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_INITDB_ARGS: '--data-checksums' + ports: + - 5432:5432 + volumes: + - database:/var/lib/postgresql/data + healthcheck: + test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1 + interval: 5m + start_interval: 30s + start_period: 5m + restart: always + + kanae-prometheus: + container_name: kanae_prometheus + ports: + - 9090:9090 + image: prom/prometheus:latest + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus-data:/prometheus + + # first login uses admin/admin + # add data source for http://kanae-prometheus:9090 to get started + kanae-grafana: + container_name: kanae_grafana + command: ['./run.sh', '-disable-reporting'] + ports: + - 3000:3000 + image: grafana/grafana-enterprise:latest-ubuntu + volumes: + - grafana-data:/var/lib/grafana + +volumes: + database: \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..295c28f --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,38 @@ +name: kanae + +services: + kanae: + container_name: kanae + image: ghcr.io/UCMercedACM/kanae:edge + volumes: + # Do not edit the next line. If you want to change the path of the configuration file, please edit the CONFIG_LOCATION variable + - ${CONFIG_LOCATION}:/kanae/server/config.yml + ports: + - 9619:9619 + depends_on: + - database + # Safety script to fully wait until PostgreSQL is up + command: sh -c '/kanae/wait-for database:5432 -- echo "[Wait-for] PostgreSQL is fully up. Starting Kanae." && /kanae/start.sh' + restart: always + + database: + container_name: kanae_postgres + image: postgres:16 + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_DATABASE_NAME} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_INITDB_ARGS: '--data-checksums' + ports: + - 5432:5432 + volumes: + - database:/var/lib/postgresql/data + healthcheck: + test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1 + interval: 5m + start_interval: 30s + start_period: 5m + restart: always + +volumes: + database: \ No newline at end of file diff --git a/docker/example.env b/docker/example.env new file mode 100644 index 0000000..d220dd2 --- /dev/null +++ b/docker/example.env @@ -0,0 +1,11 @@ +# The location of where Rodhaj's configuration is stored. +# The configuration can be found under the config-example.yml +CONFIG_LOCATION=./config.yml + +# Connection secret for the postgres user. You should change it to a random password +DB_PASSWORD=password + +# The values below this line do not need to be changed +################################################################################### +DB_USERNAME=postgres +DB_DATABASE_NAME=kanae \ No newline at end of file diff --git a/docker/prometheus.yml b/docker/prometheus.yml new file mode 100644 index 0000000..0064987 --- /dev/null +++ b/docker/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: kanae + static_configs: + - targets: ['kanae:9555'] \ No newline at end of file diff --git a/docker/start.sh b/docker/start.sh new file mode 100644 index 0000000..14b1027 --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +KANAE_FIRST_START_CHECK="KANAE_FIRST_START" + +if [ ! -f $KANAE_FIRST_START_CHECK ]; then + touch $KANAE_FIRST_START_CHECK + echo "DO NOT EDIT THIS FILE! THIS IS USED WHEN YOU FIRST RUN KANAE USING DOCKER!" >> $KANAE_FIRST_START_CHECK + # python3 /kanae/server/migrations.py init +fi + +exec python3 /kanae/server/launcher.py --no-workers \ No newline at end of file diff --git a/docker/wait-for b/docker/wait-for new file mode 100644 index 0000000..eae37dd --- /dev/null +++ b/docker/wait-for @@ -0,0 +1,191 @@ +#!/bin/sh + +# The MIT License (MIT) +# +# Copyright (c) 2017 Eficode Oy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +VERSION="2.2.3" + +set -- "$@" -- "$TIMEOUT" "$QUIET" "$PROTOCOL" "$HOST" "$PORT" "$result" +TIMEOUT=15 +QUIET=0 +# The protocol to make the request with, either "tcp" or "http" +PROTOCOL="tcp" + +echoerr() { + if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi +} + +usage() { + exitcode="$1" + cat << USAGE >&2 +Usage: + $0 host:port|url [-t timeout] [-- command args] + -q | --quiet Do not output any status messages + -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout + -v | --version Show the version of this tool + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit "$exitcode" +} + +wait_for() { + case "$PROTOCOL" in + tcp) + if ! command -v nc >/dev/null; then + echoerr 'nc command is missing!' + exit 1 + fi + ;; + http) + if ! command -v wget >/dev/null; then + echoerr 'wget command is missing!' + exit 1 + fi + ;; + esac + + TIMEOUT_END=$(($(date +%s) + TIMEOUT)) + + while :; do + case "$PROTOCOL" in + tcp) + nc -w 1 -z "$HOST" "$PORT" > /dev/null 2>&1 + ;; + http) + wget --timeout=1 -q "$HOST" -O /dev/null > /dev/null 2>&1 + ;; + *) + echoerr "Unknown protocol '$PROTOCOL'" + exit 1 + ;; + esac + + result=$? + + if [ $result -eq 0 ] ; then + if [ $# -gt 7 ] ; then + for result in $(seq $(($# - 7))); do + result=$1 + shift + set -- "$@" "$result" + done + + TIMEOUT=$2 QUIET=$3 PROTOCOL=$4 HOST=$5 PORT=$6 result=$7 + shift 7 + exec "$@" + fi + exit 0 + fi + + if [ $TIMEOUT -ne 0 -a $(date +%s) -ge $TIMEOUT_END ]; then + echo "Operation timed out" >&2 + exit 1 + fi + + sleep 1 + done +} + +while :; do + case "$1" in + http://*|https://*) + HOST="$1" + PROTOCOL="http" + shift 1 + ;; + *:* ) + HOST=$(printf "%s\n" "$1"| cut -d : -f 1) + PORT=$(printf "%s\n" "$1"| cut -d : -f 2) + shift 1 + ;; + -v | --version) + echo $VERSION + exit + ;; + -q | --quiet) + QUIET=1 + shift 1 + ;; + -q-*) + QUIET=0 + echoerr "Unknown option: $1" + usage 1 + ;; + -q*) + QUIET=1 + result=$1 + shift 1 + set -- -"${result#-q}" "$@" + ;; + -t | --timeout) + TIMEOUT="$2" + shift 2 + ;; + -t*) + TIMEOUT="${1#-t}" + shift 1 + ;; + --timeout=*) + TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + break + ;; + --help) + usage 0 + ;; + -*) + QUIET=0 + echoerr "Unknown option: $1" + usage 1 + ;; + *) + QUIET=0 + echoerr "Unknown argument: $1" + usage 1 + ;; + esac +done + +if ! [ "$TIMEOUT" -ge 0 ] 2>/dev/null; then + echoerr "Error: invalid timeout '$TIMEOUT'" + usage 3 +fi + +case "$PROTOCOL" in + tcp) + if [ "$HOST" = "" ] || [ "$PORT" = "" ]; then + echoerr "Error: you need to provide a host and port to test." + usage 2 + fi + ;; + http) + if [ "$HOST" = "" ]; then + echoerr "Error: you need to provide a host to test." + usage 2 + fi + ;; +esac + +wait_for "$@" diff --git a/server/launcher.py b/server/launcher.py index 8793fb6..43a6b72 100644 --- a/server/launcher.py +++ b/server/launcher.py @@ -20,13 +20,13 @@ parser.add_argument( "-H", "--host", - default=config["server"]["host"], + default=config["kanae"]["host"], help="The host to bind to. Defaults to value set in config", ) parser.add_argument( "-p", "--port", - default=config["server"]["port"], + default=config["kanae"]["port"], help="The port to bind to. Defaults to value set in config", type=int, )