stakkr.actions   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 266
Duplicated Lines 0 %

Test Coverage

Coverage 94.01%

Importance

Changes 0
Metric Value
eloc 180
dl 0
loc 266
ccs 157
cts 167
cp 0.9401
rs 8.64
c 0
b 0
f 0
wmc 47

14 Methods

Rating   Name   Duplication   Size   Complexity  
A StakkrActions._is_up() 0 15 4
A StakkrActions._get_compose_base_cmd() 0 5 2
A StakkrActions._get_relative_dir() 0 5 2
A StakkrActions.stop() 0 17 4
A StakkrActions.__init__() 0 9 1
A StakkrActions.console() 0 13 2
A StakkrActions.get_services_urls() 0 23 4
A StakkrActions.exec_cmd() 0 17 3
A StakkrActions.get_url() 0 16 4
A StakkrActions._run_iptables_rules() 0 15 4
B StakkrActions.start() 0 27 5
A StakkrActions.get_config() 0 9 2
A StakkrActions.status() 0 14 2
A StakkrActions.init_project() 0 15 2

3 Functions

Rating   Name   Duplication   Size   Complexity  
A _print_status_headers() 0 12 1
A _print_status_body() 0 11 3
A _get_single_container_option() 0 5 2

How to fix   Complexity   

Complexity

Complex classes like stakkr.actions 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
# coding: utf-8
2 1
"""Stakkr main controller. Used by the CLI to do all its actions."""
3
4 1
import os
5 1
from platform import system as os_name
6 1
import subprocess
7 1
import sys
8 1
import click
9 1
from clint.textui import colored, puts, columns
10 1
from stakkr import command, docker_actions as docker
11 1
from stakkr.configreader import Config
12 1
from stakkr.proxy import Proxy
13
14
15 1
class StakkrActions:
16
    """Main class that does actions asked in the cli."""
17
18 1
    def __init__(self, ctx: dict):
19
        """Set all require properties."""
20 1
        self.context = ctx
21
22
        # Set some general variables
23 1
        self.config = None
24 1
        self.project_name = None
25 1
        self.project_dir = None
26 1
        self.cwd_relative = None
27
28 1
    def console(self, container: str, user: str, is_tty: bool):
29
        """Enter a container. Stakkr will try to guess the right shell."""
30 1
        self.init_project()
31
32 1
        docker.check_cts_are_running(self.project_name)
33
34 1
        tty = 't' if is_tty is True else ''
35 1
        ct_name = docker.get_ct_name(container)
36 1
        cmd = ['docker', 'exec', '-u', user, '-i' + tty]
37 1
        cmd += [docker.get_ct_name(container), docker.guess_shell(ct_name)]
38
        subprocess.call(cmd)
39
40
        command.verbose(self.context['VERBOSE'], 'Command : "' + ' '.join(cmd) + '"')
41
42 1
    def get_services_urls(self):
43
        """Once started, displays a message with a list of running containers."""
44 1
        self.init_project()
45
46 1
        cts = docker.get_running_containers(self.project_name)[1]
47
48 1
        text = ''
49 1
        for _, ct_info in cts.items():
50 1
            service_config = self.config['services'][ct_info['compose_name']]
51 1
            if ({'service_name', 'service_url'} <= set(service_config)) is False:
52 1
                continue
53
54 1
            url = self.get_url(service_config['service_url'], ct_info['compose_name'])
55 1
            name = colored.yellow(service_config['service_name'])
56
57 1
            text += '  - For {}'.format(name).ljust(55, ' ') + ' : ' + url + '\n'
58
59 1
            if 'service_extra_ports' in service_config:
60 1
                ports = ', '.join(map(str, service_config['service_extra_ports']))
61 1
                text += ' '*4 + '(In your containers use the host '
62 1
                text += '"{}" and port(s) {})\n'.format(ct_info['compose_name'], ports)
63
64 1
        return text
65
66 1
    def exec_cmd(self,
67
                 container: str, user: str,
68
                 args: tuple, is_tty: bool, workdir: str = None):
69
        """Run a command from outside to any container. Wrapped into /bin/sh."""
70 1
        self.init_project()
71
72 1
        docker.check_cts_are_running(self.project_name)
73
74
        # Protect args to avoid strange behavior in exec
75 1
        cmd_args = ['"{}"'.format(arg) for arg in args]
76 1
        workdir = "/var/{}".format(self.cwd_relative) if workdir is None else workdir
77 1
        tty = 't' if is_tty is True else ''
78 1
        ct_name = docker.get_ct_name(container)
79 1
        cmd = ['docker', 'exec', '-u', user, '-i' + tty, '-w', workdir, ct_name, 'sh', '-c']
80 1
        cmd += ["""exec {}""".format(' '.join(cmd_args))]
81 1
        command.verbose(self.context['VERBOSE'], 'Command : "' + ' '.join(cmd) + '"')
82 1
        subprocess.call(cmd, stdin=sys.stdin)
83
84 1
    def get_config(self):
85
        """Read and validate config from config file"""
86 1
        config = Config(self.context['CONFIG'])
87 1
        main_config = config.read()
88 1
        if main_config is False:
89 1
            config.display_errors()
90 1
            sys.exit(1)
91
92 1
        return main_config
93
94 1
    def init_project(self):
95
        """
96
        Initializing the project by reading config and
97
        setting some properties of the object
98
        """
99 1
        if self.config is not None:
100 1
            return
101
102 1
        self.config = self.get_config()
103 1
        self.project_name = self.config['project_name']
104 1
        self.project_dir = self.config['project_dir']
105 1
        sys.path.append(self.project_dir)
106
107 1
        self.cwd_relative = self._get_relative_dir()
108 1
        os.chdir(self.project_dir)
109
110 1
    def start(self, container: str, pull: bool, recreate: bool, proxy: bool):
111
        """If not started, start the containers defined in config."""
112 1
        self.init_project()
113 1
        verb = self.context['VERBOSE']
114 1
        debug = self.context['DEBUG']
115 1
        self._is_up(container)
116
117 1
        if pull is True:
118 1
            cmd = self._get_compose_base_cmd() + ['pull']
119 1
            command.launch_cmd_displays_output(cmd, verb, debug, True)
120
121 1
        recreate_param = '--force-recreate' if recreate is True else '--no-recreate'
122 1
        cmd = self._get_compose_base_cmd() + ['up', '-d', recreate_param, '--remove-orphans']
123 1
        cmd += _get_single_container_option(container)
124
125 1
        command.verbose(self.context['VERBOSE'], 'Command: ' + ' '.join(cmd))
126 1
        command.launch_cmd_displays_output(cmd, verb, debug, True)
127
128 1
        running_cts, cts = docker.get_running_containers(self.project_name)
129 1
        if not running_cts:
130
            raise SystemError("Couldn't start the containers, run the start with '-v' and '-d'")
131
132 1
        self._run_iptables_rules(cts)
133 1
        if proxy is True:
134 1
            conf = self.config['proxy']
135 1
            Proxy(conf.get('http_port'), conf.get('https_port'), version=conf.get('version')).start(
136
                docker.get_network_name(self.project_name))
137
138 1
    def status(self):
139
        """Return a nice table with the list of started containers."""
140 1
        self.init_project()
141
142 1
        try:
143 1
            docker.check_cts_are_running(self.project_name)
144 1
        except SystemError:
145 1
            puts(colored.yellow('[INFO]') + ' stakkr is currently stopped')
146 1
            sys.exit(0)
147
148 1
        _, cts = docker.get_running_containers(self.project_name)
149
150 1
        _print_status_headers()
151 1
        _print_status_body(cts)
152
153 1
    def stop(self, container: str, proxy: bool):
154
        """If started, stop the containers defined in config. Else throw an error."""
155 1
        self.init_project()
156 1
        verb = self.context['VERBOSE']
157 1
        debug = self.context['DEBUG']
158
159 1
        docker.check_cts_are_running(self.project_name)
160
161 1
        cmd = self._get_compose_base_cmd() + ['stop'] + _get_single_container_option(container)
162 1
        command.launch_cmd_displays_output(cmd, verb, debug, True)
163
164 1
        running_cts, _ = docker.get_running_containers(self.project_name)
165 1
        if running_cts and container is None:
166
            raise SystemError("Couldn't stop services ...")
167
168 1
        if proxy is True:
169 1
            Proxy().stop()
170
171 1
    def _get_compose_base_cmd(self):
172 1
        if self.context['CONFIG'] is None:
173
            return ['stakkr-compose']
174
175 1
        return ['stakkr-compose', '-c', self.context['CONFIG']]
176
177 1
    def _get_relative_dir(self):
178 1
        if os.getcwd().startswith(self.project_dir):
179
            return os.getcwd()[len(self.project_dir):].lstrip('/')
180
181 1
        return ''
182
183 1
    def _is_up(self, container: str):
184 1
        try:
185 1
            docker.check_cts_are_running(self.project_name)
186 1
        except SystemError:
187 1
            return
188
189 1
        if container is None:
190 1
            puts(colored.yellow('[INFO]') + ' stakkr is already started ...')
191 1
            sys.exit(0)
192
193
        # If single container : check if that specific one is running
194 1
        ct_name = docker.get_ct_item(container, 'name')
195 1
        if docker.container_running(ct_name):
196 1
            puts(colored.yellow('[INFO]') + ' service {} is already started ...'.format(container))
197 1
            sys.exit(0)
198
199 1
    def _run_iptables_rules(self, cts: dict):
200
        """For some containers we need to add iptables rules added from the config."""
201 1
        for _, ct_info in cts.items():
202 1
            container = ct_info['compose_name']
203 1
            ct_config = self.config['services'][container]
204 1
            if 'blocked_ports' not in ct_config:
205 1
                continue
206
207 1
            blocked_ports = ct_config['blocked_ports']
208 1
            error, msg = docker.block_ct_ports(container, blocked_ports, self.project_name)
209 1
            if error is True:
210
                click.secho(msg, fg='red')
211
                continue
212
213 1
            command.verbose(self.context['VERBOSE'], msg)
214
215 1
    def get_url(self, service_url: str, service: str):
216
        """Build URL to be displayed."""
217 1
        proxy_conf = self.config['proxy']
218
        # By default our URL is the IP
219 1
        url = docker.get_ct_item(service, 'ip')
220
        # If proxy enabled, display nice urls
221 1
        if bool(proxy_conf['enabled']):
222 1
            http_port = int(proxy_conf['http_port'])
223 1
            url = docker.get_ct_item(service, 'traefik_host').lower()
224 1
            url += '' if http_port == 80 else ':{}'.format(http_port)
225
        elif os_name() in ['Windows', 'Darwin']:
226
            puts(colored.yellow('[WARNING]') + ' Under Win and Mac, you need the proxy enabled')
227
228 1
        urls = [service_url.format(url) for url in url.split(',')]
229
230 1
        return ' / '.join(urls)
231
232
233 1
def _get_single_container_option(container: str):
234 1
    if container is None:
235 1
        return []
236
237 1
    return [container]
238
239
240 1
def _print_status_headers():
241
    """Display messages for stakkr status (header)"""
242 1
    puts(columns(
243
        [(colored.green('Container')), 16], [colored.green('IP'), 15],
244
        [(colored.green('Url')), 32], [(colored.green('Image')), 32],
245
        [(colored.green('Docker ID')), 15], [(colored.green('Docker Name')), 25]
246
    ))
247
248 1
    puts(columns(
249
        ['-'*16, 16], ['-'*15, 15],
250
        ['-'*32, 32], ['-'*32, 32],
251
        ['-'*15, 15], ['-'*25, 25]
252
    ))
253
254
255 1
def _print_status_body(cts: dict):
256
    """Display messages for stakkr status (body)"""
257 1
    for container in sorted(cts.keys()):
258 1
        ct_data = cts[container]
259 1
        if ct_data['ip'] == '':
260 1
            continue
261
262 1
        puts(columns(
263
            [ct_data['compose_name'], 16], [ct_data['ip'], 15],
264
            [ct_data['traefik_host'], 32], [ct_data['image'], 32],
265
            [ct_data['id'][:12], 15], [ct_data['name'], 25]
266
        ))
267