Passed
Push — master ( 457eda...4bff39 )
by Emmanuel
11:56
created

stakkr.cli.stakkr()   A

Complexity

Conditions 1

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 12
nop 4
dl 0
loc 18
ccs 12
cts 12
cp 1
crap 1
rs 9.8
c 0
b 0
f 0
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.1.6')
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
0 ignored issues
show
introduced by
Import outside toplevel (stakkr.actions)
Loading history...
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.option('--workdir', '-w', help="Working directory", default="/var/www")
65 1
@click.argument('container', required=True)
66 1
@click.argument('command', required=True, nargs=-1, type=click.UNPROCESSED)
67 1
def exec_cmd(ctx: Context, user: str, container: str, command: tuple, tty: bool, workdir: str):
0 ignored issues
show
best-practice introduced by
Too many arguments (6/5)
Loading history...
68
    """See command Help."""
69 1
    ctx.obj['STAKKR'].init_project()
70 1
    ctx.obj['CTS'] = get_running_containers_names(ctx.obj['STAKKR'].project_name)
71 1
    if ctx.obj['CTS']:
72 1
        click.Choice(ctx.obj['CTS']).convert(container, None, ctx)
73
74 1
    ctx.obj['STAKKR'].exec_cmd(container, _get_cmd_user(user, container), command, tty, workdir)
75
76
77 1
@stakkr.command(help="Restart all (or a single as CONTAINER) container(s)")
78 1
@click.argument('container', required=False)
79 1
@click.option('--pull', '-p', help="Force a pull of the latest images versions", is_flag=True)
80 1
@click.option('--recreate', '-r', help="Recreate all containers", is_flag=True)
81 1
@click.option('--proxy/--no-proxy', '-P', help="Restart the proxy", default=True)
82 1
@click.pass_context
83 1
def restart(ctx: Context, container: str, pull: bool, recreate: bool, proxy: bool):
84
    """See command Help."""
85 1
    print(click.style('[RESTARTING]', fg='green') + ' your stakkr services')
86 1
    try:
87 1
        ctx.invoke(stop, container=container, proxy=proxy)
88 1
    except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

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.

Loading history...
89 1
        pass
90
91 1
    ctx.invoke(start, container=container, pull=pull, recreate=recreate, proxy=proxy)
92
93
94 1
@stakkr.command(help="""List available services available for stakkr.yml
95
(with info if the service is enabled)""")
96 1
@click.pass_context
97
def services(ctx):
98
    """See command Help."""
99 1
    ctx.obj['STAKKR'].init_project()
100
101 1
    from stakkr.stakkr_compose import get_available_services
0 ignored issues
show
introduced by
Import outside toplevel (stakkr.stakkr_compose)
Loading history...
102
103 1
    print('Available services usable in stakkr.yml ', end='')
104 1
    print('({} = disabled) : '.format(click.style('✘', fg='red')))
105
106 1
    svcs = ctx.obj['STAKKR'].get_config()['services']
107 1
    enabled_svcs = [svc for svc, opts in svcs.items() if opts['enabled'] is True]
108 1
    available_svcs = get_available_services(ctx.obj['STAKKR'].project_dir)
109 1
    for available_svc in sorted(list(available_svcs.keys())):
110 1
        sign = click.style('✘', fg='red')
111 1
        if available_svc in enabled_svcs:
112 1
            version = svcs[available_svc]['version']
113 1
            sign = click.style(str(version), fg='green')
114
115 1
        print('  - {} ({})'.format(available_svc, sign))
116
117
118 1
@stakkr.command(help="""Download a pack of services from github (see github) containing services.
119
PACKAGE is the git url or package name.
120
NAME is the directory name to clone the repo.
121
""")
122 1
@click.argument('package', required=True)
123 1
@click.argument('name', required=False)
124 1
@click.pass_context
125 1
def services_add(ctx: Context, package: str, name: str):
126
    """See command Help."""
127 1
    from stakkr.services import install
0 ignored issues
show
introduced by
Import outside toplevel (stakkr.services)
Loading history...
128
129 1
    project_dir = _get_project_dir(ctx.obj['CONFIG'])
130 1
    services_dir = '{}/services'.format(project_dir)
131 1
    name = package if name is None else name
132 1
    success, message = install(services_dir, package, name)
133 1
    if success is False:
134 1
        click.echo(click.style(message, fg='red'))
135 1
        sys.exit(1)
136
137 1
    stdout_msg = 'Package "' + click.style(package, fg='green') + '" installed successfully'
138 1
    if message is not None:
139 1
        stdout_msg = click.style(message, fg='yellow')
140
141 1
    click.echo(stdout_msg)
142 1
    click.echo('Try ' + click.style('stakkr services', fg='green') + ' to see new available services')
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (102/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
143
144
145 1
@stakkr.command(help="Update all services packs in services/")
146 1
@click.pass_context
147
def services_update(ctx):
148
    """See command Help."""
149 1
    from stakkr.services import update_all
0 ignored issues
show
introduced by
Import outside toplevel (stakkr.services)
Loading history...
150
151 1
    project_dir = _get_project_dir(ctx.obj['CONFIG'])
152 1
    services_dir = '{}/services'.format(project_dir)
153 1
    update_all(services_dir)
154 1
    print(click.style('Packages updated', fg='green'))
155
156
157 1
@stakkr.command(help="Start all (or a single as CONTAINER) container(s) defined in compose.ini")
158 1
@click.argument('container', required=False)
159 1
@click.option('--pull', '-p', help="Force a pull of the latest images versions", is_flag=True)
160 1
@click.option('--recreate', '-r', help="Recreate all containers", is_flag=True)
161 1
@click.option('--proxy/--no-proxy', '-P', help="Start proxy", default=True)
162 1
@click.pass_context
163 1
def start(ctx: Context, container: str, pull: bool, recreate: bool, proxy: bool):
164
    """See command Help."""
165 1
    print(click.style('[STARTING]', fg='green') + ' your stakkr services')
166
167 1
    ctx.obj['STAKKR'].start(container, pull, recreate, proxy)
168 1
    _show_status(ctx)
169
170
171 1
@stakkr.command(help="Display a list of running containers")
172 1
@click.pass_context
173
def status(ctx):
174
    """See command Help."""
175 1
    ctx.obj['STAKKR'].status()
176
177
178 1
@stakkr.command(help="Stop all (or a single as CONTAINER) container(s)")
179 1
@click.argument('container', required=False)
180 1
@click.option('--proxy/--no-proxy', '-P', help="Stop the proxy", default=True)
181 1
@click.pass_context
182 1
def stop(ctx: Context, container: str, proxy: bool):
183
    """See command Help."""
184 1
    print(click.style('[STOPPING]', fg='yellow') + ' your stakkr services')
185 1
    ctx.obj['STAKKR'].stop(container, proxy)
186
187
188 1
def _get_cmd_user(user: str, container: str):
189 1
    users = {'apache': 'www-data', 'nginx': 'www-data', 'php': 'www-data'}
190
191 1
    cmd_user = 'root' if user is None else user
192 1
    if container in users and user is None:
193 1
        cmd_user = users[container]
194
195 1
    return cmd_user
196
197
198 1
def _show_status(ctx):
199 1
    services_ports = ctx.obj['STAKKR'].get_services_urls()
200 1
    if services_ports == '':
201 1
        print('\nServices Status:')
202 1
        ctx.invoke(status)
203 1
        return
204
205 1
    print('\nServices URLs :')
206 1
    print(services_ports)
207
208
209 1
def _get_project_dir(config: str):
210 1
    from os.path import abspath, dirname
0 ignored issues
show
introduced by
Import outside toplevel (os.path)
Loading history...
211
212 1
    if config is not None:
213 1
        config = abspath(config)
214 1
        return dirname(config)
215
216
    from stakkr.file_utils import find_project_dir
0 ignored issues
show
introduced by
Import outside toplevel (stakkr.file_utils)
Loading history...
217
    return find_project_dir()
218
219
220 1
def debug_mode():
221
    """Guess if we are in debug mode, useful to display runtime errors."""
222 1
    if '--debug' in sys.argv or '-d' in sys.argv:
223 1
        return True
224
225 1
    return False
226
227
228 1
def run_commands(ctx: Context, extra_args: tuple, tty: bool):
229
    """Run commands for a specific alias"""
230 1
    commands = ctx.obj['STAKKR'].get_config()['aliases'][ctx.command.name]['exec']
231 1
    for command in commands:
232 1
        user = command['user'] if 'user' in command else 'root'
233 1
        workdir = command['workdir'] if 'workdir' in command else '/'
234 1
        container = command['container']
235 1
        args = command['args'] + list(extra_args) if extra_args is not None else []
236
237 1
        ctx.invoke(exec_cmd, user=user, container=container, command=args, tty=tty, workdir=workdir)
238
239
240 1
def main():
241
    """Call the CLI Script."""
242 1
    try:
243 1
        for alias, conf in get_aliases().items():
244 1
            if conf is None:
245
                continue
246
247 1
            cmd_help = conf['description'] if 'description' in conf else 'No description'
248
249 1
            @stakkr.command(help=cmd_help, name=alias)
250 1
            @click.option('--tty/--no-tty', '-t/ ', is_flag=True, default=True, help="Use a TTY")
251 1
            @click.argument('extra_args', required=False, nargs=-1, type=click.UNPROCESSED)
252 1
            @click.pass_context
253 1
            def _f(ctx: Context, extra_args: tuple, tty: bool):
254
                """See command Help."""
255 1
                run_commands(ctx, extra_args, tty)
256
257 1
        stakkr(obj={})
0 ignored issues
show
Bug introduced by
The keyword obj does not seem to exist for the function call.
Loading history...
Bug introduced by
It seems like a value for argument ctx is missing in the function call.
Loading history...
258 1
    except Exception as error:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

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.

Loading history...
259 1
        msg = click.style(r""" ______ _____  _____   ____  _____
260
|  ____|  __ \|  __ \ / __ \|  __ \
261
| |__  | |__) | |__) | |  | | |__) |
262
|  __| |  _  /|  _  /| |  | |  _  /
263
| |____| | \ \| | \ \| |__| | | \ \
264
|______|_|  \_\_|  \_\\____/|_|  \_\
265
266
""", fg='yellow')
267 1
        msg += click.style('{}'.format(error), fg='red')
268 1
        print(msg + '\n', file=sys.stderr)
269
270 1
        if debug_mode() is True:
271 1
            raise error
272
273 1
        sys.exit(1)
274
275
276 1
if __name__ == '__main__':
277
    main()
278