browsertrix/backend/btrixcloud/swarm/utils.py
Ilya Kreymer 0c8a5a49b4 refactor to use docker swarm for local alternative to k8s instead of docker compose (#247):
- use python-on-whale to use docker cli api directly, creating docker stack for each crawl or profile browser
- configure storages via storages.yaml secret
- add crawl_job, profile_job, splitting into base and k8s/swarm implementations
- split manager into base crawlmanager and k8s/swarm implementations
- swarm: load initial scale from db to avoid modifying fixed configs, in k8s, load from configmap
- swarm: support scheduled jobs via swarm-cronjob service
- remove docker dependencies (aiodocker, apscheduler, scheduling)
- swarm: when using local minio, expose via /data/ route in nginx via extra include (in k8s, include dir is empty and routing handled via ingress)
- k8s: cleanup minio chart: move init containers to minio.yaml
- swarm: stateful set implementation to be consistent with k8s scaling:
  - don't use service replicas,
  - create a unique service with '-N' appended and allocate unique volume for each replica
  - allows crawl containers to be restarted w/o losing data
- add volume pruning background service, as volumes can be deleted only after service shuts down fully
- watch: fully simplify routing, route via replica index instead of ip for both k8s and swarm
- rename network btrix-cloud-net -> btrix-net to avoid conflict with compose network
2022-06-05 10:37:17 -07:00

160 lines
3.9 KiB
Python

""" swarm util functions """
import tempfile
import os
import base64
import subprocess
from python_on_whales import docker, client_config
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 run_swarm_stack(name, data):
""" run swarm stack via interpolated file """
with tempfile.NamedTemporaryFile("wt") as fh_io:
fh_io.write(data)
fh_io.flush()
try:
docker.stack.deploy(name, compose_files=[fh_io.name], orchestrator="swarm")
except DockerException as exc:
print(exc, flush=True)
return name
def delete_swarm_stack(name):
""" remove stack """
try:
docker.stack.remove(name)
return True
except DockerException as exc:
print(exc, flush=True)
return False
def delete_volumes(names):
""" remove stack """
try:
docker.volume.remove(names)
return True
except DockerException as exc:
print(exc, flush=True)
return False
def create_config(name, data, labels):
""" create config from specified data """
with tempfile.NamedTemporaryFile("wt") as fh_io:
fh_io.write(data)
fh_io.flush()
try:
docker.config.create(name, fh_io.name, labels=labels)
except DockerException as exc:
print(exc, flush=True)
def get_config(name):
""" get config data, base64 decode """
try:
config = docker.config.inspect(name)
return base64.b64decode(config.spec.data)
except DockerException as exc:
print(exc, flush=True)
return None
def delete_config(name):
""" get config data, base64 decode """
try:
docker.config.remove(name)
return True
except DockerException as exc:
print(exc, flush=True)
return False
def delete_configs(label):
""" delete configs with specified label """
try:
configs = docker.config.list(filters={"label": label})
for config in configs:
config.remove()
except DockerException as exc:
print(exc, flush=True)
def get_service(service_name):
""" get a swarm service """
try:
res = docker.service.inspect(service_name)
return res
except DockerException:
return None
def get_service_labels(service_name):
""" get labels from a swarm service """
service = get_service(service_name)
return service.spec.labels if service else {}
def set_service_label(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 scale_service(service_name, new_scale):
""" update scale of service """
service = get_service(service_name)
if not service:
print(f"service {service_name} not found", flush=True)
return False
try:
service.scale(new_scale)
except DockerException as exc:
print(exc, flush=True)
return False
return True
def ping_containers(value, signal_="SIGTERM"):
""" ping running containers with given service name with signal """
try:
count = 0
conts = docker.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