159 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			159 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """Docker controller"""
 | |
| from time import sleep
 | |
| from typing import Dict, Tuple
 | |
| 
 | |
| from django.conf import settings
 | |
| from docker import DockerClient
 | |
| from docker.errors import DockerException, NotFound
 | |
| from docker.models.containers import Container
 | |
| from yaml import safe_dump
 | |
| 
 | |
| from passbook import __version__
 | |
| from passbook.outposts.controllers.base import BaseController, ControllerException
 | |
| from passbook.outposts.models import DockerServiceConnection, Outpost
 | |
| 
 | |
| 
 | |
| class DockerController(BaseController):
 | |
|     """Docker controller"""
 | |
| 
 | |
|     client: DockerClient
 | |
| 
 | |
|     container: Container
 | |
|     connection: DockerServiceConnection
 | |
| 
 | |
|     image_base = "beryju/passbook"
 | |
| 
 | |
|     def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None:
 | |
|         super().__init__(outpost, connection)
 | |
|         try:
 | |
|             if self.connection.local:
 | |
|                 self.client = DockerClient.from_env()
 | |
|             else:
 | |
|                 self.client = DockerClient(
 | |
|                     base_url=self.connection.url,
 | |
|                     tls=self.connection.tls,
 | |
|                 )
 | |
|         except DockerException as exc:
 | |
|             raise ControllerException from exc
 | |
| 
 | |
|     def _get_labels(self) -> Dict[str, str]:
 | |
|         return {}
 | |
| 
 | |
|     def _get_env(self) -> Dict[str, str]:
 | |
|         return {
 | |
|             "PASSBOOK_HOST": self.outpost.config.passbook_host,
 | |
|             "PASSBOOK_INSECURE": str(self.outpost.config.passbook_host_insecure),
 | |
|             "PASSBOOK_TOKEN": self.outpost.token.key,
 | |
|         }
 | |
| 
 | |
|     def _comp_env(self, container: Container) -> bool:
 | |
|         """Check if container's env is equal to what we would set. Return true if container needs
 | |
|         to be rebuilt."""
 | |
|         should_be = self._get_env()
 | |
|         container_env = container.attrs.get("Config", {}).get("Env", {})
 | |
|         for key, expected_value in should_be.items():
 | |
|             if key not in container_env:
 | |
|                 continue
 | |
|             if container_env[key] != expected_value:
 | |
|                 return True
 | |
|         return False
 | |
| 
 | |
|     def _get_container(self) -> Tuple[Container, bool]:
 | |
|         container_name = f"passbook-proxy-{self.outpost.uuid.hex}"
 | |
|         try:
 | |
|             return self.client.containers.get(container_name), False
 | |
|         except NotFound:
 | |
|             self.logger.info("Container does not exist, creating")
 | |
|             image_name = f"{self.image_base}-{self.outpost.type}:{__version__}"
 | |
|             self.client.images.pull(image_name)
 | |
|             return (
 | |
|                 self.client.containers.create(
 | |
|                     image=image_name,
 | |
|                     name=f"passbook-proxy-{self.outpost.uuid.hex}",
 | |
|                     detach=True,
 | |
|                     ports={x: x for _, x in self.deployment_ports.items()},
 | |
|                     environment=self._get_env(),
 | |
|                     network_mode="host" if settings.TEST else "bridge",
 | |
|                     labels=self._get_labels(),
 | |
|                 ),
 | |
|                 True,
 | |
|             )
 | |
| 
 | |
|     def up(self):
 | |
|         try:
 | |
|             container, has_been_created = self._get_container()
 | |
|             # Check if the container is out of date, delete it and retry
 | |
|             if len(container.image.tags) > 0:
 | |
|                 tag: str = container.image.tags[0]
 | |
|                 _, _, version = tag.partition(":")
 | |
|                 if version != __version__:
 | |
|                     self.logger.info(
 | |
|                         "Container has mismatched version, re-creating...",
 | |
|                         has=version,
 | |
|                         should=__version__,
 | |
|                     )
 | |
|                     container.kill()
 | |
|                     container.remove(force=True)
 | |
|                     return self.up()
 | |
|             # Check that container values match our values
 | |
|             if self._comp_env(container):
 | |
|                 self.logger.info("Container has outdated config, re-creating...")
 | |
|                 container.kill()
 | |
|                 container.remove(force=True)
 | |
|                 return self.up()
 | |
|             # Check that container is healthy
 | |
|             if (
 | |
|                 container.status == "running"
 | |
|                 and container.attrs.get("State", {}).get("Health", {}).get("Status", "")
 | |
|                 != "healthy"
 | |
|             ):
 | |
|                 # At this point we know the config is correct, but the container isn't healthy,
 | |
|                 # so we just restart it with the same config
 | |
|                 if has_been_created:
 | |
|                     # Since we've just created the container, give it some time to start.
 | |
|                     # If its still not up by then, restart it
 | |
|                     self.logger.info(
 | |
|                         "Container is unhealthy and new, giving it time to boot."
 | |
|                     )
 | |
|                     sleep(60)
 | |
|                 self.logger.info("Container is unhealthy, restarting...")
 | |
|                 container.restart()
 | |
|                 return None
 | |
|             # Check that container is running
 | |
|             if container.status != "running":
 | |
|                 self.logger.info("Container is not running, restarting...")
 | |
|                 container.start()
 | |
|                 return None
 | |
|             return None
 | |
|         except DockerException as exc:
 | |
|             raise ControllerException from exc
 | |
| 
 | |
|     def down(self):
 | |
|         try:
 | |
|             container, _ = self._get_container()
 | |
|             container.kill()
 | |
|             container.remove()
 | |
|         except DockerException as exc:
 | |
|             raise ControllerException from exc
 | |
| 
 | |
|     def get_static_deployment(self) -> str:
 | |
|         """Generate docker-compose yaml for proxy, version 3.5"""
 | |
|         ports = [f"{x}:{x}" for _, x in self.deployment_ports.items()]
 | |
|         compose = {
 | |
|             "version": "3.5",
 | |
|             "services": {
 | |
|                 f"passbook_{self.outpost.type}": {
 | |
|                     "image": f"{self.image_base}-{self.outpost.type}:{__version__}",
 | |
|                     "ports": ports,
 | |
|                     "environment": {
 | |
|                         "PASSBOOK_HOST": self.outpost.config.passbook_host,
 | |
|                         "PASSBOOK_INSECURE": str(
 | |
|                             self.outpost.config.passbook_host_insecure
 | |
|                         ),
 | |
|                         "PASSBOOK_TOKEN": self.outpost.token.key,
 | |
|                     },
 | |
|                 }
 | |
|             },
 | |
|         }
 | |
|         return safe_dump(compose, default_flow_style=False)
 | 
