seto 2.0.4__tar.gz → 2.2.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 (29) hide show
  1. {seto-2.0.4 → seto-2.2.0}/PKG-INFO +1 -1
  2. {seto-2.0.4 → seto-2.2.0}/pyproject.toml +1 -1
  3. {seto-2.0.4 → seto-2.2.0}/seto/__main__.py +1 -9
  4. {seto-2.0.4 → seto-2.2.0}/seto/commands/config.py +4 -14
  5. {seto-2.0.4 → seto-2.2.0}/seto/commands/deploy.py +121 -71
  6. {seto-2.0.4 → seto-2.2.0}/seto/commands/down.py +3 -3
  7. {seto-2.0.4 → seto-2.2.0}/seto/commands/setup.py +2 -1
  8. {seto-2.0.4 → seto-2.2.0}/seto/core/docker.py +19 -15
  9. {seto-2.0.4 → seto-2.2.0}/seto/core/driver.py +8 -2
  10. seto-2.2.0/seto/core/network.py +69 -0
  11. {seto-2.0.4 → seto-2.2.0}/seto/core/parser.py +5 -33
  12. {seto-2.0.4 → seto-2.2.0}/seto/core/shell.py +4 -3
  13. seto-2.2.0/seto/core/traefik.py +90 -0
  14. {seto-2.0.4 → seto-2.2.0}/LICENSE +0 -0
  15. {seto-2.0.4 → seto-2.2.0}/LICENSE_HEADER.txt +0 -0
  16. {seto-2.0.4 → seto-2.2.0}/README.md +0 -0
  17. {seto-2.0.4 → seto-2.2.0}/seto/__init__.py +0 -0
  18. {seto-2.0.4 → seto-2.2.0}/seto/commands/mount.py +0 -0
  19. {seto-2.0.4 → seto-2.2.0}/seto/commands/umount.py +0 -0
  20. {seto-2.0.4 → seto-2.2.0}/seto/commands/volumes.py +0 -0
  21. {seto-2.0.4 → seto-2.2.0}/seto/core/command.py +0 -0
  22. {seto-2.0.4 → seto-2.2.0}/seto/core/dns.py +0 -0
  23. {seto-2.0.4 → seto-2.2.0}/seto/core/permissions.py +0 -0
  24. {seto-2.0.4 → seto-2.2.0}/seto/core/swarm.py +0 -0
  25. {seto-2.0.4 → seto-2.2.0}/seto/core/volume.py +0 -0
  26. {seto-2.0.4 → seto-2.2.0}/seto/drivers/gluster.py +0 -0
  27. {seto-2.0.4 → seto-2.2.0}/seto/drivers/nfs.py +0 -0
  28. {seto-2.0.4 → seto-2.2.0}/seto/shells/local.py +0 -0
  29. {seto-2.0.4 → seto-2.2.0}/seto/shells/remote.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: seto
3
- Version: 2.0.4
3
+ Version: 2.2.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 = "2.0.4"
7
+ version = "2.2.0"
8
8
  description = "A Docker Swarm Deployment Manager"
9
9
  keywords = ["docker", "swarm", "manager"]
10
10
  authors = ["Sébastien Demanou <demsking@gmail.com>"]
@@ -199,15 +199,7 @@ def main() -> None:
199
199
  create_shell = LocalShell if setting.local else RemoteShell
200
200
  create_driver = GlusterDriver if driver_name == 'gluster' else NFSDriver
201
201
  shell = create_shell(setting, args.key)
202
- stack_name = f'{args.project}_{args.stack}' if args.stack else args.project
203
- brickname = f'{args.project}/{args.stack}' if args.stack else args.project
204
-
205
- driver = create_driver(
206
- stack_name,
207
- project=args.project,
208
- brickname=brickname,
209
- shell=shell,
210
- )
202
+ driver = create_driver(args.stack, project=args.project, shell=shell)
211
203
 
212
204
  # Call the function associated with the selected subcommand
213
205
  if hasattr(args, 'func'):
@@ -1,4 +1,3 @@
1
- from docker.api import network
2
1
  # Copyright 2024 Sébastien Demanou. All Rights Reserved.
3
2
  #
4
3
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,8 +17,9 @@ from collections.abc import Callable
18
17
  import yaml
19
18
 
20
19
  from ..core.driver import Driver
20
+ from ..core.network import get_global_external_networks
21
+ from ..core.network import resolve_networks
21
22
  from ..core.parser import parse_services
22
- from ..core.parser import resolve_networks
23
23
  from ..core.shell import Shell
24
24
  from ..core.volume import Volume
25
25
 
@@ -31,7 +31,7 @@ def resolve(
31
31
  inject: bool = False,
32
32
  execute: Callable[[dict, str], None] | None = None,
33
33
  ) -> dict:
34
- config_networks = {}
34
+ config_networks = get_global_external_networks()
35
35
  global_networks = resolve_networks(args.project)
36
36
 
37
37
  for network_name, network_definition in global_networks.items():
@@ -40,16 +40,6 @@ def resolve(
40
40
  'external': True,
41
41
  }
42
42
 
43
- config_networks['cloud-public'] = {
44
- 'name': 'seto-cloud-public',
45
- 'external': True,
46
- }
47
-
48
- config_networks['cloud-edge'] = {
49
- 'name': 'seto-cloud-edge',
50
- 'external': True,
51
- }
52
-
53
43
  compose = {
54
44
  'x-placement': None,
55
45
  'configs': {},
@@ -97,7 +87,7 @@ def execute_config_command(args, driver: Driver) -> None:
97
87
  compose_output = yaml.dump(compose)
98
88
 
99
89
  if args.compose:
100
- command = f'docker compose -p {driver.stack} -f - config'
90
+ command = f'docker compose -p {driver.stack_id} -f - config'
101
91
  else:
102
92
  command = 'docker stack config -c -'
103
93
 
@@ -14,6 +14,7 @@
14
14
  # ==============================================================================
15
15
  import json
16
16
  import os
17
+ import random
17
18
  import re
18
19
  from typing import Any
19
20
 
@@ -23,11 +24,17 @@ from ..core.dns import resolve_hostname
23
24
  from ..core.docker import DockerCompose
24
25
  from ..core.docker import DockerSwarm
25
26
  from ..core.driver import Driver
26
- from ..core.parser import resolve_networks
27
+ from ..core.network import GLOBAL_NETWORKS
28
+ from ..core.network import resolve_networks
29
+ from ..core.parser import resolve_compose_file
30
+ from ..core.shell import Setting
31
+ from ..core.traefik import convert_middlewares_to_dict
27
32
  from .config import resolve
28
33
 
34
+
29
35
  # Define the regular expression pattern to match {{ .Node.Hostname }} with optional spaces
30
36
  NODE_HOSTNAME_RE = r'\{\{\s*\.Node\.Hostname\s*\}\}'
37
+ HTTP_PROVIDER_SERVICENAME = 'seto-http-provider'
31
38
 
32
39
 
33
40
  def parse_service_vars(entries: dict[str, Any], hostname: str) -> None:
@@ -36,15 +43,15 @@ def parse_service_vars(entries: dict[str, Any], hostname: str) -> None:
36
43
  entries[key] = re.sub(NODE_HOSTNAME_RE, hostname, value)
37
44
 
38
45
 
39
- def get_label_value(labels: dict[str, Any], name: str) -> Any | None:
46
+ def pick_label_value(labels: dict[str, Any], name: str) -> Any | None:
40
47
  for label, value in labels.items():
41
48
  if label.endswith(name):
49
+ del labels[label]
42
50
  return value
43
51
  return None
44
52
 
45
53
 
46
54
  def parse_compose_config(
47
- stack: str,
48
55
  args,
49
56
  driver: Driver,
50
57
  client: DockerClient,
@@ -56,10 +63,10 @@ def parse_compose_config(
56
63
  composes: list[DockerCompose],
57
64
  traefik_http_provider_routers: dict,
58
65
  traefik_http_provider_services: dict,
66
+ traefik_http_provider_middlewares: dict,
59
67
  ) -> None:
60
68
  if compose_config['services']:
61
69
  compose = DockerCompose(
62
- stack=stack,
63
70
  client=client,
64
71
  driver=driver,
65
72
  config=compose_config,
@@ -74,9 +81,6 @@ def parse_compose_config(
74
81
  service_networks = service.get('networks', [])
75
82
  service_ports = service.get('ports', [])
76
83
  service_deploy = {
77
- 'labels': [
78
- 'traefik.discovery.enable=false',
79
- ],
80
84
  'placement': {
81
85
  'constraints': [
82
86
  f'node.labels.{placement}',
@@ -94,33 +98,24 @@ def parse_compose_config(
94
98
  parse_service_vars(service_labels, compose.node_hostname)
95
99
  parse_service_vars(service_environment, compose.node_hostname)
96
100
 
97
- if service_ports:
98
- service_traefik_rule = get_label_value(service_labels, '.rule')
99
- service_traefik_middlewares = get_label_value(service_labels, '.middlewares')
100
- service_traefik_port = get_label_value(service_labels, '.loadbalancer.server.port')
101
- service_traefik_entryPoints = get_label_value(service_labels, '.entryPoints')
102
- service_traefik_tls_certresolver = get_label_value(service_labels, '.tls.certresolver')
103
- service_traefik_service = get_label_value(service_labels, '.service')
104
-
105
- if not service_traefik_port:
106
- raise ValueError(f'Service "{service_name}" error: port is missing')
107
-
108
- published_port = -1
109
-
110
- for entry in service_ports:
111
- source_port, target_port = entry.split(':')
101
+ service_traefik_port = pick_label_value(service_labels, '.loadbalancer.server.port')
112
102
 
113
- if int(target_port) == service_traefik_port:
114
- published_port = source_port
103
+ if service_traefik_port:
104
+ service['ports'] = service_ports
105
+ service_traefik_rule = pick_label_value(service_labels, '.rule')
106
+ service_traefik_middlewares = pick_label_value(service_labels, '.middlewares')
107
+ service_traefik_entryPoints = pick_label_value(service_labels, '.entryPoints')
108
+ service_traefik_tls_certresolver = pick_label_value(service_labels, '.tls.certresolver')
109
+ service_traefik_service = pick_label_value(service_labels, '.service')
110
+ published_port = random.randint(53100, 64200)
115
111
 
116
- if published_port == -1:
117
- raise ValueError(f'No exposed port for {service_name}:{service_traefik_port}')
112
+ service_ports.append(f'{published_port}:{service_traefik_port}')
118
113
 
119
114
  traefik_http_provider_routers[service_name] = {
120
115
  'entryPoints': [service_traefik_entryPoints],
121
116
  'service': service_traefik_service or service_name,
122
117
  'rule': service_traefik_rule,
123
- 'middlewares': service_traefik_middlewares,
118
+ 'middlewares': service_traefik_middlewares.split(','),
124
119
  'tls': {
125
120
  'certresolver': service_traefik_tls_certresolver,
126
121
  },
@@ -137,57 +132,83 @@ def parse_compose_config(
137
132
  },
138
133
  }
139
134
 
135
+ traefik_http_provider_middlewares.update(
136
+ convert_middlewares_to_dict(service_labels),
137
+ )
138
+ else:
139
+ print(f'No exposed port found for service "{service_name}"')
140
140
 
141
- def deploy_seto_stack(args, driver: Driver) -> None:
141
+
142
+ def deploy_seto_stack(args, driver: Driver, replica: list[Setting]) -> None:
143
+ # Temporary rewrite driver config
144
+ driver.project = 'seto'
145
+ driver.stack = None
146
+
147
+ # Building seto config
142
148
  client = DockerClient.from_env()
143
149
  config_networks = resolve_networks(args.project)
144
150
 
145
- config_networks.update({
146
- 'cloud-public': {
147
- 'name': 'seto-cloud-public',
148
- 'driver': 'overlay',
149
- 'attachable': True,
150
- },
151
- 'cloud-edge': {
152
- 'name': 'seto-cloud-edge',
153
- 'driver': 'overlay',
154
- 'attachable': True,
155
- },
156
- })
151
+ config_networks.update(GLOBAL_NETWORKS)
157
152
 
158
- print('Configuring ṣeto agents...')
153
+ print('Configuring seto-http-provider...')
159
154
  internal_stack = {
160
155
  'networks': config_networks,
161
156
  'services': {
162
- 'agent': {
163
- 'image': 'traefik/whoami',
157
+ HTTP_PROVIDER_SERVICENAME: {
158
+ 'image': 'demsking/traefik-http-provider',
164
159
  'networks': list(config_networks.keys()),
165
- 'deploy': {'mode': 'global'},
160
+ 'environment': [
161
+ 'WORKER=1',
162
+ 'EXPIRATION_MINUTES=10',
163
+ 'PROVIDERS_DATABASE=/data/providers.db',
164
+ ],
165
+ 'volumes-nfs': {
166
+ 'data:/data',
167
+ },
168
+ 'deploy': {
169
+ 'mode': 'global',
170
+ 'labels': {
171
+ 'traefik.discovery.enable=true',
172
+ },
173
+ },
166
174
  },
167
175
  },
168
176
  }
169
177
 
178
+ # Resolving compose local volumes
179
+ resolved_compose_data, volumes = resolve_compose_file(
180
+ driver=driver,
181
+ compose_data=internal_stack,
182
+ inject=True,
183
+ )
184
+
185
+ print('Creating seto volumes...')
186
+ driver.create_volumes(replica=replica, volumes=volumes, force=args.force)
187
+
188
+ print('Deploying seto services...')
170
189
  swarm = DockerSwarm(
171
- stack='seto',
172
190
  client=client,
173
191
  driver=driver,
174
- config=internal_stack,
192
+ config=resolved_compose_data,
175
193
  )
176
194
 
177
195
  swarm.info()
178
196
  swarm.deploy()
179
197
 
198
+ # Restore initial driver config
199
+ driver.project = args.project
200
+ driver.stack = args.stack
201
+
180
202
 
181
203
  def execute_deploy_command(args, driver: Driver) -> None:
182
204
  client = DockerClient.from_env()
183
205
 
184
206
  # Docker Swarm
185
- print(f'Resolving {driver.stack} services...')
207
+ print(f'Resolving {driver.stack_id} services...')
186
208
  setattr(args, 'compose', False)
187
209
  swarm_config = resolve(args, driver)
188
210
 
189
211
  swarm = DockerSwarm(
190
- stack=driver.stack,
191
212
  client=client,
192
213
  driver=driver,
193
214
  config=swarm_config,
@@ -199,43 +220,65 @@ def execute_deploy_command(args, driver: Driver) -> None:
199
220
  composes_items: list[DockerCompose] = []
200
221
 
201
222
  bridges_path = 'bridges'
202
- traefik_http_provider_name = 'traefik-http-provider'
203
223
  traefik_http_provider_filename = os.path.join(bridges_path, 'traefik-http-provider.json')
224
+ traefik_http_provider_target = '/traefik/config.json'
204
225
  traefik_http_provider_routers = {}
205
226
  traefik_http_provider_services = {}
227
+ traefik_http_provider_middlewares = {}
206
228
  traefik_http_provider = {
207
229
  'http': {
208
230
  'routers': traefik_http_provider_routers,
209
231
  'services': traefik_http_provider_services,
232
+ 'middlewares': traefik_http_provider_middlewares,
210
233
  },
211
234
  }
212
235
 
213
- swarm_config['configs'][traefik_http_provider_name] = {
214
- 'file': traefik_http_provider_filename,
215
- }
236
+ register_command = ' && '.join([
237
+ f'echo "Registering service {driver.stack_id}..."',
238
+ 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',
239
+ ])
240
+
241
+ entrypoint = ' && '.join([
242
+ # Log the start of the initial endpoint call
243
+ 'echo "Starting initial call to the provider endpoint..."',
244
+
245
+ # Call the POST endpoint at startup
246
+ register_command,
247
+
248
+ # Log the end of the initial endpoint call
249
+ 'echo "Initial call to the provider endpoint completed. Running cron job..."',
250
+
251
+ # Set up the cron job
252
+ f'echo "*/1 * * * * {register_command}" | crontab -',
253
+
254
+ # Start the cron service
255
+ 'crond -f',
256
+ ])
216
257
 
217
- swarm_config['services']['compose-provider'] = {
218
- 'image': 'httpd:alpine',
219
- 'networks': networks_list,
220
- 'configs': [
221
- {
222
- 'source': traefik_http_provider_name,
223
- 'target': '/usr/local/apache2/htdocs/bridge.json',
258
+ seto_agent_compose_data, _ = resolve_compose_file(
259
+ driver=driver,
260
+ compose_data={
261
+ 'services': {
262
+ 'seto_agent': {
263
+ 'image': 'curlimages/curl:latest',
264
+ 'user': 'root', # Ensure it runs as root to avoid issue `crontab: must be suid to work properly`
265
+ 'networks': networks_list,
266
+ 'entrypoint': f"sh -c '{entrypoint}'",
267
+ 'volumes-image': [
268
+ f'{traefik_http_provider_filename}:{traefik_http_provider_target}',
269
+ ],
270
+ },
224
271
  },
225
- ],
226
- 'deploy': {
227
- 'labels': [
228
- 'traefik.discovery.enable=false',
229
- ],
230
272
  },
231
- }
273
+ )
274
+
275
+ swarm_config['services'].update(seto_agent_compose_data['services'])
232
276
 
233
277
  resolve(
234
278
  args,
235
279
  driver,
236
280
  inject=True,
237
281
  execute=lambda config, placement: parse_compose_config(
238
- driver.stack,
239
282
  args,
240
283
  driver,
241
284
  client,
@@ -246,6 +289,7 @@ def execute_deploy_command(args, driver: Driver) -> None:
246
289
  composes=composes_items,
247
290
  traefik_http_provider_routers=traefik_http_provider_routers,
248
291
  traefik_http_provider_services=traefik_http_provider_services,
292
+ traefik_http_provider_middlewares=traefik_http_provider_middlewares,
249
293
  ),
250
294
  )
251
295
 
@@ -255,25 +299,31 @@ def execute_deploy_command(args, driver: Driver) -> None:
255
299
  with open(traefik_http_provider_filename, 'w', encoding='utf-8') as file:
256
300
  file.write(json.dumps(traefik_http_provider, indent=' '))
257
301
 
258
- print(f'Building {driver.stack} swarm images...')
302
+ print(f'Building {driver.stack_id} swarm images...')
259
303
  # swarm.info()
260
304
  swarm.build()
261
305
 
262
- print(f'Deploying {driver.stack} swarm environment...')
306
+ print(f'Pushing {driver.stack_id} images...')
307
+ swarm.push()
308
+
309
+ # print(f'Creating {driver.stack_id} volumes...')
310
+ # driver.create_volumes(replica=replica, volumes=volumes, force=args.force)
311
+
312
+ print(f'Deploying {driver.stack_id} swarm environment...')
263
313
  swarm.deploy()
264
314
  swarm.ps()
265
315
 
266
316
  if composes_items:
267
- print(f'Building {driver.stack} compose images...')
317
+ print(f'Building {driver.stack_id} compose images...')
268
318
  for compose in composes_items:
269
319
  # compose.info()
270
320
  compose.build()
271
321
 
272
- print(f'Pulling {driver.stack} compose images...')
322
+ print(f'Pulling {driver.stack_id} compose images...')
273
323
  for compose in composes_items:
274
324
  compose.pull()
275
325
 
276
- print(f'Deploying {driver.stack} compose environment...')
326
+ print(f'Deploying {driver.stack_id} compose environment...')
277
327
  for compose in composes_items:
278
328
  compose.deploy()
279
329
  compose.ps()
@@ -24,11 +24,11 @@ def execute_down_command(args, driver: Driver) -> None:
24
24
  client = DockerClient.from_env()
25
25
 
26
26
  # Docker Swarm
27
- print(f'Stoping {driver.stack} environment...')
27
+ print(f'Stoping {driver.stack_id} environment...')
28
28
  setattr(args, 'compose', False)
29
29
 
30
30
  swarm = DockerSwarm(
31
- stack=driver.stack,
31
+ stack=driver.stack_id,
32
32
  client=client,
33
33
  driver=driver,
34
34
  config=resolve(args, driver),
@@ -42,7 +42,7 @@ def execute_down_command(args, driver: Driver) -> None:
42
42
 
43
43
  if config['services']:
44
44
  compose = DockerCompose(
45
- stack=driver.stack,
45
+ stack=driver.stack_id,
46
46
  client=client,
47
47
  driver=driver,
48
48
  config=config,
@@ -24,10 +24,11 @@ def execute_setup_command(args, driver: Driver) -> None:
24
24
  clients: list[Setting] = [items[0] for items in args.clients]
25
25
 
26
26
  driver.connect()
27
+ deploy_seto_stack(args, driver, replica)
27
28
  generate_ssh_keys(args, driver)
28
29
  driver.setup_manager(replica, args.force)
29
30
  driver.setup_nodes(clients, args.force)
30
- deploy_seto_stack(args, driver)
31
+ driver.apply_manager_changes()
31
32
  driver.terminate()
32
33
 
33
34
 
@@ -26,12 +26,10 @@ from .shell import Shell
26
26
  class Docker:
27
27
  def __init__(
28
28
  self,
29
- stack: str,
30
29
  config: dict,
31
30
  driver: Driver,
32
31
  client: DockerClient,
33
32
  ) -> None:
34
- self.stack = stack
35
33
  self.driver = driver
36
34
  self.client = client
37
35
  self.config = config
@@ -48,7 +46,7 @@ class Docker:
48
46
  def external_networks(self) -> list[str]:
49
47
  return [
50
48
  item.attrs['Name'] for item in self.client.networks.list()
51
- if item.attrs['Name'].startswith(self.stack)
49
+ if item.attrs['Name'].startswith(self.driver.stack_id)
52
50
  ]
53
51
 
54
52
  @staticmethod
@@ -63,13 +61,19 @@ class Docker:
63
61
 
64
62
  def build(self) -> None:
65
63
  Shell.pipe_exec(
66
- command=f'docker compose -f - -p {self.stack} build --push --no-cache',
64
+ command=f'docker compose -f - -p {self.driver.stack_id} build --no-cache',
65
+ pipe_input=self.resolved_config,
66
+ )
67
+
68
+ def push(self) -> None:
69
+ Shell.pipe_exec(
70
+ command=f'docker compose -f - -p {self.driver.stack_id} push',
67
71
  pipe_input=self.resolved_config,
68
72
  )
69
73
 
70
74
  def pull(self) -> None:
71
75
  Shell.pipe_exec(
72
- command=f'docker compose -f - -p {self.stack} pull --policy=always',
76
+ command=f'docker compose -f - -p {self.driver.stack_id} pull --policy=always',
73
77
  pipe_input=self.resolved_config,
74
78
  )
75
79
 
@@ -109,20 +113,20 @@ class DockerCompose(Docker):
109
113
  return None
110
114
 
111
115
  def info(self) -> None:
112
- self._exec(f'docker compose -p {self.stack} -f - config')
116
+ self._exec(f'docker compose -p {self.driver.stack_id} -f - config')
113
117
 
114
118
  def deploy(self) -> None:
115
119
  print(f'Deploying on {self.node_hostname}...')
116
- self._exec(f'docker compose -p {self.stack} -f - up -d --remove-orphans')
120
+ self._exec(f'docker compose -p {self.driver.stack_id} -f - up -d --remove-orphans')
117
121
 
118
122
  def ps(self) -> None:
119
- self._exec(f'docker compose -p {self.stack} -f - ps')
123
+ self._exec(f'docker compose -p {self.driver.stack_id} -f - ps')
120
124
 
121
125
  def logs(self) -> None:
122
- self._exec(f'docker compose -p {self.stack} -f - logs')
126
+ self._exec(f'docker compose -p {self.driver.stack_id} -f - logs')
123
127
 
124
128
  def down(self) -> None:
125
- self._exec(f'docker compose -p {self.stack} -f - down')
129
+ self._exec(f'docker compose -p {self.driver.stack_id} -f - down')
126
130
 
127
131
  def _exec(self, command: str) -> None:
128
132
  Docker.remote_node_run(
@@ -141,17 +145,17 @@ class DockerSwarm(Docker):
141
145
 
142
146
  def deploy(self) -> None:
143
147
  Shell.pipe_exec(
144
- command=f'docker stack deploy --prune --detach=true --resolve-image=always -c - {self.stack}',
148
+ command=f'docker stack deploy --prune --detach=true --resolve-image=always -c - {self.driver.stack_id}',
145
149
  pipe_input=self.resolved_config,
146
150
  )
147
151
 
148
152
  def ps(self) -> None:
149
- # Shell.exec(f'docker stack ps --no-trunc - {self.stack}')
150
- Shell.exec(f'docker stack services {self.stack}')
153
+ # Shell.exec(f'docker stack ps --no-trunc - {self.driver.stack_id}')
154
+ Shell.exec(f'docker stack services {self.driver.stack_id}')
151
155
 
152
156
  def logs(self) -> None:
153
- # Shell.exec(f'docker stack service logs -f -c - {self.stack}')
157
+ # Shell.exec(f'docker stack service logs -f -c - {self.driver.stack_id}')
154
158
  pass
155
159
 
156
160
  def down(self) -> None:
157
- Shell.exec(f'docker stack rm {self.stack}')
161
+ Shell.exec(f'docker stack rm {self.driver.stack_id}')
@@ -28,14 +28,20 @@ class Driver:
28
28
  stack: str,
29
29
  *,
30
30
  project: str,
31
- brickname: str,
32
31
  shell: Shell,
33
32
  ) -> None:
34
33
  self.stack = stack
35
34
  self.project = project
36
- self.brickname = brickname
37
35
  self.shell = shell
38
36
 
37
+ @property
38
+ def brickname(self) -> str:
39
+ return f'{self.project}/{self.stack}' if self.stack else self.project
40
+
41
+ @property
42
+ def stack_id(self) -> str:
43
+ return f'{self.project}_{self.stack}' if self.stack else self.project
44
+
39
45
  @property
40
46
  def slug(self) -> str:
41
47
  raise NotImplementedError()
@@ -0,0 +1,69 @@
1
+ # Copyright 2024 Sébastien Demanou. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ import glob
16
+ import os
17
+
18
+ import yaml
19
+
20
+ GLOBAL_NETWORKS = {
21
+ 'seto-cloud-public': {
22
+ 'name': 'seto-cloud-public',
23
+ 'driver': 'overlay',
24
+ 'attachable': True,
25
+ },
26
+ 'seto-cloud-edge': {
27
+ 'name': 'seto-cloud-edge',
28
+ 'driver': 'overlay',
29
+ 'attachable': True,
30
+ },
31
+ 'seto-http-provider': {
32
+ 'name': 'seto-http-provider',
33
+ 'driver': 'overlay',
34
+ 'attachable': True,
35
+ },
36
+ }
37
+
38
+
39
+ def get_global_external_networks() -> dict:
40
+ return {
41
+ shortname: {
42
+ 'name': network['name'],
43
+ 'external': True,
44
+ } for shortname, network in GLOBAL_NETWORKS.items()
45
+ }
46
+
47
+
48
+ def resolve_networks(
49
+ project: str,
50
+ external_networks: list[str] | None = None,
51
+ ) -> dict:
52
+ networks_files = glob.glob(os.path.join('networks', '*.yaml'))
53
+ merged_data = {}
54
+
55
+ for network_file in networks_files:
56
+ with open(network_file, encoding='utf-8') as file:
57
+ network_key = os.path.splitext(os.path.basename(network_file))[0]
58
+ network_data = yaml.safe_load(file)
59
+ network_name = network_data.get('name', f'{project}_{network_key}')
60
+ merged_data[network_key] = network_data
61
+
62
+ if external_networks and network_key in external_networks:
63
+ network_data.clear()
64
+
65
+ network_data['external'] = True
66
+
67
+ network_data['name'] = network_name
68
+
69
+ return merged_data
@@ -70,9 +70,8 @@ def parse_local_volumes(
70
70
 
71
71
  for volume_entry in service_volumes_image:
72
72
  if isinstance(volume_entry, str):
73
- if volume_entry.startswith('./'):
74
- source, target, _ = parse_volume_entry(volume_entry)
75
- local_volumes.append((source, target))
73
+ source, target, _ = parse_volume_entry(volume_entry)
74
+ local_volumes.append((source, target))
76
75
 
77
76
  if len(local_volumes) > 0:
78
77
  _, image_version = tuple((service['image'].split(':') + ['latest'])[0:2])
@@ -156,7 +155,6 @@ def parse_volumes(
156
155
  def resolve_compose_file(
157
156
  driver: Driver,
158
157
  *,
159
- compose_dir: str,
160
158
  compose_data: dict,
161
159
  inject: bool = False,
162
160
  mode: list[ResolveMode] | None = None,
@@ -183,7 +181,7 @@ def resolve_compose_file(
183
181
 
184
182
  service_deploy_labels.update(service_labels)
185
183
  parse_stack_values(service_deploy_labels)
186
- parse_local_volumes(driver.stack, service_name=service_name, service=service)
184
+ parse_local_volumes(driver.stack_id, service_name=service_name, service=service)
187
185
 
188
186
  parse_volumes(
189
187
  driver=driver,
@@ -278,7 +276,6 @@ def resolve_env_vars(content: str) -> str:
278
276
 
279
277
  def parse_compose_file(compose_file: str, resolve_vars=False) -> tuple[dict, str, str]:
280
278
  compose_file = os.path.realpath(compose_file)
281
- compose_dir = os.path.dirname(compose_file)
282
279
 
283
280
  with open(compose_file, encoding='utf-8') as file:
284
281
  compose_content = file.read()
@@ -288,31 +285,7 @@ def parse_compose_file(compose_file: str, resolve_vars=False) -> tuple[dict, str
288
285
 
289
286
  compose_data = yaml.safe_load(compose_content)
290
287
 
291
- return compose_data, compose_file, compose_dir
292
-
293
-
294
- def resolve_networks(
295
- project: str,
296
- external_networks: list[str] | None = None,
297
- ) -> dict:
298
- networks_files = glob.glob(os.path.join('networks', '*.yaml'))
299
- merged_data = {}
300
-
301
- for network_file in networks_files:
302
- with open(network_file, encoding='utf-8') as file:
303
- network_key = os.path.splitext(os.path.basename(network_file))[0]
304
- network_data = yaml.safe_load(file)
305
- network_name = network_data.get('name', f'{project}_{network_key}')
306
- merged_data[network_key] = network_data
307
-
308
- if external_networks and network_key in external_networks:
309
- network_data.clear()
310
-
311
- network_data['external'] = True
312
-
313
- network_data['name'] = network_name
314
-
315
- return merged_data
288
+ return compose_data, compose_file
316
289
 
317
290
 
318
291
  def parse_services(
@@ -329,7 +302,7 @@ def parse_services(
329
302
 
330
303
  for service_file in services_files:
331
304
  resolve_vars = mode and 'compose' in mode
332
- compose_data, _, compose_dir = parse_compose_file(service_file, resolve_vars)
305
+ compose_data, _ = parse_compose_file(service_file, resolve_vars)
333
306
  x_mode = compose_data.get('x-mode', 'swarm')
334
307
 
335
308
  if x_mode not in ['compose', 'swarm']:
@@ -346,7 +319,6 @@ def parse_services(
346
319
  # resolve compose local volumes
347
320
  resolved_compose_data, volumes = resolve_compose_file(
348
321
  driver=driver,
349
- compose_dir=compose_dir,
350
322
  compose_data=compose_data,
351
323
  inject=inject,
352
324
  mode=[x_mode],
@@ -126,7 +126,7 @@ class Shell:
126
126
  return username in ouput
127
127
 
128
128
  @staticmethod
129
- def exec(command: str, *, stdout=True, **kwargs) -> str:
129
+ def exec(command: str, *, stdout=True, env_vars: dict | None = None, **kwargs) -> str:
130
130
  # Run docker stack deploy with the content piped to it
131
131
  process = subprocess.Popen(
132
132
  command,
@@ -135,6 +135,7 @@ class Shell:
135
135
  stderr=subprocess.PIPE,
136
136
  text=True,
137
137
  shell=True,
138
+ env={**(env_vars or {}), **dict(subprocess.os.environ)},
138
139
  )
139
140
 
140
141
  standard_output, standard_error = process.communicate(**kwargs)
@@ -150,9 +151,9 @@ class Shell:
150
151
  return standard_output
151
152
 
152
153
  @staticmethod
153
- def pipe_exec(command: str, *, stdout=True, pipe_input: str) -> str:
154
+ def pipe_exec(command: str, *, stdout=True, pipe_input: str, env_vars: dict | None = None) -> str:
154
155
  with StringIO(pipe_input) as pipe:
155
- return Shell.exec(command, stdout=stdout, input=pipe.read())
156
+ return Shell.exec(command, stdout=stdout, env_vars=env_vars, input=pipe.read())
156
157
 
157
158
  def install(self, package_name: str, *, user='nobody', group='nogroup') -> None:
158
159
  result = self.run(f'dpkg -l | grep {package_name}', quiet=True, stdout=False)
@@ -0,0 +1,90 @@
1
+ # Copyright 2024 Sébastien Demanou. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ from typing import Any
16
+
17
+
18
+ def deep_set(dct: dict[str, Any], keys: str, value: Any):
19
+ """
20
+ Helper function to set a nested dictionary value using a dot-separated key.
21
+ This handles array notation like 'rewrites[0]' and converts plural-named keys to arrays.
22
+
23
+ Args:
24
+ dct: The dictionary to modify.
25
+ keys: A dot-separated string representing the nested keys.
26
+ value: The value to set at the nested location.
27
+ """
28
+ keys_list = keys.replace('[', '.').replace(']', '').split('.')
29
+ current = dct
30
+
31
+ for index, key in enumerate(keys_list[:-1]):
32
+ if key.isdigit(): # Convert to list if the key is an index
33
+ key = int(key)
34
+ if not isinstance(current, list):
35
+ current = []
36
+
37
+ # Extend the list if the index doesn't exist yet
38
+ while len(current) <= key:
39
+ current.append({})
40
+ current = current[key]
41
+ else:
42
+ if isinstance(current, list): # Handle case where current is mistakenly a list
43
+ current = current[-1]
44
+ current = current.setdefault(key, {})
45
+
46
+ # Handle the last key separately
47
+ final_key = keys_list[-1]
48
+
49
+ # Convert plural-named parameters to arrays (e.g. 'middlewares', 'rewrites')
50
+ if final_key.endswith('s') and isinstance(value, str):
51
+ value = [item.strip() for item in value.split(',')]
52
+
53
+ if final_key.isdigit(): # Handle if the final key is an array index
54
+ final_key = int(final_key)
55
+ if not isinstance(current, list):
56
+ current = []
57
+ while len(current) <= final_key:
58
+ current.append({})
59
+ current[final_key] = value
60
+ else:
61
+ current[final_key] = value
62
+
63
+
64
+ def convert_middlewares_to_dict(labels: dict[str, str]) -> dict:
65
+ """
66
+ Convert all Traefik middleware labels to JSON format, handling arrays for plural-named keys
67
+ and deeply nested fields.
68
+
69
+ Args:
70
+ labels: A dictionary of Docker labels.
71
+
72
+ Returns:
73
+ A JSON string representing the middleware configurations.
74
+ """
75
+ middlewares = {}
76
+
77
+ for label, value in labels.items():
78
+ if label.startswith('traefik.http.middlewares.'):
79
+ parts = label.split('.')
80
+ if len(parts) > 3:
81
+ middleware_name = parts[3]
82
+ middleware_key = '.'.join(parts[4:])
83
+
84
+ if middleware_name not in middlewares:
85
+ middlewares[middleware_name] = {}
86
+
87
+ # Use the deep_set helper to handle nested keys
88
+ deep_set(middlewares[middleware_name], middleware_key, value)
89
+
90
+ return middlewares
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