stakkr.cli   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 282
Duplicated Lines 0 %

Test Coverage

Coverage 97.13%

Importance

Changes 0
Metric Value
eloc 180
dl 0
loc 282
ccs 169
cts 174
cp 0.9713
rs 9.0399
c 0
b 0
f 0
wmc 42

16 Functions

Rating   Name   Duplication   Size   Complexity  
A services_update() 0 10 1
A status() 0 5 1
A stakkr() 0 21 3
A _get_project_dir() 0 9 2
A stop() 0 8 1
A console() 0 15 2
A run_commands() 0 10 5
A exec_cmd() 0 21 2
A services() 0 22 3
A _show_status() 0 9 2
A restart() 0 15 2
A _get_cmd_user() 0 8 4
B main() 0 34 6
A start() 0 12 1
A debug_mode() 0 6 3
A services_add() 0 25 4

How to fix   Complexity   

Complexity

Complex classes like stakkr.cli often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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