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.
Files changed (28) hide show
  1. {seto-3.2.0 → seto-3.4.0}/PKG-INFO +1 -1
  2. {seto-3.2.0 → seto-3.4.0}/pyproject.toml +2 -1
  3. {seto-3.2.0 → seto-3.4.0}/seto/__main__.py +3 -0
  4. {seto-3.2.0 → seto-3.4.0}/seto/commands/config.py +4 -3
  5. {seto-3.2.0 → seto-3.4.0}/seto/commands/deploy.py +25 -21
  6. {seto-3.2.0 → seto-3.4.0}/seto/core/command.py +2 -1
  7. {seto-3.2.0 → seto-3.4.0}/seto/core/parser.py +31 -12
  8. {seto-3.2.0 → seto-3.4.0}/LICENSE +0 -0
  9. {seto-3.2.0 → seto-3.4.0}/LICENSE_HEADER.txt +0 -0
  10. {seto-3.2.0 → seto-3.4.0}/README.md +0 -0
  11. {seto-3.2.0 → seto-3.4.0}/seto/__init__.py +0 -0
  12. {seto-3.2.0 → seto-3.4.0}/seto/commands/down.py +0 -0
  13. {seto-3.2.0 → seto-3.4.0}/seto/commands/mount.py +0 -0
  14. {seto-3.2.0 → seto-3.4.0}/seto/commands/setup.py +0 -0
  15. {seto-3.2.0 → seto-3.4.0}/seto/commands/umount.py +0 -0
  16. {seto-3.2.0 → seto-3.4.0}/seto/commands/volumes.py +0 -0
  17. {seto-3.2.0 → seto-3.4.0}/seto/core/dns.py +0 -0
  18. {seto-3.2.0 → seto-3.4.0}/seto/core/docker.py +0 -0
  19. {seto-3.2.0 → seto-3.4.0}/seto/core/driver.py +0 -0
  20. {seto-3.2.0 → seto-3.4.0}/seto/core/network.py +0 -0
  21. {seto-3.2.0 → seto-3.4.0}/seto/core/permissions.py +0 -0
  22. {seto-3.2.0 → seto-3.4.0}/seto/core/shell.py +0 -0
  23. {seto-3.2.0 → seto-3.4.0}/seto/core/swarm.py +0 -0
  24. {seto-3.2.0 → seto-3.4.0}/seto/core/traefik.py +0 -0
  25. {seto-3.2.0 → seto-3.4.0}/seto/core/volume.py +0 -0
  26. {seto-3.2.0 → seto-3.4.0}/seto/drivers/nfs.py +0 -0
  27. {seto-3.2.0 → seto-3.4.0}/seto/shells/local.py +0 -0
  28. {seto-3.2.0 → seto-3.4.0}/seto/shells/remote.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: seto
3
- Version: 3.2.0
3
+ Version: 3.4.0
4
4
  Summary: A Docker Swarm Deployment Manager
5
5
  License: Apache 2.0
6
6
  Keywords: docker,swarm,manager
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "seto"
7
- version = "3.2.0"
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(f'WARN: Service "{service_name}" has no defined port. Skipped from Traefik HTTP Provider')
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
- f'echo "Registering service {driver.stack_id}..."',
244
- 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',
245
- ])
246
-
247
- entrypoint = ' && '.join([
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
- # Start the cron service
261
- 'crond -f',
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'demsking/{image_name}:{image_version}'
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(driver.stack_id, service_name=service_name, service=service)
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"{service_name}_{source.replace('/', '_').replace('.', '_')}".replace('-', '_'),
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
- if resolve_vars:
294
- compose_content = resolve_env_vars(compose_content)
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