Skip to content

Commit 201fa3f

Browse files
authored
Merge branch 'main' into chore/honor-restic-retention-settings
2 parents b70c582 + b56ba37 commit 201fa3f

File tree

6 files changed

+195
-171
lines changed

6 files changed

+195
-171
lines changed

.github/workflows/build-tag-release.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,6 @@ jobs:
6464
sed -i "s/release = .*/release = \"${clean_version}\"/" docs/conf.py
6565
uv lock --directory src --upgrade-package restic-compose-backup
6666
67-
- name: Commit changes
68-
if: ${{ steps.changed.outcome == 'success' }}
69-
uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5
70-
with:
71-
commit_message: automated version bump
72-
7367
- name: Push version tag
7468
if: ${{ steps.changed.outcome == 'success' }}
7569
uses: anothrNick/github-tag-action@e528bc2b9628971ce0e6f823f3052d1dcd9d512c # 1.73.0

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
author = 'Zetta.IO Technology AS'
2323

2424
# The full version, including alpha/beta/rc tags
25-
release = "0.0.2"
25+
release = "0.0.0"
2626

2727
# -- General configuration ---------------------------------------------------
2828

src/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "restic_compose_backup"
3-
version = "0.0.2"
3+
version = "0.0.0"
44
description = "Backup Docker Compose volumes and databases with Restic"
55
requires-python = ">=3.12"
66
dependencies = [
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.2"
1+
__version__ = "0.0.0"

src/restic_compose_backup/containers.py

Lines changed: 86 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,34 @@
1414

1515
class Container:
1616
"""Represents a docker container"""
17+
1718
container_type = None
1819

1920
def __init__(self, data: dict):
2021
self._data = data
21-
self._state = data.get('State')
22-
self._config = data.get('Config')
23-
self._mounts = [Mount(mnt, container=self) for mnt in data.get('Mounts')]
22+
self._state = data.get("State")
23+
self._config = data.get("Config")
24+
self._mounts = [Mount(mnt, container=self) for mnt in data.get("Mounts")]
2425

2526
if not self._state:
26-
raise ValueError('Container meta missing State')
27+
raise ValueError("Container meta missing State")
2728
if self._config is None:
28-
raise ValueError('Container meta missing Config')
29+
raise ValueError("Container meta missing Config")
2930

30-
self._labels = self._config.get('Labels')
31+
self._labels = self._config.get("Labels")
3132
if self._labels is None:
32-
raise ValueError('Container meta missing Config->Labels')
33+
raise ValueError("Container meta missing Config->Labels")
3334

3435
self._include = self._parse_pattern(self.get_label(enums.LABEL_VOLUMES_INCLUDE))
3536
self._exclude = self._parse_pattern(self.get_label(enums.LABEL_VOLUMES_EXCLUDE))
3637

3738
@property
38-
def instance(self) -> 'Container':
39+
def instance(self) -> "Container":
3940
"""Container: Get a service specific subclass instance"""
4041
# TODO: Do this smarter in the future (simple registry)
4142
if self.database_backup_enabled:
4243
from restic_compose_backup import containers_db
44+
4345
if self.mariadb_backup_enabled:
4446
return containers_db.MariadbContainer(self._data)
4547
if self.mysql_backup_enabled:
@@ -52,28 +54,29 @@ def instance(self) -> 'Container':
5254
@property
5355
def id(self) -> str:
5456
"""str: The id of the container"""
55-
return self._data.get('Id')
57+
return self._data.get("Id")
5658

5759
@property
5860
def hostname(self) -> str:
59-
"""12 character hostname based on id"""
60-
return self.id[:12]
61+
"""Hostname of the container"""
62+
return self.get_config("Hostname", default=self.id[0:12])
6163

6264
@property
6365
def image(self) -> str:
6466
"""Image name"""
65-
return self.get_config('Image')
67+
return self.get_config("Image")
6668

6769
@property
6870
def name(self) -> str:
6971
"""Container name"""
70-
return self._data['Name'].replace('/', '')
72+
return self._data["Name"].replace("/", "")
7173

7274
@property
7375
def service_name(self) -> str:
7476
"""Name of the container/service"""
75-
return self.get_label('com.docker.compose.service', default='') or \
76-
self.get_label('com.docker.swarm.service.name', default='')
77+
return self.get_label(
78+
"com.docker.compose.service", default=""
79+
) or self.get_label("com.docker.swarm.service.name", default="")
7780

7881
@property
7982
def backup_process_label(self) -> str:
@@ -83,7 +86,7 @@ def backup_process_label(self) -> str:
8386
@property
8487
def project_name(self) -> str:
8588
"""str: Name of the compose setup"""
86-
return self.get_label('com.docker.compose.project', default='')
89+
return self.get_label("com.docker.compose.project", default="")
8790

8891
@property
8992
def stack_name(self) -> str:
@@ -93,28 +96,28 @@ def stack_name(self) -> str:
9396
@property
9497
def is_oneoff(self) -> bool:
9598
"""Was this container started with run command?"""
96-
return self.get_label('com.docker.compose.oneoff', default='False') == 'True'
99+
return self.get_label("com.docker.compose.oneoff", default="False") == "True"
97100

98101
@property
99102
def environment(self) -> list:
100103
"""All configured env vars for the container as a list"""
101-
return self.get_config('Env')
104+
return self.get_config("Env")
102105

103106
def remove(self):
104107
self._data.remove()
105108

106109
def get_config_env(self, name) -> str:
107110
"""Get a config environment variable by name"""
108111
# convert to dict and fetch env var by name
109-
data = {i[0:i.find('=')]: i[i.find('=') + 1:] for i in self.environment}
112+
data = {i[0 : i.find("=")]: i[i.find("=") + 1 :] for i in self.environment}
110113
return data.get(name)
111114

112115
def set_config_env(self, name, value):
113116
"""Set an environment variable"""
114117
env = self.environment
115-
new_value = f'{name}={value}'
118+
new_value = f"{name}={value}"
116119
for i, entry in enumerate(env):
117-
if f'{name}=' in entry:
120+
if f"{name}=" in entry:
118121
env[i] = new_value
119122
break
120123
else:
@@ -129,19 +132,21 @@ def volumes(self) -> dict:
129132
volumes = {}
130133
for mount in self._mounts:
131134
volumes[mount.source] = {
132-
'bind': mount.destination,
133-
'mode': 'rw',
135+
"bind": mount.destination,
136+
"mode": "rw",
134137
}
135138

136139
return volumes
137140

138141
@property
139142
def backup_enabled(self) -> bool:
140143
"""Is backup enabled for this container?"""
141-
return any([
142-
self.volume_backup_enabled,
143-
self.database_backup_enabled,
144-
])
144+
return any(
145+
[
146+
self.volume_backup_enabled,
147+
self.database_backup_enabled,
148+
]
149+
)
145150

146151
@property
147152
def volume_backup_enabled(self) -> bool:
@@ -155,50 +160,63 @@ def volume_backup_enabled(self) -> bool:
155160
@property
156161
def database_backup_enabled(self) -> bool:
157162
"""bool: Is database backup enabled in any shape or form?"""
158-
return any([
159-
self.mysql_backup_enabled,
160-
self.mariadb_backup_enabled,
161-
self.postgresql_backup_enabled,
162-
])
163+
return any(
164+
[
165+
self.mysql_backup_enabled,
166+
self.mariadb_backup_enabled,
167+
self.postgresql_backup_enabled,
168+
]
169+
)
163170

164171
@property
165172
def mysql_backup_enabled(self) -> bool:
166173
"""bool: If the ``stack-back.mysql`` label is set"""
167174
explicity_enabled = utils.is_true(self.get_label(enums.LABEL_MYSQL_ENABLED))
168175
explicity_disabled = utils.is_false(self.get_label(enums.LABEL_MYSQL_ENABLED))
169-
automatically_enabled = utils.is_true(config.auto_backup_all) and self.image.startswith('mysql:')
176+
automatically_enabled = utils.is_true(
177+
config.auto_backup_all
178+
) and self.image.startswith("mysql:")
170179
return explicity_enabled or (automatically_enabled and not explicity_disabled)
171180

172181
@property
173182
def mariadb_backup_enabled(self) -> bool:
174183
"""bool: If the ``stack-back.mariadb`` label is set"""
175184
explicity_enabled = utils.is_true(self.get_label(enums.LABEL_MARIADB_ENABLED))
176185
explicity_disabled = utils.is_false(self.get_label(enums.LABEL_MARIADB_ENABLED))
177-
automatically_enabled = utils.is_true(config.auto_backup_all) and self.image.startswith('mariadb:')
186+
automatically_enabled = utils.is_true(
187+
config.auto_backup_all
188+
) and self.image.startswith("mariadb:")
178189
return explicity_enabled or (automatically_enabled and not explicity_disabled)
179190

180191
@property
181192
def postgresql_backup_enabled(self) -> bool:
182193
"""bool: If the ``stack-back.postgres`` label is set"""
183194
explicity_enabled = utils.is_true(self.get_label(enums.LABEL_POSTGRES_ENABLED))
184-
explicity_disabled = utils.is_false(self.get_label(enums.LABEL_POSTGRES_ENABLED))
185-
automatically_enabled = utils.is_true(config.auto_backup_all) and self.image.startswith('postgres:')
195+
explicity_disabled = utils.is_false(
196+
self.get_label(enums.LABEL_POSTGRES_ENABLED)
197+
)
198+
automatically_enabled = utils.is_true(
199+
config.auto_backup_all
200+
) and self.image.startswith("postgres:")
186201
return explicity_enabled or (automatically_enabled and not explicity_disabled)
187202

188203
@property
189204
def stop_during_backup(self) -> bool:
190205
"""bool: If the ``stack-back.volumes.stop-during-backup`` label is set"""
191-
return utils.is_true(self.get_label(enums.LABEL_STOP_DURING_BACKUP)) and not self.database_backup_enabled
206+
return (
207+
utils.is_true(self.get_label(enums.LABEL_STOP_DURING_BACKUP))
208+
and not self.database_backup_enabled
209+
)
192210

193211
@property
194212
def is_backup_process_container(self) -> bool:
195213
"""Is this container the running backup process?"""
196-
return self.get_label(self.backup_process_label) == 'True'
214+
return self.get_label(self.backup_process_label) == "True"
197215

198216
@property
199217
def is_running(self) -> bool:
200218
"""bool: Is the container running?"""
201-
return self._state.get('Running', False)
219+
return self._state.get("Running", False)
202220

203221
def get_config(self, name, default=None):
204222
"""Get value from config dict"""
@@ -215,7 +233,11 @@ def filter_mounts(self):
215233

216234
# If exclude_bind_mounts is true, only volume mounts are kept in the list of mounts
217235
exclude_bind_mounts = utils.is_true(config.exclude_bind_mounts)
218-
mounts = list(filter(lambda m: not exclude_bind_mounts or m.type == "volume", self._mounts))
236+
mounts = list(
237+
filter(
238+
lambda m: not exclude_bind_mounts or m.type == "volume", self._mounts
239+
)
240+
)
219241

220242
if not self.volume_backup_enabled:
221243
return filtered
@@ -239,20 +261,23 @@ def filter_mounts(self):
239261
filtered.append(mount)
240262
else:
241263
for mount in mounts:
242-
if self.database_backup_enabled and mount.destination in database_mounts:
264+
if (
265+
self.database_backup_enabled
266+
and mount.destination in database_mounts
267+
):
243268
continue
244269
filtered.append(mount)
245270

246271
return filtered
247272

248-
def volumes_for_backup(self, source_prefix='/volumes', mode='ro'):
273+
def volumes_for_backup(self, source_prefix="/volumes", mode="ro"):
249274
"""Get volumes configured for backup"""
250275
mounts = self.filter_mounts()
251276
volumes = {}
252277
for mount in mounts:
253278
volumes[mount.source] = {
254-
'bind': self.get_volume_backup_destination(mount, source_prefix),
255-
'mode': mode,
279+
"bind": self.get_volume_backup_destination(mount, source_prefix),
280+
"mode": mode,
256281
}
257282

258283
return volumes
@@ -263,7 +288,7 @@ def get_volume_backup_destination(self, mount, source_prefix) -> str:
263288

264289
if utils.is_true(config.include_project_name):
265290
project_name = self.project_name
266-
if project_name != '':
291+
if project_name != "":
267292
destination /= project_name
268293

269294
destination /= self.service_name
@@ -303,7 +328,7 @@ def _parse_pattern(self, value: str) -> List[str]:
303328
if len(value) == 0:
304329
return None
305330

306-
return value.split(',')
331+
return value.split(",")
307332

308333
def __eq__(self, other):
309334
"""Compare container by id"""
@@ -324,6 +349,7 @@ def __str__(self):
324349

325350
class Mount:
326351
"""Represents a volume mount (volume or bind)"""
352+
327353
def __init__(self, data, container=None):
328354
self._data = data
329355
self._container = container
@@ -336,22 +362,22 @@ def container(self) -> Container:
336362
@property
337363
def type(self) -> str:
338364
"""bind/volume"""
339-
return self._data.get('Type')
365+
return self._data.get("Type")
340366

341367
@property
342368
def name(self) -> str:
343369
"""Name of the mount"""
344-
return self._data.get('Name')
370+
return self._data.get("Name")
345371

346372
@property
347373
def source(self) -> str:
348374
"""Source of the mount. Volume name or path"""
349-
return self._data.get('Source')
375+
return self._data.get("Source")
350376

351377
@property
352378
def destination(self) -> str:
353379
"""Destination path for the volume mount in the container"""
354-
return self._data.get('Destination')
380+
return self._data.get("Destination")
355381

356382
def __repr__(self) -> str:
357383
return str(self)
@@ -382,7 +408,7 @@ def __init__(self):
382408
# Find the container we are running in.
383409
# If we don't have this information we cannot continue
384410
for container_data in all_containers:
385-
if container_data.get('Id').startswith(os.environ['HOSTNAME']):
411+
if container_data.get("Id").startswith(os.environ["HOSTNAME"]):
386412
self.this_container = Container(container_data)
387413

388414
if not self.this_container:
@@ -393,9 +419,11 @@ def __init__(self):
393419
container = Container(container_data)
394420

395421
# Gather stale backup process containers
396-
if (self.this_container.image == container.image
397-
and not container.is_running
398-
and container.is_backup_process_container):
422+
if (
423+
self.this_container.image == container.image
424+
and not container.is_running
425+
and container.is_backup_process_container
426+
):
399427
self.stale_backup_process_containers.append(container)
400428

401429
# We only care about running containers after this point
@@ -450,12 +478,14 @@ def containers_for_backup(self):
450478
"""Obtain all containers with backup enabled"""
451479
return [container for container in self.containers if container.backup_enabled]
452480

453-
def generate_backup_mounts(self, dest_prefix='/volumes') -> dict:
481+
def generate_backup_mounts(self, dest_prefix="/volumes") -> dict:
454482
"""Generate mounts for backup for the entire compose setup"""
455483
mounts = {}
456484
for container in self.containers_for_backup():
457485
if container.volume_backup_enabled:
458-
mounts.update(container.volumes_for_backup(source_prefix=dest_prefix, mode='ro'))
486+
mounts.update(
487+
container.volumes_for_backup(source_prefix=dest_prefix, mode="ro")
488+
)
459489

460490
return mounts
461491

0 commit comments

Comments
 (0)