Passed
Pull Request — master (#8)
by
unknown
04:52
created

StakkrActions.start()   A

Complexity

Conditions 4

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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