This commit is contained in:
2025-12-31 21:54:26 -08:00
parent eac6faf073
commit b2cae6f897
2 changed files with 91 additions and 19 deletions

1
README.md Normal file
View File

@@ -0,0 +1 @@
# Composer

View File

@@ -16,8 +16,13 @@ logger.setLevel(logging.DEBUG) #TODO: add something to change log levles
class Setup: class Setup:
"""Setup class ensuring appropriate files and symlinks are created for a Composer instance"""
@staticmethod @staticmethod
def create_symlink(): def create_symlink():
"""Creates a symlink between the path stored in HOST_DATA_PATH and store/data"""
host_data_env = getenv("HOST_DATA_PATH", None) host_data_env = getenv("HOST_DATA_PATH", None)
if host_data_env is None: if host_data_env is None:
logger.info(f"HOST_DATA_PATH is not set, symlink not created") logger.info(f"HOST_DATA_PATH is not set, symlink not created")
@@ -35,25 +40,42 @@ class Setup:
@staticmethod @staticmethod
def populate_store(): def populate_store():
"""Creates empty store/stack/.env file if it doesn't exist"""
Path("store/stack").mkdir(parents=True, exist_ok=True) Path("store/stack").mkdir(parents=True, exist_ok=True)
Path("store/stack/.env").touch(exist_ok=True) Path("store/stack/.env").touch(exist_ok=True)
class Composer: class Composer:
services = [] """Composer instance to manage a single docker compose stack
def __init__(self, remote_url): Attributes:
if not Path("store/stack/.git").exists(): docker: DockerClient instance used to interact with the compose stack
self.repo = Repo.init("store/stack") services: list of Service objects defined by the stack
self._add_remote(remote_url) """
else:
self.repo = Repo("store/stack") services = []
self.repo.remotes.origin.pull('master') docker = DockerClient(
self.docker = DockerClient(
compose_files=["store/stack/compose.yml"], compose_files=["store/stack/compose.yml"],
compose_env_files=["store/stack/.env"] compose_env_files=["store/stack/.env"]
) )
def __init__(self, remote_url):
"""Initializes the Composer based on a remote_url
Args:
remote_url: string containing the https location of the repo
"""
if not Path("store/stack/.git").exists():
self.repo = Repo.init("store/stack")
self.repo.create_remote('origin', url)
else:
self.repo = Repo("store/stack")
self.repo.remotes.origin.pull('master')
if self.docker.compose.config(return_json=True)["name"] != "stack": 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") 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.start_update_job()
@@ -61,16 +83,22 @@ class Composer:
self.set_status("up") self.set_status("up")
logger.info("Composer started and services created") logger.info("Composer started and services created")
def _add_remote(self, url):
self.repo.create_remote('origin', url)
def _create_services(self): def _create_services(self):
"""Recreates service list after the Composer stack is updated"""
for service in self.services: for service in self.services:
self.services[service].close() self.services[service].close()
services_dict = self.docker.compose.config().services services_dict = self.docker.compose.config().services
self.services = {service: Service(service, services_dict[service].labels, self) for service in services_dict} self.services = {service: Service(service, services_dict[service].labels, self) for service in services_dict}
def start_update_job(self): def start_update_job(self):
"""Schedules a recurring update based on the UPDATE environment variable
Each update occurs after an interval of UPDATE minutes. If UPDATE does not contain a valid integer in minutes, defaults to 1 minute.
"""
update_time = getenv("UPDATE", 1) update_time = getenv("UPDATE", 1)
try: try:
self.update_time = int(update_time) self.update_time = int(update_time)
@@ -81,6 +109,13 @@ class Composer:
schedule.every(self.update_time).minutes.do(self.update) schedule.every(self.update_time).minutes.do(self.update)
def set_status(self, status): def set_status(self, status):
"""Sets the running status of the compose stack
Args:
status: string containing "up", "down", or "restart" corresponding to the respective running state of the composer
"""
try: try:
if status == "up": if status == "up":
self.docker.compose.up( self.docker.compose.up(
@@ -103,6 +138,9 @@ class Composer:
logger.critical(f"Setting status of composer failed: {error}") logger.critical(f"Setting status of composer failed: {error}")
def update(self): def update(self):
"""Updates the Composer stack based on the remote repo"""
current = self.repo.head.commit current = self.repo.head.commit
self.repo.remotes.origin.pull('master') self.repo.remotes.origin.pull('master')
if current != self.repo.head.commit: if current != self.repo.head.commit:
@@ -112,23 +150,54 @@ class Composer:
class Service: class Service:
update_task = None """Service instance meant to represent a container in a Composer stack
Attributes:
labels: labels given to the service
name: name given to the service
parent: Composer instance containing the service
"""
def __init__(self, name, labels, parent): def __init__(self, name, labels, parent):
"""Initializes the Service instance
Args:
name: name given to the service
labels: labels given to the service
parent: Composer instance containing the service
"""
self.name = name self.name = name
self.labels = labels self.labels = labels
self.parent = parent self.parent = parent
self.update_task = self.start_update_job() self._update_task = self.start_update_job()
def close(self): def close(self):
"""Safely removes the service instance"""
if self.update_time is None: if self.update_time is None:
return None return None
schedule.cancel_job(self.update_task) schedule.cancel_job(self._update_task)
def get_status(self): def get_status(self):
"""Gets the running status of the service
Returns:
"up" or "down" depending on the running status of the service
"""
return "up" if bool(self.parent.docker.compose.ps(services=[self.name])) else "down" return "up" if bool(self.parent.docker.compose.ps(services=[self.name])) else "down"
def start_update_job(self): def start_update_job(self):
"""Schedules a recurring update based on the composer.update label
Each update occurs after an interval of composer.update minutes. If composer.update does not contain a valid integer in minutes, defaults to the parent Composer's update_time.
"""
self.update_time = self.parent.update_time self.update_time = self.parent.update_time
if 'composer.update' in self.labels: if 'composer.update' in self.labels:
try: try:
@@ -142,6 +211,13 @@ class Service:
return None return None
def set_status(self, status): def set_status(self, status):
"""Sets the running status of the service
Args:
status: string containing "up", "down", or "restart" corresponding to the respective running state of the service
"""
try: try:
if status == "up": if status == "up":
self.parent.docker.compose.up( self.parent.docker.compose.up(
@@ -166,14 +242,9 @@ class Service:
logger.critical(error) logger.critical(error)
def update(self, restart=True): def update(self, restart=True):
try:
self.parent.docker.compose.build( """Updates the Composer stack based on container images"""
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( self.parent.docker.compose.pull(
ignore_pull_failures=True, ignore_pull_failures=True,
quiet=True, quiet=True,