seto 2.5.0__py3-none-any.whl
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/__init__.py +14 -0
- seto/__main__.py +224 -0
- seto/commands/config.py +97 -0
- seto/commands/deploy.py +332 -0
- seto/commands/down.py +51 -0
- seto/commands/mount.py +24 -0
- seto/commands/setup.py +68 -0
- seto/commands/umount.py +24 -0
- seto/commands/volumes.py +27 -0
- seto/core/command.py +29 -0
- seto/core/dns.py +28 -0
- seto/core/docker.py +176 -0
- seto/core/driver.py +186 -0
- seto/core/network.py +69 -0
- seto/core/parser.py +333 -0
- seto/core/permissions.py +49 -0
- seto/core/shell.py +227 -0
- seto/core/swarm.py +14 -0
- seto/core/traefik.py +90 -0
- seto/core/volume.py +23 -0
- seto/drivers/gluster.py +146 -0
- seto/drivers/nfs.py +125 -0
- seto/shells/local.py +70 -0
- seto/shells/remote.py +86 -0
- seto-2.5.0.dist-info/LICENSE +201 -0
- seto-2.5.0.dist-info/LICENSE_HEADER.txt +14 -0
- seto-2.5.0.dist-info/METADATA +266 -0
- seto-2.5.0.dist-info/RECORD +30 -0
- seto-2.5.0.dist-info/WHEEL +4 -0
- seto-2.5.0.dist-info/entry_points.txt +3 -0
seto/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
# ==============================================================================
|
seto/__main__.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
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 argparse
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
from paramiko.ssh_exception import AuthenticationException
|
|
20
|
+
|
|
21
|
+
from .commands.config import execute_config_command
|
|
22
|
+
from .commands.deploy import execute_deploy_command
|
|
23
|
+
from .commands.down import execute_down_command
|
|
24
|
+
from .commands.mount import execute_mount_volumes_command
|
|
25
|
+
from .commands.setup import execute_setup_command
|
|
26
|
+
from .commands.setup import print_ssh_copy_id_commands
|
|
27
|
+
from .commands.umount import execute_umount_volumes_command
|
|
28
|
+
from .commands.volumes import execute_create_volumes_command
|
|
29
|
+
from .core.shell import Setting
|
|
30
|
+
from .drivers.gluster import GlusterDriver
|
|
31
|
+
from .drivers.nfs import NFSDriver
|
|
32
|
+
from .shells.local import LocalShell
|
|
33
|
+
from .shells.remote import RemoteShell
|
|
34
|
+
|
|
35
|
+
DriverType = type[str]
|
|
36
|
+
ShellConnectionString = type[str]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
driver_shemes = ['nfs://', 'glusterfs://']
|
|
40
|
+
driver_examples = [f'{sheme}username:password@hostname' for sheme in driver_shemes]
|
|
41
|
+
DRIVER_EXAMPLES_STR = ' or '.join(driver_examples)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def manager_type(value: str) -> tuple[DriverType, ShellConnectionString]:
|
|
45
|
+
if not any(value.startswith(scheme) for scheme in driver_shemes):
|
|
46
|
+
raise argparse.ArgumentError('manager', f'{value} is not a valid manager ({DRIVER_EXAMPLES_STR})')
|
|
47
|
+
|
|
48
|
+
return value.split('://')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def replica_type(value: str) -> list[Setting]:
|
|
52
|
+
connection_strings = value.split(' ')
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
return [
|
|
56
|
+
Setting.from_connection_string(item.strip()) for item in connection_strings
|
|
57
|
+
]
|
|
58
|
+
except Exception as exception:
|
|
59
|
+
raise argparse.ArgumentError('replica', f'{value} is not a replica value (username:password@hostname)') from exception
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def nodes_type(value: str) -> list[Setting]:
|
|
63
|
+
connection_strings = value.split(' ')
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
return [
|
|
67
|
+
Setting.from_connection_string(item.strip()) for item in connection_strings
|
|
68
|
+
]
|
|
69
|
+
except Exception as exception:
|
|
70
|
+
raise argparse.ArgumentError('nodes', f'{value} is not a nodes value (username:password@hostname)') from exception
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main() -> None:
|
|
74
|
+
parser = argparse.ArgumentParser('seto', description='')
|
|
75
|
+
subparsers = parser.add_subparsers()
|
|
76
|
+
|
|
77
|
+
parser.add_argument('--project', required=True, help='Project name')
|
|
78
|
+
parser.add_argument('--stack', help='Stack name')
|
|
79
|
+
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
'--manager',
|
|
82
|
+
type=manager_type,
|
|
83
|
+
required=True,
|
|
84
|
+
help=f'Storage manager URI to use. Can be {DRIVER_EXAMPLES_STR}',
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
'--key',
|
|
89
|
+
default=os.path.expanduser('~/.ssh/id_rsa'),
|
|
90
|
+
help='Path to the SSH private key file',
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
'--debug',
|
|
95
|
+
action='store_true',
|
|
96
|
+
help='Enable debug mode',
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
#
|
|
100
|
+
# Setup command
|
|
101
|
+
setup_parser = subparsers.add_parser('setup', description='Setup manager and replica nodes')
|
|
102
|
+
setup_parser.set_defaults(func=execute_setup_command)
|
|
103
|
+
|
|
104
|
+
setup_parser.add_argument(
|
|
105
|
+
'--replica',
|
|
106
|
+
type=replica_type,
|
|
107
|
+
nargs='+',
|
|
108
|
+
required=True,
|
|
109
|
+
help='Replica nodes to setup',
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
setup_parser.add_argument(
|
|
113
|
+
'--clients',
|
|
114
|
+
type=nodes_type,
|
|
115
|
+
nargs='+',
|
|
116
|
+
required=True,
|
|
117
|
+
help='Client nodes to setup',
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
setup_parser.add_argument(
|
|
121
|
+
'--force',
|
|
122
|
+
action='store_true',
|
|
123
|
+
help='Force setup tasks',
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
#
|
|
127
|
+
# Create Volumes command
|
|
128
|
+
create_volumes_parser = subparsers.add_parser('create-volumes', description='Create and sync shared volumes')
|
|
129
|
+
create_volumes_parser.set_defaults(func=execute_create_volumes_command)
|
|
130
|
+
create_volumes_parser.add_argument(
|
|
131
|
+
'--replica',
|
|
132
|
+
type=replica_type,
|
|
133
|
+
nargs='+',
|
|
134
|
+
required=True,
|
|
135
|
+
help='Nodes where volumes will be created',
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
create_volumes_parser.add_argument(
|
|
139
|
+
'--force',
|
|
140
|
+
action='store_true',
|
|
141
|
+
help='Force volumes data sync',
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
#
|
|
145
|
+
# Mount Volumes command
|
|
146
|
+
mount_volumes_parser = subparsers.add_parser('mount-volumes', description='Mount shared volumes')
|
|
147
|
+
mount_volumes_parser.set_defaults(func=execute_mount_volumes_command)
|
|
148
|
+
mount_volumes_parser.add_argument(
|
|
149
|
+
'--clients',
|
|
150
|
+
type=nodes_type,
|
|
151
|
+
nargs='+',
|
|
152
|
+
required=True,
|
|
153
|
+
help='Client nodes on which volumes will be mounted',
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
#
|
|
157
|
+
# Unmount Volumes command
|
|
158
|
+
umount_volumes_parser = subparsers.add_parser('unmount-volumes', description='Unmount shared volumes')
|
|
159
|
+
umount_volumes_parser.set_defaults(func=execute_umount_volumes_command)
|
|
160
|
+
umount_volumes_parser.add_argument(
|
|
161
|
+
'--clients',
|
|
162
|
+
type=nodes_type,
|
|
163
|
+
nargs='+',
|
|
164
|
+
required=True,
|
|
165
|
+
help='Client nodes on which volumes will be unmounted',
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
#
|
|
169
|
+
# Config command
|
|
170
|
+
config_parser = subparsers.add_parser('config', description='Parse, resolve and render compose file in canonical format')
|
|
171
|
+
config_parser.set_defaults(func=execute_config_command)
|
|
172
|
+
|
|
173
|
+
config_parser.add_argument(
|
|
174
|
+
'--compose',
|
|
175
|
+
action='store_true',
|
|
176
|
+
help='Resolve for Docker Compose',
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
#
|
|
180
|
+
# Deploy command
|
|
181
|
+
deploy_parser = subparsers.add_parser('deploy', description='Deploy a new stack or update an existing stack')
|
|
182
|
+
deploy_parser.set_defaults(func=execute_deploy_command)
|
|
183
|
+
|
|
184
|
+
#
|
|
185
|
+
# Down command
|
|
186
|
+
down_parser = subparsers.add_parser('down', description='Stop and remove containers, networks')
|
|
187
|
+
down_parser.set_defaults(func=execute_down_command)
|
|
188
|
+
|
|
189
|
+
#
|
|
190
|
+
# Parsing
|
|
191
|
+
try:
|
|
192
|
+
args = parser.parse_args()
|
|
193
|
+
except (Exception, argparse.ArgumentError) as exception:
|
|
194
|
+
print(exception)
|
|
195
|
+
sys.exit(40)
|
|
196
|
+
|
|
197
|
+
driver_name, connection_string = args.manager
|
|
198
|
+
setting = Setting.from_connection_string(connection_string)
|
|
199
|
+
create_shell = LocalShell if setting.local else RemoteShell
|
|
200
|
+
create_driver = GlusterDriver if driver_name == 'gluster' else NFSDriver
|
|
201
|
+
shell = create_shell(setting, args.key)
|
|
202
|
+
driver = create_driver(args.stack, project=args.project, shell=shell)
|
|
203
|
+
|
|
204
|
+
# Call the function associated with the selected subcommand
|
|
205
|
+
if hasattr(args, 'func'):
|
|
206
|
+
if args.debug:
|
|
207
|
+
args.func(args, driver)
|
|
208
|
+
else:
|
|
209
|
+
try:
|
|
210
|
+
args.func(args, driver)
|
|
211
|
+
except AuthenticationException as exception:
|
|
212
|
+
print(f'\nError: {exception}')
|
|
213
|
+
print_ssh_copy_id_commands(args)
|
|
214
|
+
sys.exit(41)
|
|
215
|
+
except Exception as exception:
|
|
216
|
+
print(exception)
|
|
217
|
+
sys.exit(50)
|
|
218
|
+
else:
|
|
219
|
+
parser.print_help()
|
|
220
|
+
|
|
221
|
+
sys.exit(0)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
main()
|
seto/commands/config.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Copyright 2024-2025 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 collections.abc import Callable
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
from ..core.driver import Driver
|
|
20
|
+
from ..core.network import get_global_external_networks
|
|
21
|
+
from ..core.network import resolve_networks
|
|
22
|
+
from ..core.parser import parse_services
|
|
23
|
+
from ..core.shell import Shell
|
|
24
|
+
from ..core.volume import Volume
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve(
|
|
28
|
+
args,
|
|
29
|
+
driver: Driver,
|
|
30
|
+
*,
|
|
31
|
+
inject: bool = False,
|
|
32
|
+
execute: Callable[[dict, str], None] | None = None,
|
|
33
|
+
) -> dict:
|
|
34
|
+
config_networks = get_global_external_networks()
|
|
35
|
+
global_networks = resolve_networks(args.project)
|
|
36
|
+
|
|
37
|
+
for network_name, network_definition in global_networks.items():
|
|
38
|
+
config_networks[network_name] = {
|
|
39
|
+
'name': network_definition['name'],
|
|
40
|
+
'external': True,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
compose = {
|
|
44
|
+
'x-placement': None,
|
|
45
|
+
'x-placement-hostname': None,
|
|
46
|
+
'configs': {},
|
|
47
|
+
'networks': config_networks,
|
|
48
|
+
'volumes': {},
|
|
49
|
+
'secrets': {},
|
|
50
|
+
'services': {},
|
|
51
|
+
}
|
|
52
|
+
|
|
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)
|
|
56
|
+
networks_ = resolved_compose_data.get('networks', {})
|
|
57
|
+
services = resolved_compose_data.get('services', {})
|
|
58
|
+
volumes = resolved_compose_data.get('volumes', {})
|
|
59
|
+
configs = resolved_compose_data.get('configs', {})
|
|
60
|
+
secrets = resolved_compose_data.get('secrets', {})
|
|
61
|
+
|
|
62
|
+
resolved_compose_data['networks'] = {**config_networks, **networks_}
|
|
63
|
+
compose['x-placement-hostname'] = placement_hostname
|
|
64
|
+
compose['x-placement'] = placement
|
|
65
|
+
compose['networks'].update(networks_)
|
|
66
|
+
compose['services'].update(services)
|
|
67
|
+
compose['volumes'].update(volumes)
|
|
68
|
+
compose['configs'].update(configs)
|
|
69
|
+
compose['secrets'].update(secrets)
|
|
70
|
+
|
|
71
|
+
if args.compose and not placement and not placement_hostname:
|
|
72
|
+
raise ValueError('Missing required x-placement or x-placement-hostname field')
|
|
73
|
+
|
|
74
|
+
if execute:
|
|
75
|
+
execute(resolved_compose_data, placement_hostname or placement)
|
|
76
|
+
|
|
77
|
+
parse_services(
|
|
78
|
+
driver=driver,
|
|
79
|
+
stack=args.stack or args.project,
|
|
80
|
+
execute=parse,
|
|
81
|
+
inject=inject,
|
|
82
|
+
mode=['compose'] if args.compose else ['swarm'],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return compose
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def execute_config_command(args, driver: Driver) -> None:
|
|
89
|
+
compose = resolve(args, driver)
|
|
90
|
+
compose_output = yaml.dump(compose)
|
|
91
|
+
|
|
92
|
+
if args.compose:
|
|
93
|
+
command = f'docker compose -p {driver.stack_id} -f - config'
|
|
94
|
+
else:
|
|
95
|
+
command = 'docker stack config -c -'
|
|
96
|
+
|
|
97
|
+
Shell.pipe_exec(command, pipe_input=compose_output)
|
seto/commands/deploy.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Copyright 2024-2025 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 json
|
|
16
|
+
import os
|
|
17
|
+
import random
|
|
18
|
+
import re
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from docker import DockerClient
|
|
22
|
+
|
|
23
|
+
from ..core.dns import resolve_hostname
|
|
24
|
+
from ..core.docker import DockerCompose
|
|
25
|
+
from ..core.docker import DockerSwarm
|
|
26
|
+
from ..core.driver import Driver
|
|
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
|
|
32
|
+
from .config import resolve
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Define the regular expression pattern to match {{ .Node.Hostname }} with optional spaces
|
|
36
|
+
NODE_HOSTNAME_RE = r'\{\{\s*\.Node\.Hostname\s*\}\}'
|
|
37
|
+
HTTP_PROVIDER_SERVICENAME = 'seto-http-provider'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_service_vars(entries: dict[str, Any], hostname: str) -> None:
|
|
41
|
+
for key, value in entries.items():
|
|
42
|
+
if isinstance(value, str):
|
|
43
|
+
entries[key] = re.sub(NODE_HOSTNAME_RE, hostname, value)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def pick_label_value(labels: dict[str, Any], name: str) -> Any | str:
|
|
47
|
+
for label, value in labels.items():
|
|
48
|
+
if label.endswith(name):
|
|
49
|
+
del labels[label]
|
|
50
|
+
return value
|
|
51
|
+
return ''
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_compose_config(
|
|
55
|
+
args,
|
|
56
|
+
driver: Driver,
|
|
57
|
+
client: DockerClient,
|
|
58
|
+
networks_list: list[str],
|
|
59
|
+
swarm_config: dict,
|
|
60
|
+
*,
|
|
61
|
+
compose_config: dict,
|
|
62
|
+
placement: str,
|
|
63
|
+
composes: list[DockerCompose],
|
|
64
|
+
traefik_http_provider_routers: dict,
|
|
65
|
+
traefik_http_provider_services: dict,
|
|
66
|
+
traefik_http_provider_middlewares: dict,
|
|
67
|
+
) -> None:
|
|
68
|
+
if compose_config['services']:
|
|
69
|
+
compose = DockerCompose(
|
|
70
|
+
client=client,
|
|
71
|
+
driver=driver,
|
|
72
|
+
config=compose_config,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
composes.append(compose)
|
|
76
|
+
|
|
77
|
+
for service_name, service in compose_config['services'].items():
|
|
78
|
+
service_bridge_name = f'{service_name}_bridge'
|
|
79
|
+
service_environment = service.get('environment', {})
|
|
80
|
+
service_labels = service.get('labels', {})
|
|
81
|
+
service_networks = service.get('networks', [])
|
|
82
|
+
service_ports = service.get('ports', [])
|
|
83
|
+
service_deploy = {
|
|
84
|
+
'placement': {
|
|
85
|
+
'constraints': [
|
|
86
|
+
f'node.labels.{placement}' if '=' in placement else f'node.hostname == {placement}',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Use a shadow service to make sure all networks are created on all node
|
|
92
|
+
swarm_config['services'][service_bridge_name] = {
|
|
93
|
+
'image': 'traefik/whoami',
|
|
94
|
+
'networks': service_networks,
|
|
95
|
+
'deploy': service_deploy,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if compose.node_hostname:
|
|
99
|
+
parse_service_vars(service_labels, compose.node_hostname)
|
|
100
|
+
parse_service_vars(service_environment, compose.node_hostname)
|
|
101
|
+
|
|
102
|
+
service['ports'] = service_ports
|
|
103
|
+
service_traefik_rule = pick_label_value(service_labels, '.rule')
|
|
104
|
+
service_traefik_middlewares = pick_label_value(service_labels, '.middlewares')
|
|
105
|
+
service_traefik_port = pick_label_value(service_labels, '.loadbalancer.server.port')
|
|
106
|
+
service_traefik_entryPoints = pick_label_value(service_labels, '.entryPoints')
|
|
107
|
+
service_traefik_tls_certresolver = pick_label_value(service_labels, '.tls.certresolver')
|
|
108
|
+
service_traefik_service = pick_label_value(service_labels, '.service') or service_name
|
|
109
|
+
published_port = random.randint(53100, 64200)
|
|
110
|
+
|
|
111
|
+
if not service_traefik_port:
|
|
112
|
+
print(f'WARN: Service "{service_name}" has no defined port. Skipped from Traefik HTTP Provider')
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
service_ports.append(f'{published_port}:{service_traefik_port}')
|
|
116
|
+
|
|
117
|
+
traefik_http_provider_routers[service_traefik_service] = {
|
|
118
|
+
'entryPoints': [service_traefik_entryPoints],
|
|
119
|
+
'service': service_traefik_service,
|
|
120
|
+
'rule': service_traefik_rule,
|
|
121
|
+
'middlewares': [item for item in service_traefik_middlewares.split(',') if item],
|
|
122
|
+
'tls': {
|
|
123
|
+
'certresolver': service_traefik_tls_certresolver,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
node_ip = resolve_hostname(compose.node_hostname)
|
|
128
|
+
traefik_http_provider_services[service_traefik_service] = {
|
|
129
|
+
'loadBalancer': {
|
|
130
|
+
'servers': [
|
|
131
|
+
{
|
|
132
|
+
'url': f'http://{node_ip}:{published_port}',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
traefik_http_provider_middlewares.update(
|
|
139
|
+
convert_middlewares_to_dict(service_labels),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def deploy_seto_stack(args, driver: Driver, replica: list[Setting]) -> None:
|
|
144
|
+
# Temporary rewrite driver config
|
|
145
|
+
driver.project = 'seto'
|
|
146
|
+
driver.stack = None
|
|
147
|
+
|
|
148
|
+
# Building seto config
|
|
149
|
+
client = DockerClient.from_env()
|
|
150
|
+
config_networks = resolve_networks(args.project)
|
|
151
|
+
|
|
152
|
+
config_networks.update(GLOBAL_NETWORKS)
|
|
153
|
+
|
|
154
|
+
print('Configuring seto-http-provider...')
|
|
155
|
+
traefik_http_provider_data_path = '/data'
|
|
156
|
+
internal_stack = {
|
|
157
|
+
'networks': config_networks,
|
|
158
|
+
'services': {
|
|
159
|
+
HTTP_PROVIDER_SERVICENAME: {
|
|
160
|
+
'image': 'demsking/traefik-http-provider',
|
|
161
|
+
'networks': list(config_networks.keys()),
|
|
162
|
+
'environment': [
|
|
163
|
+
'WORKER=1',
|
|
164
|
+
'EXPIRATION_MINUTES=10',
|
|
165
|
+
f'DATA_PATH={traefik_http_provider_data_path}',
|
|
166
|
+
],
|
|
167
|
+
'volumes-nfs': {
|
|
168
|
+
f'data:{traefik_http_provider_data_path}',
|
|
169
|
+
},
|
|
170
|
+
'deploy': {
|
|
171
|
+
'mode': 'global',
|
|
172
|
+
'labels': {
|
|
173
|
+
'traefik.discovery.enable': True,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Resolving compose local volumes
|
|
181
|
+
resolved_compose_data, volumes = resolve_compose_file(
|
|
182
|
+
driver=driver,
|
|
183
|
+
compose_data=internal_stack,
|
|
184
|
+
inject=True,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
print('Creating seto volumes...')
|
|
188
|
+
driver.create_volumes(replica=replica, volumes=volumes, force=args.force)
|
|
189
|
+
|
|
190
|
+
print('Deploying seto services...')
|
|
191
|
+
swarm = DockerSwarm(
|
|
192
|
+
client=client,
|
|
193
|
+
driver=driver,
|
|
194
|
+
config=resolved_compose_data,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
swarm.info()
|
|
198
|
+
swarm.deploy()
|
|
199
|
+
|
|
200
|
+
# Restore initial driver config
|
|
201
|
+
driver.project = args.project
|
|
202
|
+
driver.stack = args.stack
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def execute_deploy_command(args, driver: Driver) -> None:
|
|
206
|
+
client = DockerClient.from_env()
|
|
207
|
+
|
|
208
|
+
# Docker Swarm
|
|
209
|
+
print(f'Resolving {driver.stack_id} services...')
|
|
210
|
+
setattr(args, 'compose', False)
|
|
211
|
+
swarm_config = resolve(args, driver)
|
|
212
|
+
|
|
213
|
+
swarm = DockerSwarm(
|
|
214
|
+
client=client,
|
|
215
|
+
driver=driver,
|
|
216
|
+
config=swarm_config,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Docker Compose
|
|
220
|
+
setattr(args, 'compose', True)
|
|
221
|
+
networks_list = list(swarm_config['networks'].keys())
|
|
222
|
+
composes_items: list[DockerCompose] = []
|
|
223
|
+
|
|
224
|
+
bridges_path = 'bridges'
|
|
225
|
+
traefik_http_provider_filename = os.path.join(bridges_path, 'traefik-http-provider.json')
|
|
226
|
+
traefik_http_provider_target = '/traefik/config.json'
|
|
227
|
+
traefik_http_provider_routers = {}
|
|
228
|
+
traefik_http_provider_services = {}
|
|
229
|
+
traefik_http_provider_middlewares = {}
|
|
230
|
+
traefik_http_provider = {
|
|
231
|
+
'http': {
|
|
232
|
+
'routers': traefik_http_provider_routers,
|
|
233
|
+
'services': traefik_http_provider_services,
|
|
234
|
+
'middlewares': traefik_http_provider_middlewares,
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
register_command = ' && '.join([
|
|
239
|
+
f'echo "Registering service {driver.stack_id}..."',
|
|
240
|
+
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',
|
|
241
|
+
])
|
|
242
|
+
|
|
243
|
+
entrypoint = ' && '.join([
|
|
244
|
+
# Log the start of the initial endpoint call
|
|
245
|
+
'echo "Starting initial call to the provider endpoint..."',
|
|
246
|
+
|
|
247
|
+
# Call the POST endpoint at startup
|
|
248
|
+
register_command,
|
|
249
|
+
|
|
250
|
+
# Log the end of the initial endpoint call
|
|
251
|
+
'echo "Initial call to the provider endpoint completed. Running cron job..."',
|
|
252
|
+
|
|
253
|
+
# Set up the cron job
|
|
254
|
+
f'echo "*/1 * * * * {register_command}" | crontab -',
|
|
255
|
+
|
|
256
|
+
# Start the cron service
|
|
257
|
+
'crond -f',
|
|
258
|
+
])
|
|
259
|
+
|
|
260
|
+
seto_agent_compose_data, _ = resolve_compose_file(
|
|
261
|
+
driver=driver,
|
|
262
|
+
compose_data={
|
|
263
|
+
'services': {
|
|
264
|
+
'seto_agent': {
|
|
265
|
+
'image': 'curlimages/curl:latest',
|
|
266
|
+
'user': 'root', # Ensure it runs as root to avoid issue `crontab: must be suid to work properly`
|
|
267
|
+
'networks': networks_list,
|
|
268
|
+
'entrypoint': f"sh -c '{entrypoint}'",
|
|
269
|
+
'volumes-image': [
|
|
270
|
+
f'{traefik_http_provider_filename}:{traefik_http_provider_target}',
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
swarm_config['services'].update(seto_agent_compose_data['services'])
|
|
278
|
+
|
|
279
|
+
resolve(
|
|
280
|
+
args,
|
|
281
|
+
driver,
|
|
282
|
+
inject=True,
|
|
283
|
+
execute=lambda config, placement: parse_compose_config(
|
|
284
|
+
args,
|
|
285
|
+
driver,
|
|
286
|
+
client,
|
|
287
|
+
networks_list,
|
|
288
|
+
swarm_config,
|
|
289
|
+
compose_config=config,
|
|
290
|
+
placement=placement,
|
|
291
|
+
composes=composes_items,
|
|
292
|
+
traefik_http_provider_routers=traefik_http_provider_routers,
|
|
293
|
+
traefik_http_provider_services=traefik_http_provider_services,
|
|
294
|
+
traefik_http_provider_middlewares=traefik_http_provider_middlewares,
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if not os.path.exists(bridges_path):
|
|
299
|
+
os.mkdir(bridges_path)
|
|
300
|
+
|
|
301
|
+
with open(traefik_http_provider_filename, 'w', encoding='utf-8') as file:
|
|
302
|
+
file.write(json.dumps(traefik_http_provider, indent=' '))
|
|
303
|
+
|
|
304
|
+
print(f'Building {driver.stack_id} swarm images...')
|
|
305
|
+
# swarm.info()
|
|
306
|
+
swarm.build()
|
|
307
|
+
|
|
308
|
+
print(f'Pushing {driver.stack_id} images...')
|
|
309
|
+
swarm.push()
|
|
310
|
+
|
|
311
|
+
# print(f'Creating {driver.stack_id} volumes...')
|
|
312
|
+
# driver.create_volumes(replica=replica, volumes=volumes, force=args.force)
|
|
313
|
+
|
|
314
|
+
print(f'Deploying {driver.stack_id} swarm environment...')
|
|
315
|
+
swarm.deploy()
|
|
316
|
+
swarm.ps()
|
|
317
|
+
|
|
318
|
+
if composes_items:
|
|
319
|
+
print(f'Building {driver.stack_id} compose images...')
|
|
320
|
+
for compose in composes_items:
|
|
321
|
+
# compose.info()
|
|
322
|
+
compose.build()
|
|
323
|
+
|
|
324
|
+
print(f'Pulling {driver.stack_id} compose images...')
|
|
325
|
+
for compose in composes_items:
|
|
326
|
+
compose.pull()
|
|
327
|
+
|
|
328
|
+
print(f'Deploying {driver.stack_id} compose environment...')
|
|
329
|
+
for compose in composes_items:
|
|
330
|
+
compose.deploy()
|
|
331
|
+
compose.ps()
|
|
332
|
+
compose.logs()
|