* backend: refactor swarm support to also support podman (#260) - implement podman support as subclass of swarm deployment - podman is used when 'RUNTIME=podman' env var is set - podman socket is mapped instead of docker socket - podman-compose is used instead of docker-compose (though docker-compose works with podman, it does not support secrets, but podman-compose does) - separate cli utils into SwarmRunner and PodmanRunner which extends it - using config.yaml and config.env, both copied from sample versions - work on simplifying config: add docker-compose.podman.yml and docker-compose.swarm.yml and signing and debug configs in ./configs - add {build,run,stop}-{swarm,podman}.sh in scripts dir - add init-configs, only copy if configs don't exist - build local image use current version of podman, to support both podman 3.x and 4.x - additional fixes for after testing podman on centos - docs: update Deployment.md to cover swarm, podman, k8s deployment
241 lines
7.5 KiB
Python
241 lines
7.5 KiB
Python
""" swarm util functions """
|
|
|
|
import tempfile
|
|
import os
|
|
import subprocess
|
|
|
|
from python_on_whales import client_config, DockerClient
|
|
from python_on_whales.exceptions import DockerException
|
|
|
|
|
|
# ============================================================================
|
|
def get_templates_dir():
|
|
""" return directory containing templates for loading """
|
|
return os.path.join(os.path.dirname(__file__), "templates")
|
|
|
|
|
|
# ============================================================================
|
|
def get_runner(runtime=None):
|
|
""" return either Swarm or Podman Runner based on env setting """
|
|
if runtime is None:
|
|
runtime = os.environ.get("RUNTIME", "")
|
|
|
|
if runtime == "podman":
|
|
return PodmanComposeRunner()
|
|
|
|
return SwarmRunner()
|
|
|
|
|
|
# ============================================================================
|
|
class SwarmRunner:
|
|
""" Run in Swarm """
|
|
|
|
def __init__(self):
|
|
self.client = DockerClient()
|
|
|
|
def run_service_stack(self, name, data):
|
|
""" run compose/swarm stack via interpolated file """
|
|
with tempfile.NamedTemporaryFile("wt") as fh_io:
|
|
fh_io.write(data)
|
|
fh_io.flush()
|
|
|
|
try:
|
|
self.client.stack.deploy(
|
|
name,
|
|
compose_files=[fh_io.name],
|
|
orchestrator="swarm",
|
|
resolve_image="never",
|
|
)
|
|
except DockerException as exc:
|
|
print(exc, flush=True)
|
|
|
|
return name
|
|
|
|
def delete_service_stack(self, name):
|
|
""" remove stack """
|
|
try:
|
|
self.client.stack.remove(name)
|
|
return True
|
|
except DockerException as exc:
|
|
print(exc, flush=True)
|
|
return False
|
|
|
|
def delete_volumes(self, names):
|
|
""" remove stack """
|
|
try:
|
|
self.client.volume.remove(names)
|
|
return True
|
|
except DockerException as exc:
|
|
print(exc, flush=True)
|
|
return False
|
|
|
|
def create_secret(self, name, data, labels=None):
|
|
""" create secret from specified data """
|
|
with tempfile.NamedTemporaryFile("wt") as fh_io:
|
|
fh_io.write(data)
|
|
fh_io.flush()
|
|
|
|
try:
|
|
self.client.secret.create(name, fh_io.name, labels=labels)
|
|
except DockerException as exc:
|
|
print(exc, flush=True)
|
|
|
|
def delete_secret(self, name):
|
|
""" remove secret by name """
|
|
try:
|
|
self.client.secret.remove(name)
|
|
return True
|
|
except DockerException as exc:
|
|
print(exc, flush=True)
|
|
return False
|
|
|
|
def delete_secrets(self, label):
|
|
""" delete secret with specified label """
|
|
try:
|
|
configs = self.client.secret.list(filters={"label": label})
|
|
for config in configs:
|
|
config.remove()
|
|
|
|
return True
|
|
except DockerException as exc:
|
|
print(exc, flush=True)
|
|
return False
|
|
|
|
def get_service(self, service_name):
|
|
""" get a swarm service """
|
|
try:
|
|
res = self.client.service.inspect(service_name)
|
|
return res
|
|
except DockerException:
|
|
return None
|
|
|
|
def get_service_labels(self, service_name):
|
|
""" get labels from a swarm service """
|
|
service = self.get_service(service_name)
|
|
return service.spec.labels if service else {}
|
|
|
|
def set_service_label(self, service_name, label):
|
|
""" update label """
|
|
exe_file = client_config.get_docker_binary_path_in_cache()
|
|
|
|
try:
|
|
subprocess.run(
|
|
[
|
|
exe_file,
|
|
"service",
|
|
"update",
|
|
service_name,
|
|
"--label-add",
|
|
label,
|
|
],
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
# pylint: disable=broad-except
|
|
except Exception as exc:
|
|
print(exc, flush=True)
|
|
|
|
def ping_containers(self, value, signal_="SIGTERM"):
|
|
""" ping running containers with given service name with signal """
|
|
try:
|
|
count = 0
|
|
conts = self.client.container.list(filters={"name": value})
|
|
for cont in conts:
|
|
print("Sending Signal: " + signal_, flush=True)
|
|
cont.kill(signal_)
|
|
count += 1
|
|
return count
|
|
except DockerException as exc:
|
|
print(exc, flush=True)
|
|
return 0
|
|
|
|
|
|
# ============================================================================
|
|
class PodmanComposeRunner(SwarmRunner):
|
|
""" Run via Docker Compose """
|
|
|
|
def __init__(self):
|
|
# pylint: disable=super-init-not-called
|
|
self.podman_exe = "podman"
|
|
# self.podman_exe = client_config.get_docker_binary_path_in_cache()
|
|
|
|
self.client = DockerClient(client_call=[self.podman_exe])
|
|
|
|
def run_service_stack(self, name, data):
|
|
""" run compose/swarm stack via interpolated file """
|
|
with tempfile.NamedTemporaryFile("wt") as fh_io:
|
|
fh_io.write(data)
|
|
fh_io.flush()
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
[
|
|
"podman-compose",
|
|
"--podman-path",
|
|
self.podman_exe,
|
|
"-f",
|
|
fh_io.name,
|
|
"-p",
|
|
name,
|
|
"up",
|
|
"-d",
|
|
],
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
print("stdout")
|
|
print("------")
|
|
print(result.stdout.decode("utf-8"))
|
|
print("stderr")
|
|
print("------")
|
|
print(result.stderr.decode("utf-8"), flush=True)
|
|
# pylint: disable=broad-except
|
|
except Exception as exc:
|
|
print(exc, flush=True)
|
|
|
|
def delete_service_stack(self, name):
|
|
""" delete compose stack """
|
|
print("Deleting Stack: " + name, flush=True)
|
|
|
|
for container in self.client.container.list(
|
|
filters={"label": f"com.docker.compose.project={name}"}
|
|
):
|
|
container.kill()
|
|
container.remove(volumes=True, force=True)
|
|
|
|
for volume in self.client.volume.list(
|
|
filters={"label": f"com.docker.compose.project={name}"}
|
|
):
|
|
volume.remove()
|
|
|
|
def create_secret(self, name, data, labels=None):
|
|
""" create secret from specified data """
|
|
with tempfile.NamedTemporaryFile("wt") as fh_io:
|
|
fh_io.write(data)
|
|
fh_io.flush()
|
|
|
|
try:
|
|
# labels not supported
|
|
self.client.secret.create(name, fh_io.name)
|
|
except DockerException as exc:
|
|
print(exc, flush=True)
|
|
|
|
def delete_secret(self, name):
|
|
""" remove secret by name """
|
|
# python-on-whale calls 'remove' but podman only supports 'rm', so call directly
|
|
try:
|
|
subprocess.run([self.podman_exe, "secret", "rm", name], check=True)
|
|
return True
|
|
# pylint: disable=broad-except
|
|
except Exception as exc:
|
|
print(exc, flush=True)
|
|
return False
|
|
|
|
def get_service(self, service_name):
|
|
""" get a swarm service """
|
|
try:
|
|
res = self.client.container.inspect(service_name)
|
|
return res
|
|
except DockerException:
|
|
return None
|