Initial Commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.venv
|
||||
store
|
||||
data
|
||||
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM docker
|
||||
WORKDIR /
|
||||
COPY composer /composer
|
||||
COPY stack /stack
|
||||
RUN apk update
|
||||
RUN apk add --no-cache git python3 py3-pip
|
||||
RUN pip3 install -r composer/requirements.txt
|
||||
ENTRYPOINT ['python3', 'composer/entrypoint.py']
|
||||
BIN
composer/__pycache__/agent.cpython-312.pyc
Normal file
BIN
composer/__pycache__/agent.cpython-312.pyc
Normal file
Binary file not shown.
174
composer/agent.py
Normal file
174
composer/agent.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from git import Repo
|
||||
import logging
|
||||
from os import getenv
|
||||
from pathlib import Path
|
||||
from python_on_whales import DockerClient
|
||||
from python_on_whales.exceptions import DockerException
|
||||
import schedule
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger("agent-logger")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||
|
||||
class Setup:
|
||||
|
||||
@staticmethod
|
||||
def create_symlink():
|
||||
host_data_env = getenv("HOST_DATA_PATH", None)
|
||||
if host_data_env is None:
|
||||
logger.info(f"HOST_DATA_PATH is not set, symlink not created")
|
||||
return None
|
||||
try:
|
||||
host_data_path = Path(host_data_env)
|
||||
container_data_path = Path("store/data")
|
||||
if not host_data_path.is_symlink():
|
||||
host_data_path.symlink_to(container_data_path)
|
||||
logger.info(f"Created symlink: {host_data_path} -> {container_data_path}")
|
||||
else:
|
||||
logger.info(f"Symlink already exists: {host_data_path} -> {container_data_path}")
|
||||
except:
|
||||
logger.error(f"Failed to create symlink from {host_data_env}")
|
||||
|
||||
class Composer:
|
||||
|
||||
services = []
|
||||
|
||||
def __init__(self, remote_url):
|
||||
if (not Path("store/stack/compose.yml").exists()) or (not Path("store/stack/.git").exists()):
|
||||
self.repo = Repo.init("store/stack")
|
||||
self._add_remote(remote_url)
|
||||
else:
|
||||
self.repo = Repo("store/stack")
|
||||
self.repo.remotes.origin.pull('master')
|
||||
self.docker = DockerClient(
|
||||
compose_files=["store/stack/compose.yml"],
|
||||
compose_env_files=["store/stack/.env"]
|
||||
)
|
||||
if self.docker.compose.config(return_json=True)["name"] != "stack":
|
||||
logger.warn(f"Composer stack name is wrong, either make sure it is set manually or keep it store/stack")
|
||||
self.start_update_job()
|
||||
self._create_services()
|
||||
self.set_status("up")
|
||||
logger.info("Composer started and services created")
|
||||
|
||||
def _add_remote(self, url):
|
||||
self.repo.create_remote('origin', url)
|
||||
|
||||
def _create_services(self):
|
||||
for service in self.services:
|
||||
self.services[service].close()
|
||||
services_dict = self.docker.compose.config().services
|
||||
self.services = {service: Service(service, services_dict[service].labels, self) for service in services_dict}
|
||||
|
||||
def start_update_job(self):
|
||||
update_time = getenv("UPDATE", 1)
|
||||
try:
|
||||
self.update_time = int(update_time)
|
||||
assert self.update_time > 0
|
||||
except (AssertionError, ValueError):
|
||||
self.update_time = 1
|
||||
logging.warn(f"Composer has an invalid UPDATE value of {getenv("UPDATE")}, defaulting to update interval of {self.update_time} minutes")
|
||||
schedule.every(self.update_time).minutes.do(self.update)
|
||||
|
||||
def set_status(self, status):
|
||||
try:
|
||||
if status == "up":
|
||||
self.docker.compose.up(
|
||||
build=True,
|
||||
detach=True,
|
||||
quiet=True,
|
||||
remove_orphans=True
|
||||
)
|
||||
elif status == "down":
|
||||
self.docker.compose.down(
|
||||
services=[self.name]
|
||||
)
|
||||
elif status == "restart":
|
||||
self.docker.compose.restart(
|
||||
services=[self.name]
|
||||
)
|
||||
else:
|
||||
logger.warn(f"Undefined status {status} - use up, down, or restart")
|
||||
except DockerException as error:
|
||||
logger.critical(f"Setting status of composer failed: {error}")
|
||||
|
||||
def update(self):
|
||||
current = self.repo.head.commit
|
||||
self.repo.remotes.origin.pull('master')
|
||||
if current != self.repo.head.commit:
|
||||
self._create_services()
|
||||
self.set_status("up")
|
||||
logger.info(f"New compose stack deployed! Commit: {self.repo.head.commit}")
|
||||
|
||||
class Service:
|
||||
|
||||
update_task = None
|
||||
|
||||
def __init__(self, name, labels, parent):
|
||||
self.name = name
|
||||
self.labels = labels
|
||||
self.parent = parent
|
||||
self.update_task = self.start_update_job()
|
||||
|
||||
def close(self):
|
||||
if self.update_time is None:
|
||||
return None
|
||||
schedule.cancel_job(self.update_task)
|
||||
|
||||
def get_status(self):
|
||||
return "up" if bool(self.parent.docker.compose.ps(services=[self.name])) else "down"
|
||||
|
||||
def start_update_job(self):
|
||||
self.update_time = self.parent.update_time
|
||||
if 'composer.update' in self.labels:
|
||||
try:
|
||||
self.update_time = int(self.labels['composer.update'])
|
||||
assert self.update_time > 0
|
||||
except (AssertionError, ValueError):
|
||||
self.update_time = self.parent.update_time
|
||||
logging.warn(f"Service {self.name} has an invalid composer.update value of {self.labels['composer.update']}, defaulting to update interval of {self.update_time} minutes")
|
||||
if self.update_time != 0:
|
||||
return schedule.every(self.update_time).minutes.do(self.update)
|
||||
return None
|
||||
|
||||
def set_status(self, status):
|
||||
try:
|
||||
if status == "up":
|
||||
self.parent.docker.compose.up(
|
||||
build=True,
|
||||
detach=True,
|
||||
quiet=True,
|
||||
services=[self.name],
|
||||
remove_orphans=True
|
||||
)
|
||||
elif status == "down":
|
||||
self.parent.docker.compose.down(
|
||||
services=[self.name]
|
||||
)
|
||||
elif status == "restart":
|
||||
self.parent.docker.compose.restart(
|
||||
services=[self.name]
|
||||
)
|
||||
else:
|
||||
logger.warn(f"Undefined status {status} - use up, down, or restart")
|
||||
except DockerException as error:
|
||||
logger.critical(f"Setting status for service {self.name} failed: {error}")
|
||||
logger.critical(error)
|
||||
|
||||
def update(self, restart=True):
|
||||
try:
|
||||
self.parent.docker.compose.build(
|
||||
quiet=True,
|
||||
services=[self.name]
|
||||
)
|
||||
except DockerException:
|
||||
logger.warn(f"Failed to build docker image for service {self.name} - cancelling container update")
|
||||
return None
|
||||
self.parent.docker.compose.pull(
|
||||
ignore_pull_failures=True,
|
||||
quiet=True,
|
||||
services=[self.name]
|
||||
)
|
||||
logger.info(f"Updated service {self.name}")
|
||||
self.set_status(self.get_status())
|
||||
10
composer/entrypoint.py
Normal file
10
composer/entrypoint.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from agent import *
|
||||
import logging
|
||||
from os import getenv
|
||||
import time
|
||||
|
||||
Setup.create_symlink()
|
||||
composer = Composer(getenv("REMOTE_REPO"))
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
2
composer/requirements.txt
Normal file
2
composer/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
python-on-whales
|
||||
GitPython
|
||||
Reference in New Issue
Block a user