seto 1.0.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 +216 -0
- seto/commands/config.py +102 -0
- seto/commands/deploy.py +238 -0
- seto/commands/down.py +51 -0
- seto/commands/mount.py +24 -0
- seto/commands/setup.py +65 -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 +163 -0
- seto/core/driver.py +171 -0
- seto/core/parser.py +380 -0
- seto/core/permissions.py +49 -0
- seto/core/shell.py +209 -0
- seto/core/swarm.py +14 -0
- seto/core/volume.py +23 -0
- seto/drivers/gluster.py +146 -0
- seto/drivers/nfs.py +125 -0
- seto/shells/local.py +59 -0
- seto/shells/remote.py +75 -0
- seto-1.0.0.dist-info/LICENSE +201 -0
- seto-1.0.0.dist-info/LICENSE_HEADER.txt +14 -0
- seto-1.0.0.dist-info/METADATA +265 -0
- seto-1.0.0.dist-info/RECORD +28 -0
- seto-1.0.0.dist-info/WHEEL +4 -0
- seto-1.0.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,216 @@
|
|
|
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('--stack', required=True, help='Stack name')
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
'--manager',
|
|
80
|
+
type=manager_type,
|
|
81
|
+
required=True,
|
|
82
|
+
help=f'Storage manager URI to use. Can be {DRIVER_EXAMPLES_STR}',
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
parser.add_argument('--key', default=os.path.expanduser('~/.ssh/id_rsa'), help='Path to the SSH private key file')
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
'--debug',
|
|
89
|
+
action='store_true',
|
|
90
|
+
help='Enable debug mode',
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
#
|
|
94
|
+
# Setup command
|
|
95
|
+
setup_parser = subparsers.add_parser('setup', description='Setup manager and replica nodes')
|
|
96
|
+
setup_parser.set_defaults(func=execute_setup_command)
|
|
97
|
+
|
|
98
|
+
setup_parser.add_argument(
|
|
99
|
+
'--replica',
|
|
100
|
+
type=replica_type,
|
|
101
|
+
nargs='+',
|
|
102
|
+
required=True,
|
|
103
|
+
help='Replica nodes to setup',
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
setup_parser.add_argument(
|
|
107
|
+
'--clients',
|
|
108
|
+
type=nodes_type,
|
|
109
|
+
nargs='+',
|
|
110
|
+
required=True,
|
|
111
|
+
help='Client nodes to setup',
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
setup_parser.add_argument(
|
|
115
|
+
'--force',
|
|
116
|
+
action='store_true',
|
|
117
|
+
help='Force setup tasks',
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
#
|
|
121
|
+
# Create Volumes command
|
|
122
|
+
create_volumes_parser = subparsers.add_parser('create-volumes', description='Create and sync shared volumes')
|
|
123
|
+
create_volumes_parser.set_defaults(func=execute_create_volumes_command)
|
|
124
|
+
create_volumes_parser.add_argument(
|
|
125
|
+
'--replica',
|
|
126
|
+
type=replica_type,
|
|
127
|
+
nargs='+',
|
|
128
|
+
required=True,
|
|
129
|
+
help='Nodes where volumes will be created',
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
create_volumes_parser.add_argument(
|
|
133
|
+
'--force',
|
|
134
|
+
action='store_true',
|
|
135
|
+
help='Force volumes data sync',
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
#
|
|
139
|
+
# Mount Volumes command
|
|
140
|
+
mount_volumes_parser = subparsers.add_parser('mount-volumes', description='Mount shared volumes')
|
|
141
|
+
mount_volumes_parser.set_defaults(func=execute_mount_volumes_command)
|
|
142
|
+
mount_volumes_parser.add_argument(
|
|
143
|
+
'--clients',
|
|
144
|
+
type=nodes_type,
|
|
145
|
+
nargs='+',
|
|
146
|
+
required=True,
|
|
147
|
+
help='Client nodes on which volumes will be mounted',
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
#
|
|
151
|
+
# Unmount Volumes command
|
|
152
|
+
umount_volumes_parser = subparsers.add_parser('unmount-volumes', description='Unmount shared volumes')
|
|
153
|
+
umount_volumes_parser.set_defaults(func=execute_umount_volumes_command)
|
|
154
|
+
umount_volumes_parser.add_argument(
|
|
155
|
+
'--clients',
|
|
156
|
+
type=nodes_type,
|
|
157
|
+
nargs='+',
|
|
158
|
+
required=True,
|
|
159
|
+
help='Client nodes on which volumes will be unmounted',
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
#
|
|
163
|
+
# Config command
|
|
164
|
+
config_parser = subparsers.add_parser('config', description='Parse, resolve and render compose file in canonical format')
|
|
165
|
+
config_parser.set_defaults(func=execute_config_command)
|
|
166
|
+
|
|
167
|
+
config_parser.add_argument(
|
|
168
|
+
'--compose',
|
|
169
|
+
action='store_true',
|
|
170
|
+
help='Resolve for Docker Compose',
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
#
|
|
174
|
+
# Deploy command
|
|
175
|
+
deploy_parser = subparsers.add_parser('deploy', description='Deploy a new stack or update an existing stack')
|
|
176
|
+
deploy_parser.set_defaults(func=execute_deploy_command)
|
|
177
|
+
|
|
178
|
+
#
|
|
179
|
+
# Down command
|
|
180
|
+
down_parser = subparsers.add_parser('down', description='Stop and remove containers, networks')
|
|
181
|
+
down_parser.set_defaults(func=execute_down_command)
|
|
182
|
+
|
|
183
|
+
#
|
|
184
|
+
# Parsing
|
|
185
|
+
try:
|
|
186
|
+
args = parser.parse_args()
|
|
187
|
+
except (Exception, argparse.ArgumentError) as exception:
|
|
188
|
+
print(exception)
|
|
189
|
+
sys.exit(40)
|
|
190
|
+
|
|
191
|
+
driver_name, connection_string = args.manager
|
|
192
|
+
setting = Setting.from_connection_string(connection_string)
|
|
193
|
+
create_shell = LocalShell if setting.local else RemoteShell
|
|
194
|
+
create_driver = GlusterDriver if driver_name == 'gluster' else NFSDriver
|
|
195
|
+
shell = create_shell(setting, args.key)
|
|
196
|
+
driver = create_driver(args.stack, shell)
|
|
197
|
+
|
|
198
|
+
# Call the function associated with the selected subcommand
|
|
199
|
+
if hasattr(args, 'func'):
|
|
200
|
+
if args.debug:
|
|
201
|
+
args.func(args, driver)
|
|
202
|
+
else:
|
|
203
|
+
try:
|
|
204
|
+
args.func(args, driver)
|
|
205
|
+
except AuthenticationException as exception:
|
|
206
|
+
print(f'\nError: {exception}')
|
|
207
|
+
print_ssh_copy_id_commands(args)
|
|
208
|
+
sys.exit(41)
|
|
209
|
+
except Exception as exception:
|
|
210
|
+
print(exception)
|
|
211
|
+
sys.exit(50)
|
|
212
|
+
else:
|
|
213
|
+
parser.print_help()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
main()
|
seto/commands/config.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
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 collections.abc import Callable
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
from ..core.driver import Driver
|
|
20
|
+
from ..core.parser import parse_services
|
|
21
|
+
from ..core.parser import resolve_networks
|
|
22
|
+
from ..core.shell import Shell
|
|
23
|
+
from ..core.volume import Volume
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve(
|
|
27
|
+
args,
|
|
28
|
+
driver: Driver,
|
|
29
|
+
*,
|
|
30
|
+
inject: bool = False,
|
|
31
|
+
execute: Callable[[dict, str], None] | None = None,
|
|
32
|
+
) -> dict:
|
|
33
|
+
config_networks = resolve_networks(args.stack)
|
|
34
|
+
|
|
35
|
+
config_networks['cloud-public'] = {
|
|
36
|
+
'name': 'seto-cloud-public',
|
|
37
|
+
'driver': 'overlay',
|
|
38
|
+
'attachable': True,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
config_networks['cloud-edge'] = {
|
|
42
|
+
'name': 'seto-cloud-edge',
|
|
43
|
+
'driver': 'overlay',
|
|
44
|
+
'attachable': True,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if args.compose:
|
|
48
|
+
external_networks_keys = config_networks.keys()
|
|
49
|
+
config_networks = resolve_networks(args.stack, external_networks_keys)
|
|
50
|
+
|
|
51
|
+
compose = {
|
|
52
|
+
'x-placement': None,
|
|
53
|
+
'configs': {},
|
|
54
|
+
'networks': config_networks,
|
|
55
|
+
'volumes': {},
|
|
56
|
+
'secrets': {},
|
|
57
|
+
'services': {},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def parse(resolved_compose_data: dict, volumes: list[Volume]):
|
|
61
|
+
placement = resolved_compose_data.get('x-placement', None)
|
|
62
|
+
networks_ = resolved_compose_data.get('networks', {})
|
|
63
|
+
services = resolved_compose_data.get('services', {})
|
|
64
|
+
volumes = resolved_compose_data.get('volumes', {})
|
|
65
|
+
configs = resolved_compose_data.get('configs', {})
|
|
66
|
+
secrets = resolved_compose_data.get('secrets', {})
|
|
67
|
+
|
|
68
|
+
resolved_compose_data['networks'] = {*config_networks, *networks_}
|
|
69
|
+
compose['x-placement'] = placement
|
|
70
|
+
compose['networks'].update(networks_)
|
|
71
|
+
compose['services'].update(services)
|
|
72
|
+
compose['volumes'].update(volumes)
|
|
73
|
+
compose['configs'].update(configs)
|
|
74
|
+
compose['secrets'].update(secrets)
|
|
75
|
+
|
|
76
|
+
if args.compose and not placement:
|
|
77
|
+
raise ValueError('Missing required x-placement field')
|
|
78
|
+
|
|
79
|
+
if execute:
|
|
80
|
+
execute(resolved_compose_data, placement)
|
|
81
|
+
|
|
82
|
+
parse_services(
|
|
83
|
+
driver=driver,
|
|
84
|
+
stack=args.stack,
|
|
85
|
+
execute=parse,
|
|
86
|
+
inject=inject,
|
|
87
|
+
mode=['compose'] if args.compose else ['swarm'],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return compose
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def execute_config_command(args, driver: Driver) -> None:
|
|
94
|
+
compose = resolve(args, driver)
|
|
95
|
+
compose_output = yaml.dump(compose)
|
|
96
|
+
|
|
97
|
+
if args.compose:
|
|
98
|
+
command = f'docker compose -p {args.stack} -f - config'
|
|
99
|
+
else:
|
|
100
|
+
command = 'docker stack config -c -'
|
|
101
|
+
|
|
102
|
+
Shell.pipe_exec(command, pipe_input=compose_output)
|
seto/commands/deploy.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
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 json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from docker import DockerClient
|
|
21
|
+
|
|
22
|
+
from ..core.dns import resolve_hostname
|
|
23
|
+
from ..core.docker import DockerCompose
|
|
24
|
+
from ..core.docker import DockerSwarm
|
|
25
|
+
from ..core.driver import Driver
|
|
26
|
+
from .config import resolve
|
|
27
|
+
|
|
28
|
+
# Define the regular expression pattern to match {{ .Node.Hostname }} with optional spaces
|
|
29
|
+
NODE_HOSTNAME_RE = r'\{\{\s*\.Node\.Hostname\s*\}\}'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_service_vars(entries: dict[str, Any], hostname: str) -> None:
|
|
33
|
+
for key, value in entries.items():
|
|
34
|
+
if isinstance(value, str):
|
|
35
|
+
entries[key] = re.sub(NODE_HOSTNAME_RE, hostname, value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_label_value(labels: dict[str, Any], name: str) -> Any | None:
|
|
39
|
+
for label, value in labels.items():
|
|
40
|
+
if label.endswith(name):
|
|
41
|
+
return value
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_compose_config(
|
|
46
|
+
args,
|
|
47
|
+
driver: Driver,
|
|
48
|
+
client: DockerClient,
|
|
49
|
+
networks_list: list[str],
|
|
50
|
+
swarm_config: dict,
|
|
51
|
+
*,
|
|
52
|
+
compose_config: dict,
|
|
53
|
+
placement: str,
|
|
54
|
+
composes: list[DockerCompose],
|
|
55
|
+
traefik_http_provider_routers: dict,
|
|
56
|
+
traefik_http_provider_services: dict,
|
|
57
|
+
) -> None:
|
|
58
|
+
if compose_config['services']:
|
|
59
|
+
compose = DockerCompose(
|
|
60
|
+
stack=args.stack,
|
|
61
|
+
client=client,
|
|
62
|
+
driver=driver,
|
|
63
|
+
config=compose_config,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
composes.append(compose)
|
|
67
|
+
|
|
68
|
+
for service_name, service in compose_config['services'].items():
|
|
69
|
+
service_bridge_name = f'{service_name}_bridge'
|
|
70
|
+
service_environment = service.get('environment', {})
|
|
71
|
+
service_labels = service.get('labels', {})
|
|
72
|
+
service_networks = service.get('networks', [])
|
|
73
|
+
service_ports = service.get('ports', [])
|
|
74
|
+
service_deploy = {
|
|
75
|
+
'labels': [
|
|
76
|
+
'traefik.discovery.enable=false',
|
|
77
|
+
],
|
|
78
|
+
'placement': {
|
|
79
|
+
'constraints': [
|
|
80
|
+
f'node.labels.{placement}',
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Use a shadow service to make sure all networks are created on all node
|
|
86
|
+
swarm_config['services'][service_bridge_name] = {
|
|
87
|
+
'image': 'traefik/whoami',
|
|
88
|
+
'networks': service_networks,
|
|
89
|
+
'deploy': service_deploy,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
parse_service_vars(service_labels, compose.node_hostname)
|
|
93
|
+
parse_service_vars(service_environment, compose.node_hostname)
|
|
94
|
+
|
|
95
|
+
if service_ports:
|
|
96
|
+
service_traefik_rule = get_label_value(service_labels, '.rule')
|
|
97
|
+
service_traefik_middlewares = get_label_value(service_labels, '.middlewares')
|
|
98
|
+
service_traefik_port = get_label_value(service_labels, '.loadbalancer.server.port')
|
|
99
|
+
service_traefik_entryPoints = get_label_value(service_labels, '.entryPoints')
|
|
100
|
+
service_traefik_tls_certresolver = get_label_value(service_labels, '.tls.certresolver')
|
|
101
|
+
service_traefik_service = get_label_value(service_labels, '.service')
|
|
102
|
+
|
|
103
|
+
if not service_traefik_port:
|
|
104
|
+
raise ValueError(f'Service "{service_name}" error: port is missing')
|
|
105
|
+
|
|
106
|
+
published_port = -1
|
|
107
|
+
|
|
108
|
+
for entry in service_ports:
|
|
109
|
+
source_port, target_port = entry.split(':')
|
|
110
|
+
|
|
111
|
+
if int(target_port) == service_traefik_port:
|
|
112
|
+
published_port = source_port
|
|
113
|
+
|
|
114
|
+
if published_port == -1:
|
|
115
|
+
raise ValueError(f'No exposed port for {service_name}:{service_traefik_port}')
|
|
116
|
+
|
|
117
|
+
traefik_http_provider_routers[service_name] = {
|
|
118
|
+
'entryPoints': [service_traefik_entryPoints],
|
|
119
|
+
'service': service_traefik_service or service_name,
|
|
120
|
+
'rule': service_traefik_rule,
|
|
121
|
+
'middlewares': service_traefik_middlewares,
|
|
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_name] = {
|
|
129
|
+
'loadBalancer': {
|
|
130
|
+
'servers': [
|
|
131
|
+
{
|
|
132
|
+
'url': f'http://{node_ip}:{published_port}',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def execute_deploy_command(args, driver: Driver) -> None:
|
|
140
|
+
client = DockerClient.from_env()
|
|
141
|
+
|
|
142
|
+
# Docker Swarm
|
|
143
|
+
print('Resolving services...')
|
|
144
|
+
setattr(args, 'compose', False)
|
|
145
|
+
swarm_config = resolve(args, driver)
|
|
146
|
+
swarm = DockerSwarm(
|
|
147
|
+
stack=args.stack,
|
|
148
|
+
client=client,
|
|
149
|
+
driver=driver,
|
|
150
|
+
config=swarm_config,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Docker Compose
|
|
154
|
+
setattr(args, 'compose', True)
|
|
155
|
+
networks_list = list(swarm_config['networks'].keys())
|
|
156
|
+
composes_items: list[DockerCompose] = []
|
|
157
|
+
|
|
158
|
+
bridges_path = 'bridges'
|
|
159
|
+
traefik_http_provider_name = 'traefik-http-provider'
|
|
160
|
+
traefik_http_provider_filename = os.path.join(bridges_path, 'traefik-http-provider.json')
|
|
161
|
+
traefik_http_provider_routers = {}
|
|
162
|
+
traefik_http_provider_services = {}
|
|
163
|
+
traefik_http_provider = {
|
|
164
|
+
'http': {
|
|
165
|
+
'routers': traefik_http_provider_routers,
|
|
166
|
+
'services': traefik_http_provider_services,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
swarm_config['configs'][traefik_http_provider_name] = {
|
|
171
|
+
'file': traefik_http_provider_filename,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
swarm_config['services']['compose-provider'] = {
|
|
175
|
+
'image': 'httpd:alpine',
|
|
176
|
+
'networks': networks_list,
|
|
177
|
+
'configs': [
|
|
178
|
+
{
|
|
179
|
+
'source': traefik_http_provider_name,
|
|
180
|
+
'target': '/usr/local/apache2/htdocs/bridge.json',
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
'deploy': {
|
|
184
|
+
'labels': [
|
|
185
|
+
'traefik.discovery.enable=false',
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
resolve(
|
|
191
|
+
args,
|
|
192
|
+
driver,
|
|
193
|
+
inject=True,
|
|
194
|
+
execute=lambda config, placement: parse_compose_config(
|
|
195
|
+
args,
|
|
196
|
+
driver,
|
|
197
|
+
client,
|
|
198
|
+
networks_list,
|
|
199
|
+
swarm_config,
|
|
200
|
+
compose_config=config,
|
|
201
|
+
placement=placement,
|
|
202
|
+
composes=composes_items,
|
|
203
|
+
traefik_http_provider_routers=traefik_http_provider_routers,
|
|
204
|
+
traefik_http_provider_services=traefik_http_provider_services,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if not os.path.exists(bridges_path):
|
|
209
|
+
os.mkdir(bridges_path)
|
|
210
|
+
|
|
211
|
+
with open(traefik_http_provider_filename, 'w', encoding='utf-8') as file:
|
|
212
|
+
file.write(json.dumps(traefik_http_provider, indent=' '))
|
|
213
|
+
|
|
214
|
+
print('Building swarm images...')
|
|
215
|
+
# swarm.info()
|
|
216
|
+
swarm.build()
|
|
217
|
+
swarm.push()
|
|
218
|
+
|
|
219
|
+
print('Deploying swarm environment...')
|
|
220
|
+
swarm.deploy()
|
|
221
|
+
swarm.ps()
|
|
222
|
+
|
|
223
|
+
if composes_items:
|
|
224
|
+
print('Building compose images...')
|
|
225
|
+
for compose in composes_items:
|
|
226
|
+
# compose.info()
|
|
227
|
+
compose.build()
|
|
228
|
+
compose.push()
|
|
229
|
+
|
|
230
|
+
print('Pulling compose images...')
|
|
231
|
+
for compose in composes_items:
|
|
232
|
+
compose.pull()
|
|
233
|
+
|
|
234
|
+
print('Deploying compose environment...')
|
|
235
|
+
for compose in composes_items:
|
|
236
|
+
compose.deploy()
|
|
237
|
+
compose.ps()
|
|
238
|
+
compose.logs()
|
seto/commands/down.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
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 docker import DockerClient
|
|
16
|
+
|
|
17
|
+
from ..core.docker import DockerCompose
|
|
18
|
+
from ..core.docker import DockerSwarm
|
|
19
|
+
from ..core.driver import Driver
|
|
20
|
+
from .config import resolve
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def execute_down_command(args, driver: Driver) -> None:
|
|
24
|
+
client = DockerClient.from_env()
|
|
25
|
+
|
|
26
|
+
# Docker Swarm
|
|
27
|
+
print('Stoping environment...')
|
|
28
|
+
setattr(args, 'compose', False)
|
|
29
|
+
|
|
30
|
+
swarm = DockerSwarm(
|
|
31
|
+
stack=args.stack,
|
|
32
|
+
client=client,
|
|
33
|
+
driver=driver,
|
|
34
|
+
config=resolve(args, driver),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
swarm.down()
|
|
38
|
+
|
|
39
|
+
# Docker Compose
|
|
40
|
+
setattr(args, 'compose', True)
|
|
41
|
+
config = resolve(args, driver, inject=True)
|
|
42
|
+
|
|
43
|
+
if config['services']:
|
|
44
|
+
compose = DockerCompose(
|
|
45
|
+
stack=args.stack,
|
|
46
|
+
client=client,
|
|
47
|
+
driver=driver,
|
|
48
|
+
config=config,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
compose.down()
|
seto/commands/mount.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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 ..core.command import parse_volumes_args
|
|
16
|
+
from ..core.driver import Driver
|
|
17
|
+
from ..core.shell import Setting
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def execute_mount_volumes_command(args, driver: Driver) -> None:
|
|
21
|
+
clients: list[Setting] = [items[0] for items in args.clients]
|
|
22
|
+
all_volumes = parse_volumes_args(args, driver)
|
|
23
|
+
|
|
24
|
+
driver.mount_volumes(clients=clients, volumes=all_volumes)
|