From b2cae6f897b0bcacef6505597bdab5c408dab264 Mon Sep 17 00:00:00 2001 From: craisin Date: Wed, 31 Dec 2025 21:54:26 -0800 Subject: [PATCH] add docs --- README.md | 1 + composer/agent.py | 109 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a8247b --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Composer \ No newline at end of file diff --git a/composer/agent.py b/composer/agent.py index e92e35f..59da37b 100644 --- a/composer/agent.py +++ b/composer/agent.py @@ -16,8 +16,13 @@ logger.setLevel(logging.DEBUG) #TODO: add something to change log levles class Setup: + """Setup class ensuring appropriate files and symlinks are created for a Composer instance""" + @staticmethod 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) if host_data_env is None: logger.info(f"HOST_DATA_PATH is not set, symlink not created") @@ -35,42 +40,65 @@ class Setup: @staticmethod 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/.env").touch(exist_ok=True) class Composer: + """Composer instance to manage a single docker compose stack + + Attributes: + docker: DockerClient instance used to interact with the compose stack + services: list of Service objects defined by the stack + """ + services = [] + docker = DockerClient( + compose_files=["store/stack/compose.yml"], + 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._add_remote(remote_url) + self.repo.create_remote('origin', 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): + + """Recreates service list after the Composer stack is updated""" + 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): + + """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) try: self.update_time = int(update_time) @@ -81,6 +109,13 @@ class Composer: schedule.every(self.update_time).minutes.do(self.update) 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: if status == "up": self.docker.compose.up( @@ -103,6 +138,9 @@ class Composer: logger.critical(f"Setting status of composer failed: {error}") def update(self): + + """Updates the Composer stack based on the remote repo""" + current = self.repo.head.commit self.repo.remotes.origin.pull('master') if current != self.repo.head.commit: @@ -112,23 +150,54 @@ class Composer: 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): + + """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.labels = labels self.parent = parent - self.update_task = self.start_update_job() + self._update_task = self.start_update_job() def close(self): + + """Safely removes the service instance""" + if self.update_time is None: return None - schedule.cancel_job(self.update_task) + schedule.cancel_job(self._update_task) 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" 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 if 'composer.update' in self.labels: try: @@ -142,6 +211,13 @@ class Service: return None 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: if status == "up": self.parent.docker.compose.up( @@ -166,14 +242,9 @@ class Service: 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 + + """Updates the Composer stack based on container images""" + self.parent.docker.compose.pull( ignore_pull_failures=True, quiet=True,