mirror of
https://github.com/cupcakearmy/tumbo.git
synced 2024-12-21 15:56:25 +00:00
refactor to package
This commit is contained in:
parent
be05028e4a
commit
3f975f6057
204
src/main.py
204
src/main.py
@ -1,204 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import random
|
||||
import argparse
|
||||
from itertools import product
|
||||
from typing import Optional
|
||||
from multiprocessing import Pool, cpu_count
|
||||
|
||||
from jinja2 import Template
|
||||
from yaml import load, Loader
|
||||
from termcolor import colored, cprint
|
||||
|
||||
|
||||
def compile_string(template, variables={}):
|
||||
return Template(template).render(**variables)
|
||||
|
||||
|
||||
def buffer_to_string(buffer) -> str:
|
||||
return buffer.decode(sys.getdefaultencoding()).strip()
|
||||
|
||||
|
||||
def exec(command, *args, **kwargs):
|
||||
output = subprocess.run(command, *args, capture_output=True, **kwargs)
|
||||
|
||||
if output.returncode is not 0:
|
||||
raise Exception(buffer_to_string(output.stderr))
|
||||
|
||||
return buffer_to_string(output.stdout)
|
||||
|
||||
|
||||
def exec_multiple(commands, *args, show=True, **kwargs):
|
||||
try:
|
||||
for command in commands:
|
||||
print(colored(' '.join(command), 'blue', attrs=['bold']))
|
||||
result = exec(command, *args, **kwargs)
|
||||
if show:
|
||||
print(result + '\n')
|
||||
except Exception as e:
|
||||
print(colored(e, 'red'))
|
||||
|
||||
|
||||
def build_push_run(rendered, name, context, push, run, show=False):
|
||||
try:
|
||||
|
||||
# Create a tmp dockerfile to work with
|
||||
hash = resolve_path(context, str(random.getrandbits(128)))
|
||||
|
||||
if not os.access('.', os.W_OK):
|
||||
print(colored('Cannot write to directory', 'red'))
|
||||
return
|
||||
|
||||
if os.path.isfile(hash):
|
||||
print(colored(f'File {hash} already exitsts.', 'red'))
|
||||
return
|
||||
|
||||
with open(hash, 'w') as f:
|
||||
f.write(rendered)
|
||||
|
||||
commands = [['docker', 'build', '-f', hash, '-t', name, '.']]
|
||||
if(push):
|
||||
commands.append(['docker', 'push', name])
|
||||
if(run):
|
||||
commands.append(['docker', 'run', '--rm', name])
|
||||
|
||||
exec_multiple(commands, cwd=context, show=show)
|
||||
|
||||
finally:
|
||||
# Delete the tmp file
|
||||
if os.path.exists(hash):
|
||||
os.remove(hash)
|
||||
|
||||
|
||||
def process_queue(queue, context, parallel, push, run):
|
||||
|
||||
queue = [
|
||||
(rendered, tag, context, push, run, True)
|
||||
for rendered, tag in queue
|
||||
]
|
||||
|
||||
if parallel:
|
||||
thread_count = parallel if type(parallel) is int else cpu_count()
|
||||
with Pool(thread_count) as pool:
|
||||
pool.starmap(build_push_run, queue)
|
||||
else:
|
||||
for item in queue:
|
||||
build_push_run(*item)
|
||||
|
||||
|
||||
def compile_templates(variables, recipe, tag, context, show=True):
|
||||
queue = []
|
||||
for item in product(*variables.values()):
|
||||
zipped_variables = dict(zip(variables.keys(), item))
|
||||
path = compile_string(recipe, zipped_variables)
|
||||
if tag:
|
||||
name = compile_string(tag, zipped_variables)
|
||||
else:
|
||||
# If no tag is provided construct one from all variables to avoid any duplicated tags
|
||||
all_values_as_strings = map(str, zipped_variables.values())
|
||||
name = '-'.join(all_values_as_strings)
|
||||
name = name.lower() # Docker tags must be lower case
|
||||
|
||||
with open(resolve_path(context, path), 'r') as f:
|
||||
rendered = compile_string(f.read(), zipped_variables)
|
||||
|
||||
queue.append((rendered, name))
|
||||
|
||||
if show:
|
||||
print(
|
||||
'\n' +
|
||||
colored('Variation:', 'green') + f'\t{str(zipped_variables)}\n' +
|
||||
colored('Recipe:', 'green') + f'\t\t{path}\n' +
|
||||
colored('Tag:', 'green') + f'\t\t{name}\n'
|
||||
)
|
||||
|
||||
return queue
|
||||
|
||||
|
||||
def login_if_required(registry) -> Optional[str]:
|
||||
if registry:
|
||||
host = registry.get('host')
|
||||
username = registry.get('username')
|
||||
password = registry.get('password')
|
||||
|
||||
if not host:
|
||||
raise Exception(colored('No host set for registry', 'red'))
|
||||
|
||||
cmd = ['docker', 'login']
|
||||
if username or password:
|
||||
if not username or not password:
|
||||
raise Exception(colored('Username or password not set', 'red'))
|
||||
# TODO: use stdin
|
||||
cmd += ['-u', username, '-p', password]
|
||||
|
||||
try:
|
||||
out = exec(cmd + [host])
|
||||
print(colored(out + '\n', 'green'))
|
||||
return host
|
||||
except Exception as e:
|
||||
print(colored('Could not log in, check your username', 'red'))
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def run(config: dict, context):
|
||||
variables = config.get('variables')
|
||||
recipe = config.get('recipe')
|
||||
tag = config.get('tag')
|
||||
registry = config.get('registry')
|
||||
|
||||
host = login_if_required(registry)
|
||||
|
||||
queue = compile_templates(variables, recipe, tag, context)
|
||||
if host:
|
||||
queue = [
|
||||
(rendered, f'{host}/{tag}')
|
||||
for rendered, tag in queue
|
||||
]
|
||||
|
||||
process_queue(
|
||||
queue,
|
||||
context,
|
||||
parallel=config.get('parallel', True),
|
||||
push=config.get('push', False),
|
||||
run=config.get('run', False),
|
||||
)
|
||||
|
||||
|
||||
def resolve_path(base: str, path: str) -> str:
|
||||
if not os.path.isabs(path):
|
||||
# If the path is not abolute take the relative path from the base directory
|
||||
path = os.path.join(base, path)
|
||||
return os.path.abspath(path) # Normalise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Build and push docker matrix build')
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
default='./spec.yml', nargs='?', type=str,
|
||||
help='Config file for the build', metavar='file',
|
||||
)
|
||||
|
||||
args = vars(parser.parse_args())
|
||||
|
||||
config_file = os.path.abspath(args['config'])
|
||||
config_context = os.path.dirname(config_file)
|
||||
if not os.path.isfile(os.path.abspath(config_file)):
|
||||
cprint(f'Config file "{args["config"]}" does not exist', 'red')
|
||||
|
||||
with open(config_file, 'r') as f:
|
||||
config = load(f, Loader=Loader)
|
||||
context = config.get('context', config_context)
|
||||
context = resolve_path(config_context, context)
|
||||
|
||||
if not os.path.isdir(context):
|
||||
cprint(f'Context is not a valid directory. {context}', 'red')
|
||||
|
||||
try:
|
||||
run(config, context)
|
||||
except FileNotFoundError as e:
|
||||
cprint(e, 'red')
|
20
tumbo/__init__.py
Normal file
20
tumbo/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
from tumbo.tumbo import login_if_required, compile_templates, process_queue
|
||||
|
||||
|
||||
def runner(context, variables, recipe, tag=None, registry=None, parallel=True, push=False, run=False):
|
||||
host = login_if_required(registry)
|
||||
|
||||
queue = compile_templates(variables, recipe, tag, context)
|
||||
if host:
|
||||
queue = [
|
||||
(rendered, f'{host}/{tag}')
|
||||
for rendered, tag in queue
|
||||
]
|
||||
|
||||
process_queue(
|
||||
queue,
|
||||
context,
|
||||
parallel=parallel,
|
||||
push=push,
|
||||
run=run,
|
||||
)
|
4
tumbo/__main__.py
Normal file
4
tumbo/__main__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from tumbo.cli import cli
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
41
tumbo/cli.py
Normal file
41
tumbo/cli.py
Normal file
@ -0,0 +1,41 @@
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from termcolor import cprint
|
||||
from yaml import load, Loader
|
||||
|
||||
from tumbo import runner
|
||||
from tumbo.utils import resolve_path
|
||||
|
||||
|
||||
def cli():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Build and push docker matrix build')
|
||||
parser.add_argument(
|
||||
'config',
|
||||
default='./spec.yml', nargs='?', type=str,
|
||||
help='Config file for the build', metavar='file',
|
||||
)
|
||||
|
||||
args = vars(parser.parse_args())
|
||||
|
||||
config_file = os.path.abspath(args['config'])
|
||||
config_context = os.path.dirname(config_file)
|
||||
if not os.path.isfile(os.path.abspath(config_file)):
|
||||
cprint(f'Config file "{args["config"]}" does not exist', 'red')
|
||||
return
|
||||
|
||||
with open(config_file, 'r') as f:
|
||||
config = load(f, Loader=Loader)
|
||||
context = config.get('context', config_context)
|
||||
context = resolve_path(config_context, context)
|
||||
|
||||
if not os.path.isdir(context):
|
||||
cprint(f'Context is not a valid directory. {context}', 'red')
|
||||
|
||||
config['context'] = context
|
||||
|
||||
try:
|
||||
runner(**config)
|
||||
except FileNotFoundError as e:
|
||||
cprint(e, 'red')
|
19
tumbo/utils.py
Normal file
19
tumbo/utils.py
Normal file
@ -0,0 +1,19 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
|
||||
def compile_string(template: str, variables={}):
|
||||
return Template(template).render(**variables)
|
||||
|
||||
|
||||
def buffer_to_string(buffer) -> str:
|
||||
return buffer.decode(sys.getdefaultencoding()).strip()
|
||||
|
||||
|
||||
def resolve_path(base: str, path: str) -> str:
|
||||
if not os.path.isabs(path):
|
||||
# If the path is not absolute take the relative path from the base directory
|
||||
path = os.path.join(base, path)
|
||||
return os.path.abspath(path) # Normalise
|
Loading…
Reference in New Issue
Block a user