Passed
Push — master ( b9e3b5...041dd8 )
by Emmanuel
05:58
created

stakkr.actions.StakkrActions._get_relative_dir()   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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