From fc94212d97e7a1d3a6e80ca1d7092f3334f331cb Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Mon, 14 Nov 2022 19:41:46 +0100 Subject: [PATCH] api-server: Initial PoC --- .editorconfig | 3 +- .gitignore | 53 ++++++++++++++++++- api/Dockerfile | 29 ++++++++++ api/api.py | 53 +++++++++++++++++++ .../00-check-project-mounted.sh | 6 +++ .../docker-entrypoint.d/01-proxy-ssh-agent.sh | 16 ++++++ .../10-install-galaxy-requirements.sh | 6 +++ .../20-create-ansible-password-file.sh | 7 +++ api/bin/docker-entrypoint.sh | 46 ++++++++++++++++ api/requirements.txt | 4 ++ docker-compose.yaml | 15 ++++++ ping.yaml | 4 ++ requirements.txt | 1 - 13 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 api/Dockerfile create mode 100644 api/api.py create mode 100755 api/bin/docker-entrypoint.d/00-check-project-mounted.sh create mode 100755 api/bin/docker-entrypoint.d/01-proxy-ssh-agent.sh create mode 100755 api/bin/docker-entrypoint.d/10-install-galaxy-requirements.sh create mode 100755 api/bin/docker-entrypoint.d/20-create-ansible-password-file.sh create mode 100755 api/bin/docker-entrypoint.sh create mode 100644 api/requirements.txt create mode 100644 docker-compose.yaml create mode 100644 ping.yaml delete mode 100644 requirements.txt diff --git a/.editorconfig b/.editorconfig index 03d2cbb..fb7908c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,7 @@ end_of_line = lf insert_final_newline = true indent_style = space charset = utf-8 +indent_size = 4 [*.{yaml,yml}] -indent_size = 2 \ No newline at end of file +indent_size = 2 diff --git a/.gitignore b/.gitignore index fa075d1..01254cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,52 @@ -/.vagrant/ \ No newline at end of file +/.vagrant/ +/docker-compose.override.yaml + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Environments +.virtualenv/ diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..33a2d75 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,29 @@ +FROM python:3.11-alpine + +RUN apk --no-cache add ansible openssh-client tini su-exec socat + +RUN adduser \ + --disabled-password \ + --gecos "" \ + api-server + +USER api-server +WORKDIR /opt/api-server + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . + +ENV API_PROJECT_DIR=/var/project \ + API_GALAXY_REQUIREMENTS=/var/project/galaxy-requirements.yml \ + API_PIP_REQUIREMENTS=/var/project/requirements.txt \ + API_RUNAS=api-server \ + PATH="/home/api-server/.local/bin:${PATH}" + +VOLUME [ "${API_PROJECT_DIR}" ] +WORKDIR ${API_PROJECT_DIR} + +# switch to root as it must be available +USER root +ENTRYPOINT [ "tini", "--", "/opt/api-server/bin/docker-entrypoint.sh" ] diff --git a/api/api.py b/api/api.py new file mode 100644 index 0000000..41738b8 --- /dev/null +++ b/api/api.py @@ -0,0 +1,53 @@ +import asyncio +from os import system +from time import sleep +from typing import Any, Dict +from fastapi import FastAPI +from fastapi.responses import StreamingResponse +from pydantic import BaseModel +import ansible_runner + +class DeployArgs(BaseModel): + extra_vars: Dict[str, Any] + +app = FastAPI() + +@app.post("/deploy/{service}") +async def deploy(service: str, args: DeployArgs): + finished = False + lines = [] + + def finish_callback(_): + nonlocal finished + finished = True + + def event_callback(data: Dict): + if 'stdout' in data: + lines.append(data['stdout']) + + async def logs(): + nonlocal lines + while not finished: + for line in lines: + yield line.rstrip() + "\n" + lines = [] + await asyncio.sleep(0.1) + + ansible_runner.run_async( + playbook='deploy.yaml', + extravars={ + 'services': [service], + **args.extra_vars + }, + private_data_dir='/home/api-server', + project_dir='/var/project', + inventory='inventory/m2.ini', + event_handler=event_callback, + finished_callback=finish_callback, + settings={ + 'suppress_ansible_output': True + } + ) + + return StreamingResponse(logs(), media_type='text/plain') + diff --git a/api/bin/docker-entrypoint.d/00-check-project-mounted.sh b/api/bin/docker-entrypoint.d/00-check-project-mounted.sh new file mode 100755 index 0000000..4ca6e7e --- /dev/null +++ b/api/bin/docker-entrypoint.d/00-check-project-mounted.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +if [ -z "$(ls -A ${API_PROJECT_DIR})" ]; then + echo "No files found in project dir, maybe you forgot to mount it?" >&2 + exit 1 +fi diff --git a/api/bin/docker-entrypoint.d/01-proxy-ssh-agent.sh b/api/bin/docker-entrypoint.d/01-proxy-ssh-agent.sh new file mode 100755 index 0000000..3e37130 --- /dev/null +++ b/api/bin/docker-entrypoint.d/01-proxy-ssh-agent.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +PROXIED_AUTH_SOCK="${PROXIED_AUTH_SOCK:-/var/run/proxied-ssh-auth.sock}" + +if [ -S "${PROXIED_AUTH_SOCK}" ]; then + echo "Found previously not closed ssh auth socket, closing." + rm ${PROXIED_AUTH_SOCK} +fi + +# This should be run only when run as is specified and SSH Agent socket is present +if [ -n "$API_RUNAS" ] && [ -S "$SSH_AUTH_SOCK" ]; then + echo "Proxying ${SSH_AUTH_SOCK} -> ${PROXIED_AUTH_SOCK} for ${API_RUNAS%%:*}" + socat UNIX-LISTEN:${PROXIED_AUTH_SOCK},fork,user=${API_RUNAS%%:*},mode=600 \ + UNIX-CONNECT:${SSH_AUTH_SOCK} & + export SSH_AUTH_SOCK=/var/run/proxied-ssh-auth.sock +fi diff --git a/api/bin/docker-entrypoint.d/10-install-galaxy-requirements.sh b/api/bin/docker-entrypoint.d/10-install-galaxy-requirements.sh new file mode 100755 index 0000000..d63072f --- /dev/null +++ b/api/bin/docker-entrypoint.d/10-install-galaxy-requirements.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +if [ -f "${API_GALAXY_REQUIREMENTS}" ]; then + echo "Installing galaxy stuff from ${API_GALAXY_REQUIREMENTS}" + run-user ansible-galaxy install -r ${API_GALAXY_REQUIREMENTS} +fi diff --git a/api/bin/docker-entrypoint.d/20-create-ansible-password-file.sh b/api/bin/docker-entrypoint.d/20-create-ansible-password-file.sh new file mode 100755 index 0000000..b964adc --- /dev/null +++ b/api/bin/docker-entrypoint.d/20-create-ansible-password-file.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +if [ -n "$ANSIBLE_VAULT_PASSWORD" ]; then + export ANSIBLE_VAULT_PASSWORD_FILE=/var/run/secrets/vault-password + echo "$ANSIBLE_VAULT_PASSWORD" > $ANSIBLE_VAULT_PASSWORD_FILE + unset ANSIBLE_VAULT_PASSWORD +fi diff --git a/api/bin/docker-entrypoint.sh b/api/bin/docker-entrypoint.sh new file mode 100755 index 0000000..fa53ae3 --- /dev/null +++ b/api/bin/docker-entrypoint.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +# fail on first error +set -e + +function runuser { + EXEC=0 + + while true; do + case "$1" in + -E|--exec) + EXEC=1 + shift + ;; + --) + shift + break + ;; + *) + break + ;; + esac + done + + if [ -n "$API_RUNAS" ]; then + set -- su-exec "$API_RUNAS" "$@" + fi + + if [ $EXEC -eq 1 ]; then + exec "$@" + else + "$@" + fi +} + +alias run-user=runuser + +for part in $(dirname $0)/docker-entrypoint.d/*.sh; do + [ -x $part ] && source $part +done + +if [ "${1#-}" != "$1" ] || [ $# -eq 0 ]; then + set -- uvicorn --app-dir /opt/api-server api:app --host ${API_HOST:-0.0.0.0} --port ${API_PORT:-8080} "$@" +fi + +run-user --exec -- "$@" diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..84c67c7 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,4 @@ +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +ansible-runner>=2.3.0,<2.4.0 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..2ccf353 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + api: + build: api + image: registry.kadet.net/mgmt/api:${API_VERSION:-latest} + environment: + - SSH_AUTH_SOCK + ports: + - "8080:8080" + command: ['--reload', '--reload-dir', '/opt/api-server'] + volumes: + - .:/var/project + - ./api:/opt/api-server + - ${SSH_AUTH_SOCK}:${SSH_AUTH_SOCK} diff --git a/ping.yaml b/ping.yaml new file mode 100644 index 0000000..510e388 --- /dev/null +++ b/ping.yaml @@ -0,0 +1,4 @@ +- hosts: + - all + tasks: + - action: ping diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index bdb9670..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -docker