Passed
Push — master ( 0fd795...73442f )
by Emmanuel
14:19
created

actions.StakkrActions.get_url()   A

Complexity

Conditions 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.1755

Importance

Changes 0
Metric Value
cc 4
eloc 9
nop 4
dl 0
loc 14
ccs 7
cts 9
cp 0.7778
crap 4.1755
rs 9.95
c 0
b 0
f 0
1
"""
2
Stakkr main controller. Used by the CLI to do all its actions
3
"""
4
5 1
import os
6 1
from platform import system as os_name
7 1
import subprocess
8 1
import sys
9 1
import click
10 1
from clint.textui import colored, puts, columns
11 1
from stakkr import command, docker_actions, package_utils
12 1
from stakkr.configreader import Config
13 1
from stakkr.proxy import Proxy
14
15
16 1
class StakkrActions():
17
    """Main class that does actions asked in the cli"""
18
19 1
    _services_to_display = {
20
        'adminer': {'name': 'Adminer', 'url': 'http://{}'},
21
        'apache': {'name': 'Web Server', 'url': 'http://{}'},
22
        'elasticsearch': {'name': 'ElastiscSearch', 'url': 'http://{}'},
23
        'mailcatcher': {'name': 'Mailcatcher (fake SMTP)', 'url': 'http://{}', 'extra_port': 25},
24
        'maildev': {'name': 'Maildev (Fake SMTP)', 'url': 'http://{}', 'extra_port': 25},
25
        'nginx': {'name': 'Web Server', 'url': 'http://{}'},
26
        'portainer': {'name': 'Portainer (Docker GUI)', 'url': 'http://{}'},
27
        'phpmyadmin': {'name': 'PhpMyAdmin', 'url': 'http://{}'},
28
        'xhgui': {'name': 'XHGui (PHP Profiling)', 'url': 'http://{}'}
29
    }
30
31
32 1
    def __init__(self, base_dir: str, ctx: dict):
33
        # Work with directories and move to the right place
34 1
        self.stakkr_base_dir = base_dir
35 1
        self.context = ctx
36 1
        self.cwd_abs = os.getcwd()
37 1
        self.cwd_relative = self._get_relative_dir()
38 1
        os.chdir(self.stakkr_base_dir)
39
40
        # Set some general variables
41 1
        self.config_file = ctx['CONFIG']
42 1
        self.compose_base_cmd = self._get_compose_base_cmd()
43 1
        self.cts = []
44 1
        self.running_cts = []
45
46
        # Get info from config
47 1
        self.config = self._get_config()
48 1
        self.main_config = self.config['main']
49 1
        self.project_name = self.main_config.get('project_name')
50
51
52 1
    def console(self, container: str, user: str, tty: bool):
53
        """Enter a container. Stakkr will try to guess the right shell"""
54
55 1
        docker_actions.check_cts_are_running(self.project_name)
56
57 1
        tty = 't' if tty is True else ''
58 1
        ct_name = docker_actions.get_ct_name(container)
59 1
        cmd = ['docker', 'exec', '-u', user, '-i' + tty]
60 1
        cmd += [docker_actions.get_ct_name(container), docker_actions.guess_shell(ct_name)]
61
        subprocess.call(cmd)
62
63
        command.verbose(self.context['VERBOSE'], 'Command : "' + ' '.join(cmd) + '"')
64
65 1
    def get_services_urls(self):
66
        """Once started, stakkr displays a message with the list of launched containers."""
67 1
        cts = docker_actions.get_running_containers(self.project_name)[1]
68
69 1
        text = ''
70 1
        for ct_id, ct_info in cts.items():
71 1
            if ct_info['compose_name'] not in self._services_to_display:
72 1
                continue
73
74 1
            options = self._services_to_display[ct_info['compose_name']]
75 1
            url = self.get_url(ct_info['ports'], options['url'], ct_info['compose_name'])
76 1
            name = colored.yellow(options['name'])
77 1
            text += '  - For {}'.format(name).ljust(55, ' ') + ' : ' + url + '\n'
78
79 1
            if 'extra_port' in options:
80 1
                port = str(options['extra_port'])
81 1
                text += ' '*4 + '(In your containers use the host '
82 1
                text += '"{}" and port {})\n'.format(ct_info['compose_name'], port)
83
84 1
        return text
85
86
87 1
    def exec_cmd(self, container: str, user: str, args: tuple, tty: bool):
88
        """Run a command from outside to any container. Wrapped into /bin/sh"""
89
90 1
        docker_actions.check_cts_are_running(self.project_name)
91
92
        # Protect args to avoid strange behavior in exec
93 1
        args = ['"{}"'.format(arg) for arg in args]
94
95 1
        tty = 't' if tty is True else ''
96 1
        ct_name = docker_actions.get_ct_name(container)
97 1
        cmd = ['docker', 'exec', '-u', user, '-i' + tty, ct_name, 'sh', '-c']
98 1
        cmd += ["""test -d "/var/{0}" && cd "/var/{0}" ; exec {1}""".format(self.cwd_relative, ' '.join(args))]
99 1
        command.verbose(self.context['VERBOSE'], 'Command : "' + ' '.join(cmd) + '"')
100 1
        subprocess.call(cmd, stdin=sys.stdin)
101
102
103 1
    def start(self, container: str, pull: bool, recreate: bool, proxy: bool):
104
        """If not started, start the containers defined in config"""
105 1
        verb = self.context['VERBOSE']
106 1
        debug = self.context['DEBUG']
107
108 1
        self._is_containers_running(container)
109
110 1
        if pull is True:
111
            command.launch_cmd_displays_output(self.compose_base_cmd + ['pull'], verb, debug, True)
112
113 1
        recreate_param = '--force-recreate' if recreate is True else '--no-recreate'
114 1
        cmd = self.compose_base_cmd + ['up', '-d', recreate_param, '--remove-orphans']
115 1
        cmd += self._get_single_container_option(container)
116
117 1
        command.verbose(self.context['VERBOSE'], 'Command: ' + ' '.join(cmd))
118 1
        command.launch_cmd_displays_output(cmd, verb, debug, True)
119
120 1
        self.running_cts, self.cts = docker_actions.get_running_containers(self.project_name)
121 1
        if self.running_cts is 0:
122
            raise SystemError("Couldn't start the containers, run the start with '-v' and '-d'")
123
124 1
        self._run_iptables_rules()
125 1
        self._run_services_post_scripts()
126 1
        if proxy is True:
127 1
            network_name = docker_actions.get_network_name(self.project_name)
128 1
            Proxy(self.config['proxy'].get('port')).start(network_name)
129
130
131 1
    def status(self):
132
        """Returns a nice table with the list of started containers"""
133
134 1
        try:
135 1
            docker_actions.check_cts_are_running(self.project_name)
136 1
        except SystemError:
137 1
            puts(colored.yellow('[INFO]') + ' stakkr is currently stopped')
138 1
            sys.exit(0)
139
140 1
        self._print_status_headers()
141 1
        self._print_status_body()
142
143
144 1
    def stop(self, container: str, proxy: bool):
145
        """If started, stop the containers defined in config. Else throw an error"""
146
147 1
        verb = self.context['VERBOSE']
148 1
        debug = self.context['DEBUG']
149
150 1
        docker_actions.check_cts_are_running(self.project_name)
151
152 1
        cmd = self.compose_base_cmd + ['stop'] + self._get_single_container_option(container)
153 1
        command.launch_cmd_displays_output(cmd, verb, debug, True)
154
155 1
        self.running_cts, self.cts = docker_actions.get_running_containers(self.project_name)
156 1
        if self.running_cts is not 0 and container is None:
157
            raise SystemError("Couldn't stop services ...")
158
159 1
        if proxy is True:
160 1
            network_name = docker_actions.get_network_name(self.project_name)
161 1
            Proxy(self.config['proxy'].get('port')).stop()
162
163
164 1
    def _call_service_post_script(self, service: str):
165 1
        service_script = package_utils.get_file('static', 'services/{}.sh'.format(service))
166 1
        if os.path.isfile(service_script) is True:
167
            cmd = ['bash', service_script, docker_actions.get_ct_item(service, 'name')]
168
            subprocess.call(cmd)
169
            command.verbose(self.context['VERBOSE'], 'Service Script : ' + ' '.join(cmd))
170
171
172 1
    def _get_compose_base_cmd(self):
173 1
        if self.context['CONFIG'] is None:
174 1
            return ['stakkr-compose']
175
176 1
        return ['stakkr-compose', '-c', self.context['CONFIG']]
177
178
179 1
    def _get_config(self):
180 1
        config = Config(self.config_file)
181 1
        main_config = config.read()
182 1
        if main_config is False:
183 1
            config.display_errors()
184 1
            sys.exit(1)
185
186 1
        return main_config
187
188
189 1
    def _get_single_container_option(self, container: str):
190 1
        if container is None:
191 1
            return []
192
193 1
        return [container]
194
195
196 1
    def _get_relative_dir(self):
197 1
        if self.cwd_abs.startswith(self.stakkr_base_dir):
198 1
            return self.cwd_abs[len(self.stakkr_base_dir):].lstrip('/')
199
200
        return ''
201
202
203 1
    def _is_containers_running(self, container: str):
204
205 1
        try:
206 1
            docker_actions.check_cts_are_running(self.project_name)
207 1
        except SystemError:
208 1
            return
209
210 1
        if container is None:
211 1
            puts(colored.yellow('[INFO]') + ' stakkr is already started ...')
212 1
            sys.exit(0)
213
214
        # If single container : check if that specific one is running
215 1
        ct_name = docker_actions.get_ct_item(container, 'name')
216 1
        if docker_actions.container_running(ct_name):
217 1
            puts(colored.yellow('[INFO]') + ' service {} is already started ...'.format(container))
218 1
            sys.exit(0)
219
220
221 1
    def _print_status_headers(self):
222 1
        puts(columns(
223
            [(colored.green('Container')), 16], [colored.green('IP'), 15],
224
            [(colored.green('Url')), 32], [(colored.green('Image')), 32],
225
            [(colored.green('Docker ID')), 15], [(colored.green('Docker Name')), 25]
226
            ))
227
228 1
        puts(columns(
229
            ['-'*16, 16], ['-'*15, 15],
230
            ['-'*32, 32], ['-'*32, 32],
231
            ['-'*15, 15], ['-'*25, 25]
232
            ))
233
234
235 1
    def _print_status_body(self):
236 1
        self.running_cts, self.cts = docker_actions.get_running_containers(self.project_name)
237
238 1
        for container in sorted(self.cts.keys()):
239 1
            ct_data = self.cts[container]
240 1
            if ct_data['ip'] == '':
241
                continue
242
243 1
            puts(columns(
244
                [ct_data['compose_name'], 16], [ct_data['ip'], 15],
245
                [ct_data['traefik_host'], 32], [ct_data['image'], 32],
246
                [ct_data['id'][:12], 15], [ct_data['name'], 25]
247
                ))
248
249
250 1
    def _run_iptables_rules(self):
251
        """For some containers we need to add iptables rules added from the config"""
252
253 1
        block_config = self.config['network-block']
254 1
        for service, ports in block_config.items():
255 1
            error, msg = docker_actions.block_ct_ports(service, ports, self.project_name)
256 1
            if error is True:
257
                click.secho(msg, fg='red')
258
                continue
259
260 1
            command.verbose(self.context['VERBOSE'], msg)
261
262
263 1
    def _run_services_post_scripts(self):
264
        """A service can have a .sh file that will be executed once it's started.
265
        Useful to override some actions of the classical /run.sh
266
267
        """
268
269 1
        if os.name == 'nt':
270
            click.secho('Could not run service post scripts under Windows', fg='red')
271
            return
272
273 1
        for service in self.main_config.get('services'):
274 1
            self._call_service_post_script(service)
275
276
277 1
    def get_url(self, ports: list, service_url: str, service: str):
278
        """Build URL to be displayed"""
279
280 1
        proxy_conf = self.config['proxy']
281
        # By default our URL is the IP
282 1
        url = docker_actions.get_ct_item(service, 'ip')
283
        # If proxy enabled, display nice urls
284 1
        if int(proxy_conf['enabled']) is 1:
285 1
            url = docker_actions.get_ct_item(service, 'traefik_host')
286 1
            url += '' if int(proxy_conf['port']) is 80 else ':{}'.format(proxy_conf['port'])
287
        elif os_name() in ['Windows', 'Darwin']:
288
            puts(colored.yellow('[WARNING]') + ' Under Win and Mac, you need the proxy enabled')
289
290
        return service_url.format(url)
291