Skip to content

Commit 9807ec1

Browse files
authored
Merge pull request #66 from AlexMcDermott/main #minor
Podman support and option to backup across all projects
2 parents 8cf5691 + 66934a2 commit 9807ec1

File tree

12 files changed

+89
-42
lines changed

12 files changed

+89
-42
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Contributions are welcome regardless of experience level.
133133

134134
## Python environment
135135

136-
Use [`uv`](https://docs.astral.sh/uv/) within the `src/` directory to manage your development environment.
136+
Use [`uv`](https://docs.astral.sh/uv/) within the repo root directory to manage your development environment.
137137

138138
```bash
139139
git clone https://github.com/lawndoc/stack-back.git

docs/guide/configuration.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,16 @@ docker volumes. Often host binds are only used
257257
for mapping in configuration. This saves the user
258258
from manually excluding these bind volumes.
259259

260+
INCLUDE_ALL_COMPOSE_PROJECTS
261+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
262+
263+
If defined all compose projects found will be available for backup.
264+
By default only the compose project the backup container is
265+
running in is available for backup.
266+
267+
This is useful when not wanting to run a separate backup container
268+
for each compose project.
269+
260270
SWARM_MODE
261271
~~~~~~~~~~
262272

src/entrypoint.sh

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ rcb dump-env > /.env
66
# Write crontab
77
rcb crontab > crontab
88

9-
# start cron in the foreground
9+
# Start cron in the background and capture its PID
1010
crontab crontab
11-
crond -f
11+
crond -f &
12+
CRON_PID=$!
13+
14+
# Trap termination signals and kill the cron process
15+
trap 'kill $CRON_PID; exit 0' TERM INT
16+
17+
# Wait for cron and handle signals
18+
wait $CRON_PID

src/restic_compose_backup/backup_runner.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,28 @@ def run(
1212
volumes: dict = None,
1313
environment: dict = None,
1414
labels: dict = None,
15-
source_container_id: str = None,
15+
network_names: set[str] = set(),
1616
):
1717
logger.info("Starting backup container")
1818
client = utils.docker_client()
1919

20-
container = client.containers.run(
20+
container = client.containers.create(
2121
image,
2222
command,
2323
labels=labels,
24-
# auto_remove=True, # We remove the container further down
2524
detach=True,
2625
environment=environment + ["BACKUP_PROCESS_CONTAINER=true"],
2726
volumes=volumes,
28-
network_mode=f"container:{source_container_id}", # Reuse original container's network stack.
2927
working_dir=os.getcwd(),
3028
tty=True,
3129
)
3230

31+
for network_name in network_names:
32+
network = client.networks.get(network_name)
33+
network.connect(container)
34+
35+
container.start()
36+
3337
logger.info("Backup process container: %s", container.name)
3438
log_generator = container.logs(stdout=True, stderr=True, stream=True, follow=True)
3539

src/restic_compose_backup/cli.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def status(config, containers):
8181
"Exclude bind mounts from backups?: %s",
8282
utils.is_true(config.exclude_bind_mounts),
8383
)
84+
logger.debug(
85+
"Include all compose projects?: %s",
86+
utils.is_true(config.include_all_compose_projects),
87+
)
8488
logger.debug(
8589
f"Use cache for integrity check?: {utils.is_true(config.check_with_cache)}"
8690
)
@@ -138,7 +142,7 @@ def status(config, containers):
138142
logger.info("-" * 67)
139143

140144

141-
def backup(config, containers):
145+
def backup(config, containers: RunningContainers):
142146
"""Request a backup to start"""
143147
# Make sure we don't spawn multiple backup processes
144148
if containers.backup_process_running:
@@ -169,7 +173,7 @@ def backup(config, containers):
169173
command="rcb start-backup-process",
170174
volumes=volumes,
171175
environment=containers.this_container.environment,
172-
source_container_id=containers.this_container.id,
176+
network_names=containers.networks_for_backup(),
173177
labels={
174178
containers.backup_process_label: "True",
175179
"com.docker.compose.project": containers.project_name,

src/restic_compose_backup/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ def __init__(self, check=True):
2929
self.swarm_mode = os.environ.get("SWARM_MODE") or False
3030
self.include_project_name = os.environ.get("INCLUDE_PROJECT_NAME") or False
3131
self.exclude_bind_mounts = os.environ.get("EXCLUDE_BIND_MOUNTS") or False
32+
self.include_all_compose_projects = (
33+
os.environ.get("INCLUDE_ALL_COMPOSE_PROJECTS") or False
34+
)
3235
self.include_all_volumes = os.environ.get("INCLUDE_ALL_VOLUMES") or False
3336
if self.include_all_volumes:
3437
logger.warning(

src/restic_compose_backup/containers.py

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import os
21
import logging
32
from pathlib import Path
3+
import socket
44
from typing import List
55

66
from restic_compose_backup import enums, utils
@@ -35,6 +35,10 @@ def __init__(self, data: dict):
3535
self._include = self._parse_pattern(self.get_label(enums.LABEL_VOLUMES_INCLUDE))
3636
self._exclude = self._parse_pattern(self.get_label(enums.LABEL_VOLUMES_EXCLUDE))
3737

38+
network_settings: dict = data.get("NetworkSettings", {})
39+
networks: dict = network_settings.get("Networks", {})
40+
self._network_details: dict = list(networks.values())[0]
41+
3842
@property
3943
def instance(self) -> "Container":
4044
"""Container: Get a service specific subclass instance"""
@@ -57,9 +61,14 @@ def id(self) -> str:
5761
return self._data.get("Id")
5862

5963
@property
60-
def hostname(self) -> str:
61-
"""Hostname of the container"""
62-
return self.get_config("Hostname", default=self.id[0:12])
64+
def network_name(self) -> str:
65+
"""str: Name of the first network the container is connected to"""
66+
return self._network_details.get("NetworkID", "")
67+
68+
@property
69+
def ip_address(self) -> str:
70+
"""str: IP address the container has on its first network"""
71+
return self._network_details.get("IPAddress", "")
6372

6473
@property
6574
def image(self) -> str:
@@ -407,13 +416,13 @@ def __init__(self):
407416
# Find the container we are running in.
408417
# If we don't have this information we cannot continue
409418
for container_data in all_containers:
410-
if container_data.get("Id").startswith(os.environ["HOSTNAME"]):
419+
if container_data.get("Id").startswith(socket.gethostname()):
411420
self.this_container = Container(container_data)
412421

413422
if not self.this_container:
414423
raise ValueError("Cannot find metadata for backup container")
415424

416-
# Gather all running containers in the current compose setup
425+
# Gather relevant containers
417426
for container_data in all_containers:
418427
container = Container(container_data)
419428

@@ -429,25 +438,22 @@ def __init__(self):
429438
if not container.is_running:
430439
continue
431440

441+
# If not swarm mode we need to filter in compose project
442+
if (
443+
not config.swarm_mode
444+
and not config.include_all_compose_projects
445+
and container.project_name != self.this_container.project_name
446+
):
447+
continue
448+
432449
# Gather stop during backup containers
433450
if container.stop_during_backup:
434-
if config.swarm_mode:
435-
self.stop_during_backup_containers.append(container)
436-
else:
437-
if container.project_name == self.this_container.project_name:
438-
self.stop_during_backup_containers.append(container)
451+
self.stop_during_backup_containers.append(container)
439452

440453
# Detect running backup process container
441454
if container.is_backup_process_container:
442455
self.backup_process_container = container
443456

444-
# --- Determine what containers should be evaluated
445-
446-
# If not swarm mode we need to filter in compose project
447-
if not config.swarm_mode:
448-
if container.project_name != self.this_container.project_name:
449-
continue
450-
451457
# Containers started manually are not included
452458
if container.is_oneoff:
453459
continue
@@ -473,10 +479,14 @@ def backup_process_running(self) -> bool:
473479
"""Is the backup process container running?"""
474480
return self.backup_process_container is not None
475481

476-
def containers_for_backup(self):
482+
def containers_for_backup(self) -> list[Container]:
477483
"""Obtain all containers with backup enabled"""
478484
return [container for container in self.containers if container.backup_enabled]
479485

486+
def networks_for_backup(self) -> set[str]:
487+
"""Obtain all networks needed for backup"""
488+
return {container.network_name for container in self.containers_for_backup()}
489+
480490
def generate_backup_mounts(self, dest_prefix="/volumes") -> dict:
481491
"""Generate mounts for backup for the entire compose setup"""
482492
mounts = {}

src/restic_compose_backup/containers_db.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def get_credentials(self) -> dict:
2121
username = self.get_config_env("MARIADB_USER")
2222
password = self.get_config_env("MARIADB_PASSWORD")
2323
return {
24-
"host": self.hostname,
24+
"host": self.ip_address,
2525
"username": username,
2626
"password": password,
2727
"port": "3306",
@@ -91,7 +91,7 @@ def get_credentials(self) -> dict:
9191
username = self.get_config_env("MYSQL_USER")
9292
password = self.get_config_env("MYSQL_PASSWORD")
9393
return {
94-
"host": self.hostname,
94+
"host": self.ip_address,
9595
"username": username,
9696
"password": password,
9797
"port": "3306",
@@ -155,7 +155,7 @@ class PostgresContainer(Container):
155155
def get_credentials(self) -> dict:
156156
"""dict: get credentials for the service"""
157157
return {
158-
"host": self.hostname,
158+
"host": self.ip_address,
159159
"username": self.get_config_env("POSTGRES_USER"),
160160
"password": self.get_config_env("POSTGRES_PASSWORD"),
161161
"port": "5432",

src/restic_compose_backup/log.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import logging
2-
import os
32
import sys
43

54
logger = logging.getLogger("restic_compose_backup")
6-
HOSTNAME = os.environ["HOSTNAME"]
75

86
DEFAULT_LOG_LEVEL = logging.INFO
97
LOG_LEVELS = {
@@ -22,7 +20,5 @@ def setup(level: str = "warning"):
2220

2321
ch = logging.StreamHandler(stream=sys.stdout)
2422
ch.setLevel(level)
25-
# ch.setFormatter(logging.Formatter('%(asctime)s - {HOSTNAME} - %(name)s - %(levelname)s - %(message)s'))
26-
# ch.setFormatter(logging.Formatter('%(asctime)s - {HOSTNAME} - %(levelname)s - %(message)s'))
2723
ch.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s: %(message)s"))
2824
logger.addHandler(ch)

src/tests/fixtures.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def wrapper(*args, **kwargs):
5353
"Status": "running",
5454
"Running": True,
5555
},
56+
"NetworkSettings": {
57+
"Networks": {
58+
"NetworkID": "network-id",
59+
"IPAddress": "10.0.0.1",
60+
}
61+
},
5662
}
5763
for container in containers
5864
]

0 commit comments

Comments
 (0)