Test Failed
Branch v4.0-dev (e44d7e)
by Emmanuel
04:49
created

actions.StakkrActions.stop()   A

Complexity

Conditions 4

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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