|
1
|
|
|
#!/usr/bin/env python |
|
2
|
|
|
# coding: utf-8 |
|
3
|
1 |
|
""" |
|
4
|
|
|
CLI Entry Point. |
|
5
|
|
|
|
|
6
|
|
|
From click, build stakkr. |
|
7
|
|
|
Give all options to manage services to be launched, stopped, etc. |
|
8
|
|
|
""" |
|
9
|
|
|
|
|
10
|
1 |
|
import sys |
|
11
|
1 |
|
import click |
|
12
|
1 |
|
from click.core import Context |
|
13
|
1 |
|
from stakkr.docker_actions import get_running_containers_names |
|
14
|
1 |
|
from stakkr.aliases import get_aliases |
|
15
|
|
|
|
|
16
|
|
|
|
|
17
|
1 |
|
@click.group(help="""Main CLI Tool that easily create / maintain |
|
18
|
|
|
a stack of services, for example for web development. |
|
19
|
|
|
|
|
20
|
|
|
Read the configuration file and setup the required services by |
|
21
|
|
|
linking and managing everything for you.""") |
|
22
|
1 |
|
@click.version_option('4.0.5') |
|
23
|
1 |
|
@click.option('--config', '-c', help='Set the configuration filename (stakkr.yml by default)') |
|
24
|
1 |
|
@click.option('--debug/--no-debug', '-d', default=False) |
|
25
|
1 |
|
@click.option('--verbose', '-v', is_flag=True) |
|
26
|
1 |
|
@click.pass_context |
|
27
|
1 |
|
def stakkr(ctx: Context, config=None, debug=False, verbose=True): |
|
28
|
|
|
"""Click group, set context and main object.""" |
|
29
|
1 |
|
from stakkr.actions import StakkrActions |
|
30
|
|
|
|
|
31
|
1 |
|
ctx.obj['CONFIG'] = config |
|
32
|
1 |
|
ctx.obj['DEBUG'] = debug |
|
33
|
1 |
|
ctx.obj['VERBOSE'] = verbose |
|
34
|
1 |
|
ctx.obj['STAKKR'] = StakkrActions(ctx.obj) |
|
35
|
|
|
|
|
36
|
|
|
|
|
37
|
1 |
|
@stakkr.command(help="""Enter a container to perform direct actions such as |
|
38
|
|
|
install packages, run commands, etc.""") |
|
39
|
1 |
|
@click.argument('container', required=True) |
|
40
|
1 |
|
@click.option('--user', '-u', help="User's name. Valid choices : www-data or root") |
|
41
|
1 |
|
@click.option('--tty/--no-tty', '-t/ ', is_flag=True, default=True, help="Use a TTY") |
|
42
|
1 |
|
@click.pass_context |
|
43
|
1 |
|
def console(ctx: Context, container: str, user: str, tty: bool): |
|
44
|
|
|
"""See command Help.""" |
|
45
|
1 |
|
ctx.obj['STAKKR'].init_project() |
|
46
|
1 |
|
ctx.obj['CTS'] = get_running_containers_names(ctx.obj['STAKKR'].project_name) |
|
47
|
1 |
|
if ctx.obj['CTS']: |
|
48
|
1 |
|
ct_choice = click.Choice(ctx.obj['CTS']) |
|
49
|
1 |
|
ct_choice.convert(container, None, ctx) |
|
50
|
|
|
|
|
51
|
1 |
|
ctx.obj['STAKKR'].console(container, _get_cmd_user(user, container), tty) |
|
52
|
|
|
|
|
53
|
|
|
|
|
54
|
1 |
|
@stakkr.command(help="""Execute a command into a container. |
|
55
|
|
|
|
|
56
|
|
|
Examples:\n |
|
57
|
|
|
- ``stakkr -v exec mysql mysqldump -p'$MYSQL_ROOT_PASSWORD' mydb > /tmp/backup.sql``\n |
|
58
|
|
|
- ``stakkr exec php php -v`` : Execute the php binary in the php container with option -v\n |
|
59
|
|
|
- ``stakkr exec apache service apache2 restart``\n |
|
60
|
|
|
""", name='exec', context_settings=dict(ignore_unknown_options=True, allow_interspersed_args=False)) |
|
61
|
1 |
|
@click.pass_context |
|
62
|
1 |
|
@click.option('--user', '-u', help="User's name. Be careful, each container have its own users.") |
|
63
|
1 |
|
@click.option('--tty/--no-tty', '-t/ ', is_flag=True, default=True, help="Use a TTY") |
|
64
|
1 |
|
@click.argument('container', required=True) |
|
65
|
1 |
|
@click.argument('command', required=True, nargs=-1, type=click.UNPROCESSED) |
|
66
|
1 |
|
def exec_cmd(ctx: Context, user: str, container: str, command: tuple, tty: bool): |
|
67
|
|
|
"""See command Help.""" |
|
68
|
1 |
|
ctx.obj['STAKKR'].init_project() |
|
69
|
1 |
|
ctx.obj['CTS'] = get_running_containers_names(ctx.obj['STAKKR'].project_name) |
|
70
|
1 |
|
if ctx.obj['CTS']: |
|
71
|
1 |
|
click.Choice(ctx.obj['CTS']).convert(container, None, ctx) |
|
72
|
|
|
|
|
73
|
1 |
|
ctx.obj['STAKKR'].exec_cmd(container, _get_cmd_user(user, container), command, tty) |
|
74
|
|
|
|
|
75
|
|
|
|
|
76
|
1 |
|
@stakkr.command(help="Restart all (or a single as CONTAINER) container(s)") |
|
77
|
1 |
|
@click.argument('container', required=False) |
|
78
|
1 |
|
@click.option('--pull', '-p', help="Force a pull of the latest images versions", is_flag=True) |
|
79
|
1 |
|
@click.option('--recreate', '-r', help="Recreate all containers", is_flag=True) |
|
80
|
1 |
|
@click.option('--proxy/--no-proxy', '-P', help="Restart the proxy", default=True) |
|
81
|
1 |
|
@click.pass_context |
|
82
|
1 |
|
def restart(ctx: Context, container: str, pull: bool, recreate: bool, proxy: bool): |
|
83
|
|
|
"""See command Help.""" |
|
84
|
1 |
|
print(click.style('[RESTARTING]', fg='green') + ' your stakkr services') |
|
85
|
1 |
|
try: |
|
86
|
1 |
|
ctx.invoke(stop, container=container, proxy=proxy) |
|
87
|
1 |
|
except Exception: |
|
|
|
|
|
|
88
|
1 |
|
pass |
|
89
|
|
|
|
|
90
|
1 |
|
ctx.invoke(start, container=container, pull=pull, recreate=recreate, proxy=proxy) |
|
91
|
|
|
|
|
92
|
|
|
|
|
93
|
1 |
|
@stakkr.command(help="""List available services available for stakkr.yml |
|
94
|
|
|
(with info if the service is enabled)""") |
|
95
|
1 |
|
@click.pass_context |
|
96
|
|
|
def services(ctx): |
|
97
|
|
|
"""See command Help.""" |
|
98
|
1 |
|
ctx.obj['STAKKR'].init_project() |
|
99
|
|
|
|
|
100
|
1 |
|
from stakkr.stakkr_compose import get_available_services |
|
101
|
|
|
|
|
102
|
1 |
|
print('Available services usable in stakkr.yml ', end='') |
|
103
|
1 |
|
print('({} = disabled) : '.format(click.style('✘', fg='red'))) |
|
104
|
|
|
|
|
105
|
1 |
|
svcs = ctx.obj['STAKKR'].get_config()['services'] |
|
106
|
1 |
|
enabled_svcs = [svc for svc, opts in svcs.items() if opts['enabled'] is True] |
|
107
|
1 |
|
available_svcs = get_available_services(ctx.obj['STAKKR'].project_dir) |
|
108
|
1 |
|
for available_svc in sorted(list(available_svcs.keys())): |
|
109
|
1 |
|
sign = click.style('✘', fg='red') |
|
110
|
1 |
|
if available_svc in enabled_svcs: |
|
111
|
1 |
|
version = svcs[available_svc]['version'] |
|
112
|
1 |
|
sign = click.style(str(version), fg='green') |
|
113
|
|
|
|
|
114
|
1 |
|
print(' - {} ({})'.format(available_svc, sign)) |
|
115
|
|
|
|
|
116
|
|
|
|
|
117
|
1 |
|
@stakkr.command(help="""Download a pack of services from github (see github) containing services. |
|
118
|
|
|
PACKAGE is the git url or package name. |
|
119
|
|
|
NAME is the directory name to clone the repo. |
|
120
|
|
|
""") |
|
121
|
1 |
|
@click.argument('package', required=True) |
|
122
|
1 |
|
@click.argument('name', required=False) |
|
123
|
1 |
|
@click.pass_context |
|
124
|
1 |
|
def services_add(ctx: Context, package: str, name: str): |
|
125
|
|
|
"""See command Help.""" |
|
126
|
1 |
|
from stakkr.services import install |
|
127
|
|
|
|
|
128
|
1 |
|
project_dir = _get_project_dir(ctx.obj['CONFIG']) |
|
129
|
1 |
|
services_dir = '{}/services'.format(project_dir) |
|
130
|
1 |
|
name = package if name is None else name |
|
131
|
1 |
|
success, message = install(services_dir, package, name) |
|
132
|
1 |
|
if success is False: |
|
133
|
1 |
|
click.echo(click.style(message, fg='red')) |
|
134
|
1 |
|
sys.exit(1) |
|
135
|
|
|
|
|
136
|
1 |
|
stdout_msg = 'Package "' + click.style(package, fg='green') + '" installed successfully' |
|
137
|
1 |
|
if message is not None: |
|
138
|
1 |
|
stdout_msg = click.style(message, fg='yellow') |
|
139
|
|
|
|
|
140
|
1 |
|
click.echo(stdout_msg) |
|
141
|
1 |
|
click.echo('Try ' + click.style('stakkr services', fg='green') + ' to see new available services') |
|
|
|
|
|
|
142
|
|
|
|
|
143
|
|
|
|
|
144
|
1 |
|
@stakkr.command(help="Update all services packs in services/") |
|
145
|
1 |
|
@click.pass_context |
|
146
|
|
|
def services_update(ctx): |
|
147
|
|
|
"""See command Help.""" |
|
148
|
1 |
|
from stakkr.services import update_all |
|
149
|
|
|
|
|
150
|
1 |
|
project_dir = _get_project_dir(ctx.obj['CONFIG']) |
|
151
|
1 |
|
services_dir = '{}/services'.format(project_dir) |
|
152
|
1 |
|
update_all(services_dir) |
|
153
|
1 |
|
print(click.style('Packages updated', fg='green')) |
|
154
|
|
|
|
|
155
|
|
|
|
|
156
|
1 |
|
@stakkr.command(help="Start all (or a single as CONTAINER) container(s) defined in compose.ini") |
|
157
|
1 |
|
@click.argument('container', required=False) |
|
158
|
1 |
|
@click.option('--pull', '-p', help="Force a pull of the latest images versions", is_flag=True) |
|
159
|
1 |
|
@click.option('--recreate', '-r', help="Recreate all containers", is_flag=True) |
|
160
|
1 |
|
@click.option('--proxy/--no-proxy', '-P', help="Start proxy", default=True) |
|
161
|
1 |
|
@click.pass_context |
|
162
|
1 |
|
def start(ctx: Context, container: str, pull: bool, recreate: bool, proxy: bool): |
|
163
|
|
|
"""See command Help.""" |
|
164
|
1 |
|
print(click.style('[STARTING]', fg='green') + ' your stakkr services') |
|
165
|
|
|
|
|
166
|
1 |
|
ctx.obj['STAKKR'].start(container, pull, recreate, proxy) |
|
167
|
1 |
|
_show_status(ctx) |
|
168
|
|
|
|
|
169
|
|
|
|
|
170
|
1 |
|
@stakkr.command(help="Display a list of running containers") |
|
171
|
1 |
|
@click.pass_context |
|
172
|
|
|
def status(ctx): |
|
173
|
|
|
"""See command Help.""" |
|
174
|
1 |
|
ctx.obj['STAKKR'].status() |
|
175
|
|
|
|
|
176
|
|
|
|
|
177
|
1 |
|
@stakkr.command(help="Stop all (or a single as CONTAINER) container(s)") |
|
178
|
1 |
|
@click.argument('container', required=False) |
|
179
|
1 |
|
@click.option('--proxy/--no-proxy', '-P', help="Stop the proxy", default=True) |
|
180
|
1 |
|
@click.pass_context |
|
181
|
1 |
|
def stop(ctx: Context, container: str, proxy: bool): |
|
182
|
|
|
"""See command Help.""" |
|
183
|
1 |
|
print(click.style('[STOPPING]', fg='yellow') + ' your stakkr services') |
|
184
|
1 |
|
ctx.obj['STAKKR'].stop(container, proxy) |
|
185
|
|
|
|
|
186
|
|
|
|
|
187
|
1 |
|
def _get_cmd_user(user: str, container: str): |
|
188
|
1 |
|
users = {'apache': 'www-data', 'nginx': 'www-data', 'php': 'www-data'} |
|
189
|
|
|
|
|
190
|
1 |
|
cmd_user = 'root' if user is None else user |
|
191
|
1 |
|
if container in users and user is None: |
|
192
|
1 |
|
cmd_user = users[container] |
|
193
|
|
|
|
|
194
|
1 |
|
return cmd_user |
|
195
|
|
|
|
|
196
|
|
|
|
|
197
|
1 |
|
def _show_status(ctx): |
|
198
|
1 |
|
services_ports = ctx.obj['STAKKR'].get_services_urls() |
|
199
|
1 |
|
if services_ports == '': |
|
200
|
1 |
|
print('\nServices Status:') |
|
201
|
1 |
|
ctx.invoke(status) |
|
202
|
1 |
|
return |
|
203
|
|
|
|
|
204
|
1 |
|
print('\nServices URLs :') |
|
205
|
1 |
|
print(services_ports) |
|
206
|
|
|
|
|
207
|
|
|
|
|
208
|
1 |
|
def _get_project_dir(config: str): |
|
209
|
1 |
|
from os.path import abspath, dirname |
|
210
|
|
|
|
|
211
|
1 |
|
if config is not None: |
|
212
|
1 |
|
config = abspath(config) |
|
213
|
1 |
|
return dirname(config) |
|
214
|
|
|
|
|
215
|
|
|
from stakkr.file_utils import find_project_dir |
|
216
|
|
|
return find_project_dir() |
|
217
|
|
|
|
|
218
|
|
|
|
|
219
|
1 |
|
def debug_mode(): |
|
220
|
|
|
"""Guess if we are in debug mode, useful to display runtime errors.""" |
|
221
|
1 |
|
if '--debug' in sys.argv or '-d' in sys.argv: |
|
222
|
1 |
|
return True |
|
223
|
|
|
|
|
224
|
1 |
|
return False |
|
225
|
|
|
|
|
226
|
|
|
|
|
227
|
1 |
|
def run_commands(ctx: Context, extra_args: tuple, tty: bool): |
|
228
|
|
|
"""Run commands for a specific alias""" |
|
229
|
1 |
|
commands = ctx.obj['STAKKR'].get_config()['aliases'][ctx.command.name]['exec'] |
|
230
|
1 |
|
for command in commands: |
|
231
|
1 |
|
user = command['user'] if 'user' in command else 'root' |
|
232
|
1 |
|
container = command['container'] |
|
233
|
1 |
|
args = command['args'] + list(extra_args) if extra_args is not None else [] |
|
234
|
|
|
|
|
235
|
1 |
|
ctx.invoke(exec_cmd, user=user, container=container, command=args, tty=tty) |
|
236
|
|
|
|
|
237
|
|
|
|
|
238
|
1 |
|
def main(): |
|
239
|
|
|
"""Call the CLI Script.""" |
|
240
|
1 |
|
try: |
|
241
|
1 |
|
for alias, conf in get_aliases().items(): |
|
242
|
1 |
|
if conf is None: |
|
243
|
|
|
continue |
|
244
|
|
|
|
|
245
|
1 |
|
cmd_help = conf['description'] if 'description' in conf else 'No description' |
|
246
|
|
|
|
|
247
|
1 |
|
@stakkr.command(help=cmd_help, name=alias) |
|
248
|
1 |
|
@click.option('--tty/--no-tty', '-t/ ', is_flag=True, default=True, help="Use a TTY") |
|
249
|
1 |
|
@click.argument('extra_args', required=False, nargs=-1, type=click.UNPROCESSED) |
|
250
|
1 |
|
@click.pass_context |
|
251
|
1 |
|
def _f(ctx: Context, extra_args: tuple, tty: bool): |
|
252
|
|
|
"""See command Help.""" |
|
253
|
1 |
|
run_commands(ctx, extra_args, tty) |
|
254
|
|
|
|
|
255
|
1 |
|
stakkr(obj={}) |
|
|
|
|
|
|
256
|
1 |
|
except Exception as error: |
|
|
|
|
|
|
257
|
1 |
|
msg = click.style(r""" ______ _____ _____ ____ _____ |
|
258
|
|
|
| ____| __ \| __ \ / __ \| __ \ |
|
259
|
|
|
| |__ | |__) | |__) | | | | |__) | |
|
260
|
|
|
| __| | _ /| _ /| | | | _ / |
|
261
|
|
|
| |____| | \ \| | \ \| |__| | | \ \ |
|
262
|
|
|
|______|_| \_\_| \_\\____/|_| \_\ |
|
263
|
|
|
|
|
264
|
|
|
""", fg='yellow') |
|
265
|
1 |
|
msg += click.style('{}'.format(error), fg='red') |
|
266
|
1 |
|
print(msg + '\n', file=sys.stderr) |
|
267
|
|
|
|
|
268
|
1 |
|
if debug_mode() is True: |
|
269
|
1 |
|
raise error |
|
270
|
|
|
|
|
271
|
1 |
|
sys.exit(1) |
|
272
|
|
|
|
|
273
|
|
|
|
|
274
|
1 |
|
if __name__ == '__main__': |
|
275
|
|
|
main() |
|
276
|
|
|
|
Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.
So, unless you specifically plan to handle any error, consider adding a more specific exception.