Source code for stakkr.actions

# coding: utf-8
"""Stakkr main controller. Used by the CLI to do all its actions."""

import os
from platform import system as os_name
import subprocess
import sys
import click
from clint.textui import colored, puts, columns
from stakkr import command, docker_actions as docker
from stakkr.configreader import Config
from stakkr.proxy import Proxy


[docs]class StakkrActions: """Main class that does actions asked in the cli.""" def __init__(self, ctx: dict): """Set all require properties.""" self.context = ctx # Set some general variables self.config = None self.project_name = None self.project_dir = None self.cwd_relative = None
[docs] def console(self, container: str, user: str, tty: bool): """Enter a container. Stakkr will try to guess the right shell.""" self.init_project() docker.check_cts_are_running(self.project_name) tty = 't' if tty is True else '' ct_name = docker.get_ct_name(container) cmd = ['docker', 'exec', '-u', user, '-i' + tty] cmd += [docker.get_ct_name(container), docker.guess_shell(ct_name)] subprocess.call(cmd) command.verbose(self.context['VERBOSE'], 'Command : "' + ' '.join(cmd) + '"')
[docs] def get_services_urls(self): """Once started, displays a message with a list of running containers.""" self.init_project() cts = docker.get_running_containers(self.project_name)[1] text = '' for _, ct_info in cts.items(): service_config = self.config['services'][ct_info['compose_name']] if ({'service_name', 'service_url'} <= set(service_config)) is False: continue url = self.get_url(service_config['service_url'], ct_info['compose_name']) name = colored.yellow(service_config['service_name']) text += ' - For {}'.format(name).ljust(55, ' ') + ' : ' + url + '\n' if 'service_extra_ports' in service_config: ports = ', '.join(map(str, service_config['service_extra_ports'])) text += ' '*4 + '(In your containers use the host ' text += '"{}" and port(s) {})\n'.format(ct_info['compose_name'], ports) return text
[docs] def exec_cmd(self, container: str, user: str, args: tuple, tty: bool): """Run a command from outside to any container. Wrapped into /bin/sh.""" self.init_project() docker.check_cts_are_running(self.project_name) # Protect args to avoid strange behavior in exec args = ['"{}"'.format(arg) for arg in args] tty = 't' if tty is True else '' ct_name = docker.get_ct_name(container) cmd = ['docker', 'exec', '-u', user, '-i' + tty, ct_name, 'sh', '-c'] cmd += ["""test -d "/var/{0}" && cd "/var/{0}" ; exec {1}""".format(self.cwd_relative, ' '.join(args))] command.verbose(self.context['VERBOSE'], 'Command : "' + ' '.join(cmd) + '"') subprocess.call(cmd, stdin=sys.stdin)
[docs] def get_config(self): """Read and validate config from config file""" config = Config(self.context['CONFIG']) main_config = config.read() if main_config is False: config.display_errors() sys.exit(1) return main_config
[docs] def init_project(self): """ Initializing the project by reading config and setting some properties of the object """ if self.config is not None: return self.config = self.get_config() self.project_name = self.config['project_name'] self.project_dir = self.config['project_dir'] sys.path.append(self.project_dir) self.cwd_relative = self._get_relative_dir() os.chdir(self.project_dir)
[docs] def start(self, container: str, pull: bool, recreate: bool, proxy: bool): """If not started, start the containers defined in config.""" self.init_project() verb = self.context['VERBOSE'] debug = self.context['DEBUG'] self._is_up(container) if pull is True: command.launch_cmd_displays_output(self._get_compose_base_cmd() + ['pull'], verb, debug, True) recreate_param = '--force-recreate' if recreate is True else '--no-recreate' cmd = self._get_compose_base_cmd() + ['up', '-d', recreate_param, '--remove-orphans'] cmd += _get_single_container_option(container) command.verbose(self.context['VERBOSE'], 'Command: ' + ' '.join(cmd)) command.launch_cmd_displays_output(cmd, verb, debug, True) running_cts, cts = docker.get_running_containers(self.project_name) if not running_cts: raise SystemError("Couldn't start the containers, run the start with '-v' and '-d'") self._run_iptables_rules(cts) if proxy is True: conf = self.config['proxy'] Proxy(conf.get('http_port'), conf.get('https_port')).start( docker.get_network_name(self.project_name))
[docs] def status(self): """Return a nice table with the list of started containers.""" self.init_project() try: docker.check_cts_are_running(self.project_name) except SystemError: puts(colored.yellow('[INFO]') + ' stakkr is currently stopped') sys.exit(0) _, cts = docker.get_running_containers(self.project_name) _print_status_headers() _print_status_body(cts)
[docs] def stop(self, container: str, proxy: bool): """If started, stop the containers defined in config. Else throw an error.""" self.init_project() verb = self.context['VERBOSE'] debug = self.context['DEBUG'] docker.check_cts_are_running(self.project_name) cmd = self._get_compose_base_cmd() + ['stop'] + _get_single_container_option(container) command.launch_cmd_displays_output(cmd, verb, debug, True) running_cts, _ = docker.get_running_containers(self.project_name) if running_cts and container is None: raise SystemError("Couldn't stop services ...") if proxy is True: Proxy().stop()
def _get_compose_base_cmd(self): if self.context['CONFIG'] is None: return ['stakkr-compose'] return ['stakkr-compose', '-c', self.context['CONFIG']] def _get_relative_dir(self): if os.getcwd().startswith(self.project_dir): return os.getcwd()[len(self.project_dir):].lstrip('/') return '' def _is_up(self, container: str): try: docker.check_cts_are_running(self.project_name) except SystemError: return if container is None: puts(colored.yellow('[INFO]') + ' stakkr is already started ...') sys.exit(0) # If single container : check if that specific one is running ct_name = docker.get_ct_item(container, 'name') if docker.container_running(ct_name): puts(colored.yellow('[INFO]') + ' service {} is already started ...'.format(container)) sys.exit(0) def _run_iptables_rules(self, cts: dict): """For some containers we need to add iptables rules added from the config.""" for _, ct_info in cts.items(): container = ct_info['compose_name'] ct_config = self.config['services'][container] if 'blocked_ports' not in ct_config: continue blocked_ports = ct_config['blocked_ports'] error, msg = docker.block_ct_ports(container, blocked_ports, self.project_name) if error is True: click.secho(msg, fg='red') continue command.verbose(self.context['VERBOSE'], msg)
[docs] def get_url(self, service_url: str, service: str): """Build URL to be displayed.""" proxy_conf = self.config['proxy'] # By default our URL is the IP url = docker.get_ct_item(service, 'ip') # If proxy enabled, display nice urls if bool(proxy_conf['enabled']): http_port = int(proxy_conf['http_port']) url = docker.get_ct_item(service, 'traefik_host').lower() url += '' if http_port == 80 else ':{}'.format(http_port) elif os_name() in ['Windows', 'Darwin']: puts(colored.yellow('[WARNING]') + ' Under Win and Mac, you need the proxy enabled') return service_url.format(url)
def _get_single_container_option(container: str): if container is None: return [] return [container] def _print_status_headers(): """Display messages for stakkr status (header)""" puts(columns( [(colored.green('Container')), 16], [colored.green('IP'), 15], [(colored.green('Url')), 32], [(colored.green('Image')), 32], [(colored.green('Docker ID')), 15], [(colored.green('Docker Name')), 25] )) puts(columns( ['-'*16, 16], ['-'*15, 15], ['-'*32, 32], ['-'*32, 32], ['-'*15, 15], ['-'*25, 25] )) def _print_status_body(cts: dict): """Display messages for stakkr status (body)""" for container in sorted(cts.keys()): ct_data = cts[container] if ct_data['ip'] == '': continue puts(columns( [ct_data['compose_name'], 16], [ct_data['ip'], 15], [ct_data['traefik_host'], 32], [ct_data['image'], 32], [ct_data['id'][:12], 15], [ct_data['name'], 25] ))