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.
- {seto-2.0.4 → seto-2.2.0}/PKG-INFO +1 -1
- {seto-2.0.4 → seto-2.2.0}/pyproject.toml +1 -1
- {seto-2.0.4 → seto-2.2.0}/seto/__main__.py +1 -9
- {seto-2.0.4 → seto-2.2.0}/seto/commands/config.py +4 -14
- {seto-2.0.4 → seto-2.2.0}/seto/commands/deploy.py +121 -71
- {seto-2.0.4 → seto-2.2.0}/seto/commands/down.py +3 -3
- {seto-2.0.4 → seto-2.2.0}/seto/commands/setup.py +2 -1
- {seto-2.0.4 → seto-2.2.0}/seto/core/docker.py +19 -15
- {seto-2.0.4 → seto-2.2.0}/seto/core/driver.py +8 -2
- seto-2.2.0/seto/core/network.py +69 -0
- {seto-2.0.4 → seto-2.2.0}/seto/core/parser.py +5 -33
- {seto-2.0.4 → seto-2.2.0}/seto/core/shell.py +4 -3
- seto-2.2.0/seto/core/traefik.py +90 -0
- {seto-2.0.4 → seto-2.2.0}/LICENSE +0 -0
- {seto-2.0.4 → seto-2.2.0}/LICENSE_HEADER.txt +0 -0
- {seto-2.0.4 → seto-2.2.0}/README.md +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/__init__.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/commands/mount.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/commands/umount.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/commands/volumes.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/core/command.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/core/dns.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/core/permissions.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/core/swarm.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/core/volume.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/drivers/gluster.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/drivers/nfs.py +0 -0
- {seto-2.0.4 → seto-2.2.0}/seto/shells/local.py +0 -0
- {seto-2.0.4 → seto-2.2.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 = "2.0
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
153
|
+
print('Configuring seto-http-provider...')
|
|
159
154
|
internal_stack = {
|
|
160
155
|
'networks': config_networks,
|
|
161
156
|
'services': {
|
|
162
|
-
|
|
163
|
-
'image': 'traefik
|
|
157
|
+
HTTP_PROVIDER_SERVICENAME: {
|
|
158
|
+
'image': 'demsking/traefik-http-provider',
|
|
164
159
|
'networks': list(config_networks.keys()),
|
|
165
|
-
'
|
|
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=
|
|
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.
|
|
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
|
-
|
|
214
|
-
'
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
302
|
+
print(f'Building {driver.stack_id} swarm images...')
|
|
259
303
|
# swarm.info()
|
|
260
304
|
swarm.build()
|
|
261
305
|
|
|
262
|
-
print(f'
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
27
|
+
print(f'Stoping {driver.stack_id} environment...')
|
|
28
28
|
setattr(args, 'compose', False)
|
|
29
29
|
|
|
30
30
|
swarm = DockerSwarm(
|
|
31
|
-
stack=driver.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
150
|
-
Shell.exec(f'docker stack services {self.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
74
|
-
|
|
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.
|
|
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
|
|
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, _
|
|
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
|