seto 3.2.0__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {seto-3.2.0 → seto-3.4.0}/PKG-INFO +1 -1
- {seto-3.2.0 → seto-3.4.0}/pyproject.toml +2 -1
- {seto-3.2.0 → seto-3.4.0}/seto/__main__.py +3 -0
- {seto-3.2.0 → seto-3.4.0}/seto/commands/config.py +4 -3
- {seto-3.2.0 → seto-3.4.0}/seto/commands/deploy.py +25 -21
- {seto-3.2.0 → seto-3.4.0}/seto/core/command.py +2 -1
- {seto-3.2.0 → seto-3.4.0}/seto/core/parser.py +31 -12
- {seto-3.2.0 → seto-3.4.0}/LICENSE +0 -0
- {seto-3.2.0 → seto-3.4.0}/LICENSE_HEADER.txt +0 -0
- {seto-3.2.0 → seto-3.4.0}/README.md +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/__init__.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/commands/down.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/commands/mount.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/commands/setup.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/commands/umount.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/commands/volumes.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/core/dns.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/core/docker.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/core/driver.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/core/network.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/core/permissions.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/core/shell.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/core/swarm.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/core/traefik.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/core/volume.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/drivers/nfs.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/shells/local.py +0 -0
- {seto-3.2.0 → seto-3.4.0}/seto/shells/remote.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "seto"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.4.0"
|
|
8
8
|
description = "A Docker Swarm Deployment Manager"
|
|
9
9
|
keywords = ["docker", "swarm", "manager"]
|
|
10
10
|
authors = ["Sébastien Demanou <demsking@gmail.com>"]
|
|
@@ -33,6 +33,7 @@ pyyaml = "^6.0.1"
|
|
|
33
33
|
docker = "^7.1.0"
|
|
34
34
|
|
|
35
35
|
[tool.poetry.group.dev.dependencies]
|
|
36
|
+
classify-imports = "^4.2.0"
|
|
36
37
|
|
|
37
38
|
[tool.pytest.ini_options]
|
|
38
39
|
# Function starting with the following pattern are considered for test cases.
|
|
@@ -194,6 +194,9 @@ def main() -> None:
|
|
|
194
194
|
'deploy', description='Deploy a new stack or update an existing stack'
|
|
195
195
|
)
|
|
196
196
|
deploy_parser.set_defaults(func=execute_deploy_command)
|
|
197
|
+
deploy_parser.add_argument(
|
|
198
|
+
'--image-prefix', default='', help='Image namespace prefix to added to internal images'
|
|
199
|
+
)
|
|
197
200
|
|
|
198
201
|
#
|
|
199
202
|
# Down command
|
|
@@ -51,8 +51,8 @@ def resolve(
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
def parse(resolved_compose_data: dict, volumes: list[Volume]):
|
|
54
|
-
placement_hostname = resolved_compose_data.get('x-placement-hostname', None)
|
|
55
|
-
placement = resolved_compose_data.get('x-placement', None)
|
|
54
|
+
placement_hostname: str | None = resolved_compose_data.get('x-placement-hostname', None)
|
|
55
|
+
placement: str | None = resolved_compose_data.get('x-placement', None)
|
|
56
56
|
networks_ = resolved_compose_data.get('networks', {})
|
|
57
57
|
services = resolved_compose_data.get('services', {})
|
|
58
58
|
volumes = resolved_compose_data.get('volumes', {})
|
|
@@ -72,10 +72,11 @@ def resolve(
|
|
|
72
72
|
raise ValueError('Missing required x-placement or x-placement-hostname field')
|
|
73
73
|
|
|
74
74
|
if execute:
|
|
75
|
-
execute(resolved_compose_data, placement_hostname or placement)
|
|
75
|
+
execute(resolved_compose_data, placement_hostname or placement or '')
|
|
76
76
|
|
|
77
77
|
parse_services(
|
|
78
78
|
driver=driver,
|
|
79
|
+
image_prefix=args.image_prefix,
|
|
79
80
|
stack=args.stack or args.project,
|
|
80
81
|
execute=parse,
|
|
81
82
|
inject=inject,
|
|
@@ -110,7 +110,9 @@ def parse_compose_config(
|
|
|
110
110
|
published_port = random.randint(53100, 64200)
|
|
111
111
|
|
|
112
112
|
if not service_traefik_port:
|
|
113
|
-
print(
|
|
113
|
+
print(
|
|
114
|
+
f'WARN: Service "{service_name}" has no defined port. Skipped from Traefik HTTP Provider'
|
|
115
|
+
)
|
|
114
116
|
continue
|
|
115
117
|
|
|
116
118
|
service_ports.append(f'{published_port}:{service_traefik_port}')
|
|
@@ -184,6 +186,7 @@ def deploy_seto_stack(args, driver: Driver, replica: list[Setting]) -> None:
|
|
|
184
186
|
# Resolving compose local volumes
|
|
185
187
|
resolved_compose_data, volumes = resolve_compose_file(
|
|
186
188
|
driver=driver,
|
|
189
|
+
image_prefix=args.image_prefix,
|
|
187
190
|
compose_data=internal_stack,
|
|
188
191
|
inject=True,
|
|
189
192
|
)
|
|
@@ -239,30 +242,31 @@ def execute_deploy_command(args, driver: Driver) -> None:
|
|
|
239
242
|
},
|
|
240
243
|
}
|
|
241
244
|
|
|
242
|
-
register_command = ' && '.join(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
# Log the start of the initial endpoint call
|
|
249
|
-
'echo "Starting initial call to the provider endpoint..."',
|
|
250
|
-
|
|
251
|
-
# Call the POST endpoint at startup
|
|
252
|
-
register_command,
|
|
253
|
-
|
|
254
|
-
# Log the end of the initial endpoint call
|
|
255
|
-
'echo "Initial call to the provider endpoint completed. Running cron job..."',
|
|
256
|
-
|
|
257
|
-
# Set up the cron job
|
|
258
|
-
f'echo "*/1 * * * * {register_command}" | crontab -',
|
|
245
|
+
register_command = ' && '.join(
|
|
246
|
+
[
|
|
247
|
+
f'echo "Registering service {driver.stack_id}..."',
|
|
248
|
+
f'curl -s -X POST http://{HTTP_PROVIDER_SERVICENAME}:6116/api/config/{driver.stack_id} -H "Content-Type: application/json" -d @{traefik_http_provider_target} > /dev/nul',
|
|
249
|
+
]
|
|
250
|
+
)
|
|
259
251
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
252
|
+
entrypoint = ' && '.join(
|
|
253
|
+
[
|
|
254
|
+
# Log the start of the initial endpoint call
|
|
255
|
+
'echo "Starting initial call to the provider endpoint..."',
|
|
256
|
+
# Call the POST endpoint at startup
|
|
257
|
+
register_command,
|
|
258
|
+
# Log the end of the initial endpoint call
|
|
259
|
+
'echo "Initial call to the provider endpoint completed. Running cron job..."',
|
|
260
|
+
# Set up the cron job
|
|
261
|
+
f'echo "*/1 * * * * {register_command}" | crontab -',
|
|
262
|
+
# Start the cron service
|
|
263
|
+
'crond -f',
|
|
264
|
+
]
|
|
265
|
+
)
|
|
263
266
|
|
|
264
267
|
seto_agent_compose_data, _ = resolve_compose_file(
|
|
265
268
|
driver=driver,
|
|
269
|
+
image_prefix=args.image_prefix,
|
|
266
270
|
compose_data={
|
|
267
271
|
'services': {
|
|
268
272
|
'seto_agent': {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2024 Sébastien Demanou. All Rights Reserved.
|
|
1
|
+
# Copyright 2024-2025 Sébastien Demanou. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -24,6 +24,7 @@ def parse_volumes_args(args, driver: Driver) -> list[Volume]:
|
|
|
24
24
|
driver=driver,
|
|
25
25
|
stack=args.stack or args.project,
|
|
26
26
|
execute=lambda resolved_compose_data, volumes: all_volumes.extend(volumes),
|
|
27
|
+
image_prefix=args.image_prefix,
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
return all_volumes
|
|
@@ -18,6 +18,7 @@ import re
|
|
|
18
18
|
import subprocess
|
|
19
19
|
import sys
|
|
20
20
|
from collections.abc import Callable
|
|
21
|
+
from pathlib import Path
|
|
21
22
|
from typing import Any
|
|
22
23
|
from typing import Literal
|
|
23
24
|
from typing import TypedDict
|
|
@@ -56,10 +57,7 @@ def parse_volume_entry(entry: str, default_mode='rw') -> tuple[str, str, str]:
|
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
def parse_local_volumes(
|
|
59
|
-
stack: str,
|
|
60
|
-
*,
|
|
61
|
-
service_name: str,
|
|
62
|
-
service: Service,
|
|
60
|
+
stack: str, *, service_name: str, service: Service, image_prefix: str
|
|
63
61
|
) -> None:
|
|
64
62
|
local_volumes = []
|
|
65
63
|
|
|
@@ -89,7 +87,7 @@ def parse_local_volumes(
|
|
|
89
87
|
service_dockerfile = '\n'.join(service_dockerfile_definition)
|
|
90
88
|
service_dockerfile = resolve_env_vars(service_dockerfile)
|
|
91
89
|
|
|
92
|
-
service['image'] = f'
|
|
90
|
+
service['image'] = f'{image_prefix}{image_name}:{image_version}'
|
|
93
91
|
service['build'] = {
|
|
94
92
|
'context': '.',
|
|
95
93
|
'dockerfile': service_dockerfile_file,
|
|
@@ -162,6 +160,7 @@ def resolve_compose_file(
|
|
|
162
160
|
compose_data: dict,
|
|
163
161
|
inject: bool = False,
|
|
164
162
|
mode: list[ResolveMode] | None = None,
|
|
163
|
+
image_prefix: str,
|
|
165
164
|
) -> tuple[dict, list]:
|
|
166
165
|
mode_value = mode or ['swarm']
|
|
167
166
|
updated_compose_data = compose_data.copy()
|
|
@@ -185,7 +184,9 @@ def resolve_compose_file(
|
|
|
185
184
|
|
|
186
185
|
service_deploy_labels.update(service_labels)
|
|
187
186
|
parse_stack_values(service_deploy_labels)
|
|
188
|
-
parse_local_volumes(
|
|
187
|
+
parse_local_volumes(
|
|
188
|
+
driver.stack_id, service_name=service_name, service=service, image_prefix=image_prefix
|
|
189
|
+
)
|
|
189
190
|
|
|
190
191
|
parse_volumes(
|
|
191
192
|
driver=driver,
|
|
@@ -242,7 +243,7 @@ def parse_service_configs(
|
|
|
242
243
|
config_name = re.sub(
|
|
243
244
|
r'_{2,}',
|
|
244
245
|
'_',
|
|
245
|
-
f
|
|
246
|
+
f'{service_name}_{source.replace("/", "_").replace(".", "_")}'.replace('-', '_'),
|
|
246
247
|
)
|
|
247
248
|
|
|
248
249
|
if inject:
|
|
@@ -272,6 +273,19 @@ def parse_service_configs(
|
|
|
272
273
|
service['volumes'] = new_volumes
|
|
273
274
|
|
|
274
275
|
|
|
276
|
+
def load_env_file(env_file_path: str) -> None:
|
|
277
|
+
"""Load environment variables from a .env file into os.environ."""
|
|
278
|
+
if os.path.exists(env_file_path):
|
|
279
|
+
with open(env_file_path, encoding='utf-8') as file:
|
|
280
|
+
for line in file:
|
|
281
|
+
line = line.strip()
|
|
282
|
+
if line and not line.startswith('#') and '=' in line:
|
|
283
|
+
key, value = line.split('=', 1)
|
|
284
|
+
# Remove quotes if present
|
|
285
|
+
value = value.strip('"\'')
|
|
286
|
+
os.environ[key.strip()] = value
|
|
287
|
+
|
|
288
|
+
|
|
275
289
|
def resolve_env_vars(content: str) -> str:
|
|
276
290
|
output = subprocess.run(
|
|
277
291
|
['envsubst'],
|
|
@@ -280,21 +294,24 @@ def resolve_env_vars(content: str) -> str:
|
|
|
280
294
|
capture_output=True,
|
|
281
295
|
check=True,
|
|
282
296
|
)
|
|
283
|
-
|
|
284
297
|
return output.stdout
|
|
285
298
|
|
|
286
299
|
|
|
287
300
|
def parse_compose_file(compose_file: str, resolve_vars=False) -> tuple[dict, str]:
|
|
288
301
|
compose_file = os.path.realpath(compose_file)
|
|
289
302
|
|
|
303
|
+
# Auto-load corresponding .env file if it exists
|
|
304
|
+
compose_path = Path(compose_file)
|
|
305
|
+
env_file_path = compose_path.parent / f'{compose_path.stem}.env'
|
|
306
|
+
load_env_file(str(env_file_path))
|
|
307
|
+
|
|
290
308
|
with open(compose_file, encoding='utf-8') as file:
|
|
291
309
|
compose_content = file.read()
|
|
292
310
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
compose_data = yaml.safe_load(compose_content)
|
|
311
|
+
if resolve_vars:
|
|
312
|
+
compose_content = resolve_env_vars(compose_content)
|
|
297
313
|
|
|
314
|
+
compose_data = yaml.safe_load(compose_content)
|
|
298
315
|
return compose_data, compose_file # type: ignore
|
|
299
316
|
|
|
300
317
|
|
|
@@ -305,6 +322,7 @@ def parse_services(
|
|
|
305
322
|
execute: Callable[[dict, list], None] | None = None,
|
|
306
323
|
mode: list[ResolveMode] | None = None,
|
|
307
324
|
inject: bool = False,
|
|
325
|
+
image_prefix: str,
|
|
308
326
|
) -> tuple[list, list]:
|
|
309
327
|
services_files = glob.glob(os.path.join(stack, '*.yaml'))
|
|
310
328
|
output_resolved_compose_data = []
|
|
@@ -332,6 +350,7 @@ def parse_services(
|
|
|
332
350
|
compose_data=compose_data,
|
|
333
351
|
inject=inject,
|
|
334
352
|
mode=[x_mode],
|
|
353
|
+
image_prefix=image_prefix,
|
|
335
354
|
)
|
|
336
355
|
|
|
337
356
|
output_resolved_compose_data.append(resolved_compose_data)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|