Extracted shlax from podctl
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from .actions import *
|
||||
from .image import Image
|
||||
from .strategies import *
|
||||
from .proc import output, Proc
|
||||
from .targets import *
|
||||
from .shlaxfile import Shlaxfile
|
||||
@@ -0,0 +1,5 @@
|
||||
from .commit import Commit
|
||||
from .packages import Packages # noqa
|
||||
from .base import Action # noqa
|
||||
from .run import Run # noqa
|
||||
from .service import Service
|
||||
@@ -0,0 +1,81 @@
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
|
||||
class Action:
|
||||
parent = None
|
||||
contextualize = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
if not self.parent:
|
||||
if '_context' not in self.__dict__:
|
||||
self._context = dict()
|
||||
return self._context
|
||||
else:
|
||||
return self.parent.context
|
||||
|
||||
def actions_filter(self, results, f=None, **filters):
|
||||
if f:
|
||||
def ff(a):
|
||||
try:
|
||||
return f(a)
|
||||
except:
|
||||
return False
|
||||
results = [*filter(ff, results)]
|
||||
|
||||
for k, v in filters.items():
|
||||
if k == 'type':
|
||||
results = [*filter(
|
||||
lambda s: type(s).__name__.lower() == str(v).lower(),
|
||||
results
|
||||
)]
|
||||
else:
|
||||
results = [*filter(
|
||||
lambda s: getattr(s, k, None) == v,
|
||||
results
|
||||
)]
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def sibblings(self, f=None, **filters):
|
||||
return self.actions_filter(
|
||||
[a for a in self.parent.actions if a is not self],
|
||||
f, **filters
|
||||
)
|
||||
|
||||
def parents(self, f=None, **filters):
|
||||
if self.parent:
|
||||
return self.actions_filter(
|
||||
[self.parent] + self.parent.parents(),
|
||||
f, **filters
|
||||
)
|
||||
return []
|
||||
|
||||
def children(self, f=None, **filters):
|
||||
children = []
|
||||
def add(parent):
|
||||
if parent != self:
|
||||
children.append(parent)
|
||||
if 'actions' not in parent.__dict__:
|
||||
return
|
||||
|
||||
for action in parent.actions:
|
||||
add(action)
|
||||
add(self)
|
||||
return self.actions_filter(children, f, **filters)
|
||||
|
||||
def __getattr__(self, name):
|
||||
for a in self.parents() + self.sibblings() + self.children():
|
||||
if name in a.contextualize:
|
||||
return getattr(a, name)
|
||||
raise AttributeError(name)
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
print(f'{self}.__call__(*args, **kwargs) not implemented')
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from .base import Action
|
||||
|
||||
from ..exceptions import WrongResult
|
||||
|
||||
CI_VARS = (
|
||||
# gitlab
|
||||
'CI_COMMIT_SHORT_SHA',
|
||||
'CI_COMMIT_REF_NAME',
|
||||
'CI_COMMIT_TAG',
|
||||
# CircleCI
|
||||
'CIRCLE_SHA1',
|
||||
'CIRCLE_TAG',
|
||||
'CIRCLE_BRANCH',
|
||||
)
|
||||
|
||||
|
||||
class Commit(Action):
|
||||
def __init__(self, repo, tags=None, format=None, push=None, registry=None):
|
||||
self.repo = repo
|
||||
self.registry = registry or 'localhost'
|
||||
self.push = push or os.getenv('CI')
|
||||
|
||||
# figure out registry host
|
||||
if '/' in self.repo and not registry:
|
||||
first = self.repo.split('/')[0]
|
||||
if '.' in first or ':' in first:
|
||||
self.registry = self.repo.split('/')[0]
|
||||
|
||||
# docker.io currently has issues with oci format
|
||||
self.format = format or 'oci'
|
||||
if self.registry == 'docker.io':
|
||||
self.format = 'docker'
|
||||
|
||||
self.tags = tags or []
|
||||
|
||||
# figure tags from CI vars
|
||||
if not self.tags:
|
||||
for name in CI_VARS:
|
||||
value = os.getenv(name)
|
||||
if value:
|
||||
self.tags.append(value)
|
||||
|
||||
# filter out tags which resolved to None
|
||||
self.tags = [t for t in self.tags if t is not None]
|
||||
|
||||
# default tag by default ...
|
||||
if not self.tags:
|
||||
self.tags = ['latest']
|
||||
|
||||
async def __call__(self, *args, ctr=None, **kwargs):
|
||||
self.sha = (await self.parent.parent.exec(
|
||||
'buildah',
|
||||
'commit',
|
||||
'--format=' + self.format,
|
||||
ctr,
|
||||
)).out
|
||||
|
||||
if 'master' in self.tags:
|
||||
self.tags.append('latest')
|
||||
|
||||
if self.tags:
|
||||
tags = ' '.join([f'{self.repo}:{tag}' for tag in self.tags])
|
||||
await script.exec('buildah', 'tag', self.sha, self.repo, tags)
|
||||
|
||||
if self.push:
|
||||
user = os.getenv('DOCKER_USER')
|
||||
passwd = os.getenv('DOCKER_PASS')
|
||||
if user and passwd and os.getenv('CI') and self.registry:
|
||||
await script.exec(
|
||||
'podman',
|
||||
'login',
|
||||
'-u',
|
||||
user,
|
||||
'-p',
|
||||
passwd,
|
||||
self.registry,
|
||||
)
|
||||
|
||||
for tag in self.tags:
|
||||
await script.exec('podman', 'push', f'{self.repo}:{tag}')
|
||||
await script.umount()
|
||||
|
||||
def __repr__(self):
|
||||
return f'Commit({self.registry}/{self.repo}:{self.tags})'
|
||||
@@ -0,0 +1,169 @@
|
||||
import asyncio
|
||||
import copy
|
||||
|
||||
from datetime import datetime
|
||||
from glob import glob
|
||||
import os
|
||||
import subprocess
|
||||
from textwrap import dedent
|
||||
|
||||
from .base import Action
|
||||
|
||||
|
||||
class Packages(Action):
|
||||
"""
|
||||
The Packages visitor wraps around the container's package manager.
|
||||
|
||||
It's a central piece of the build process, and does iterate over other
|
||||
container visitors in order to pick up packages. For example, the Pip
|
||||
visitor will declare ``self.packages = dict(apt=['python3-pip'])``, and the
|
||||
Packages visitor will pick it up.
|
||||
"""
|
||||
contextualize = ['mgr']
|
||||
|
||||
mgrs = dict(
|
||||
apk=dict(
|
||||
update='apk update',
|
||||
upgrade='apk upgrade',
|
||||
install='apk add',
|
||||
),
|
||||
apt=dict(
|
||||
update='apt-get -y update',
|
||||
upgrade='apt-get -y upgrade',
|
||||
install='apt-get -y --no-install-recommends install',
|
||||
),
|
||||
pacman=dict(
|
||||
update='pacman -Sy',
|
||||
upgrade='pacman -Su --noconfirm',
|
||||
install='pacman -S --noconfirm',
|
||||
),
|
||||
dnf=dict(
|
||||
update='dnf makecache --assumeyes',
|
||||
upgrade='dnf upgrade --best --assumeyes --skip-broken', # noqa
|
||||
install='dnf install --setopt=install_weak_deps=False --best --assumeyes', # noqa
|
||||
),
|
||||
yum=dict(
|
||||
update='yum update',
|
||||
upgrade='yum upgrade',
|
||||
install='yum install',
|
||||
),
|
||||
)
|
||||
|
||||
installed = []
|
||||
|
||||
def __init__(self, *packages, **kwargs):
|
||||
self.packages = []
|
||||
|
||||
for package in packages:
|
||||
line = dedent(package).strip().replace('\n', ' ')
|
||||
self.packages += line.split(' ')
|
||||
|
||||
self.mgr = kwargs.pop('mgr') if 'mgr' in kwargs else None
|
||||
|
||||
@property
|
||||
def cache_root(self):
|
||||
if 'CACHE_DIR' in os.environ:
|
||||
return os.path.join(os.getenv('CACHE_DIR'))
|
||||
else:
|
||||
return os.path.join(os.getenv('HOME'), '.cache')
|
||||
|
||||
async def update(self):
|
||||
# run pkgmgr_setup functions ie. apk_setup
|
||||
cachedir = await getattr(self, self.mgr + '_setup')()
|
||||
|
||||
lastupdate = None
|
||||
if os.path.exists(cachedir + '/lastupdate'):
|
||||
with open(cachedir + '/lastupdate', 'r') as f:
|
||||
try:
|
||||
lastupdate = int(f.read().strip())
|
||||
except:
|
||||
pass
|
||||
|
||||
if not os.path.exists(cachedir):
|
||||
os.makedirs(cachedir)
|
||||
|
||||
now = int(datetime.now().strftime('%s'))
|
||||
# cache for a week
|
||||
if not lastupdate or now - lastupdate > 604800:
|
||||
# crude lockfile implementation, should work against *most*
|
||||
# race-conditions ...
|
||||
lockfile = cachedir + '/update.lock'
|
||||
if not os.path.exists(lockfile):
|
||||
with open(lockfile, 'w+') as f:
|
||||
f.write(str(os.getpid()))
|
||||
|
||||
try:
|
||||
await self.rexec(self.cmds['update'])
|
||||
finally:
|
||||
os.unlink(lockfile)
|
||||
|
||||
with open(cachedir + '/lastupdate', 'w+') as f:
|
||||
f.write(str(now))
|
||||
else:
|
||||
while os.path.exists(lockfile):
|
||||
print(f'{self.container.name} | Waiting for update ...')
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
cached = getattr(self, '_pagkages_mgr', None)
|
||||
if cached:
|
||||
self.mgr = cached
|
||||
else:
|
||||
mgr = await self.which(*self.mgrs.values())
|
||||
if mgr:
|
||||
self.mgr = mgr.split('/')[-1]
|
||||
|
||||
if not self.mgr:
|
||||
raise Exception('Packages does not yet support this distro')
|
||||
|
||||
self.cmds = self.mgrs[self.mgr]
|
||||
if not getattr(self, '_packages_upgraded', None):
|
||||
await self.update()
|
||||
await self.rexec(self.cmds['upgrade'])
|
||||
|
||||
# first run on container means inject visitor packages
|
||||
packages = []
|
||||
for sibbling in self.sibblings:
|
||||
pp = getattr(sibbling, 'packages', None)
|
||||
if pp:
|
||||
if isinstance(pp, list):
|
||||
packages += pp
|
||||
elif self.mgr in pp:
|
||||
packages += pp[self.mgr]
|
||||
|
||||
self._packages_upgraded = True
|
||||
else:
|
||||
packages = self.packages
|
||||
|
||||
await self.rexec(*self.cmds['install'].split(' ') + packages)
|
||||
|
||||
async def apk_setup(self):
|
||||
cachedir = os.path.join(self.cache_root, self.mgr)
|
||||
await self.mount(cachedir, '/var/cache/apk')
|
||||
# special step to enable apk cache
|
||||
await self.rexec('ln -s /var/cache/apk /etc/apk/cache')
|
||||
return cachedir
|
||||
|
||||
async def dnf_setup(self):
|
||||
cachedir = os.path.join(self.cache_root, self.mgr)
|
||||
await self.mount(cachedir, f'/var/cache/{self.mgr}')
|
||||
await self.run('echo keepcache=True >> /etc/dnf/dnf.conf')
|
||||
return cachedir
|
||||
|
||||
async def apt_setup(self):
|
||||
codename = (await self.rexec(
|
||||
f'source {self.mnt}/etc/os-release; echo $VERSION_CODENAME'
|
||||
)).out
|
||||
cachedir = os.path.join(self.cache_root, self.mgr, codename)
|
||||
await self.rexec('rm /etc/apt/apt.conf.d/docker-clean')
|
||||
cache_archives = os.path.join(cachedir, 'archives')
|
||||
await self.mount(cache_archives, f'/var/cache/apt/archives')
|
||||
cache_lists = os.path.join(cachedir, 'lists')
|
||||
await self.mount(cache_lists, f'/var/lib/apt/lists')
|
||||
return cachedir
|
||||
|
||||
async def pacman_setup(self):
|
||||
return self.cache_root + '/pacman'
|
||||
|
||||
def __repr__(self):
|
||||
return f'Packages({self.packages})'
|
||||
@@ -0,0 +1,6 @@
|
||||
from .base import Action
|
||||
|
||||
|
||||
class Run(Action):
|
||||
async def __call__(self, *args, **kwargs):
|
||||
return (await self.exec(*self.args, **self.kwargs))
|
||||
@@ -0,0 +1,16 @@
|
||||
import asyncio
|
||||
|
||||
from .base import Action
|
||||
|
||||
|
||||
class Service(Action):
|
||||
def __init__(self, *names, state=None):
|
||||
self.state = state or 'started'
|
||||
self.names = names
|
||||
super().__init__()
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
return asyncio.gather(*[
|
||||
self.exec('systemctl', 'start', name, user='root')
|
||||
for name in self.names
|
||||
])
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
'''
|
||||
shlax is a micro-framework to orchestrate commands.
|
||||
|
||||
shlax yourfile.py: to list actions you have declared.
|
||||
shlax yourfile.py <action>: to execute a given action
|
||||
#!/usr/bin/env shlax: when making yourfile.py an executable.
|
||||
'''
|
||||
|
||||
import asyncio
|
||||
import cli2
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .exceptions import *
|
||||
from .shlaxfile import Shlaxfile
|
||||
from .targets import Localhost
|
||||
|
||||
|
||||
async def runall(*args, **kwargs):
|
||||
for name, action in cli.shlaxfile.actions.items():
|
||||
await Localhost(action)(*args, **kwargs)
|
||||
|
||||
|
||||
@cli2.option('debug', alias='d', help='Display debug output.')
|
||||
async def test(*args, **kwargs):
|
||||
breakpoint()
|
||||
"""Run podctl test over a bunch of paths."""
|
||||
report = []
|
||||
|
||||
for arg in args:
|
||||
candidates = [
|
||||
os.path.join(os.getcwd(), arg, 'pod.py'),
|
||||
os.path.join(os.getcwd(), arg, 'pod_test.py'),
|
||||
]
|
||||
for candidate in candidates:
|
||||
if not os.path.exists(candidate):
|
||||
continue
|
||||
podfile = Podfile.factory(candidate)
|
||||
|
||||
# disable push
|
||||
for name, container in podfile.containers.items():
|
||||
commit = container.visitor('commit')
|
||||
if commit:
|
||||
commit.push = False
|
||||
|
||||
output.print(
|
||||
'\n\x1b[1;38;5;160;48;5;118m BUILD START \x1b[0m'
|
||||
+ ' ' + podfile.path + '\n'
|
||||
)
|
||||
|
||||
old_exit_code = console_script.exit_code
|
||||
console_script.exit_code = 0
|
||||
try:
|
||||
await podfile.pod.script('build')()
|
||||
except Exception as e:
|
||||
report.append(('build ' + candidate, False))
|
||||
continue
|
||||
|
||||
if console_script.exit_code != 0:
|
||||
report.append(('build ' + candidate, False))
|
||||
continue
|
||||
console_script.exit_code = old_exit_code
|
||||
|
||||
for name, test in podfile.tests.items():
|
||||
name = '::'.join([podfile.path, name])
|
||||
output.print(
|
||||
'\n\x1b[1;38;5;160;48;5;118m TEST START \x1b[0m'
|
||||
+ ' ' + name + '\n'
|
||||
)
|
||||
|
||||
try:
|
||||
await test(podfile.pod)
|
||||
except Exception as e:
|
||||
report.append((name, False))
|
||||
output.print('\x1b[1;38;5;15;48;5;196m TEST FAIL \x1b[0m' + name)
|
||||
else:
|
||||
report.append((name, True))
|
||||
output.print('\x1b[1;38;5;200;48;5;44m TEST SUCCESS \x1b[0m' + name)
|
||||
output.print('\n')
|
||||
|
||||
print('\n')
|
||||
|
||||
for name, success in report:
|
||||
if success:
|
||||
output.print('\n\x1b[1;38;5;200;48;5;44m TEST SUCCESS \x1b[0m' + name)
|
||||
else:
|
||||
output.print('\n\x1b[1;38;5;15;48;5;196m TEST FAIL \x1b[0m' + name)
|
||||
|
||||
print('\n')
|
||||
|
||||
success = [*filter(lambda i: i[1], report)]
|
||||
failures = [*filter(lambda i: not i[1], report)]
|
||||
|
||||
output.print(
|
||||
'\n\x1b[1;38;5;200;48;5;44m TEST TOTAL: \x1b[0m'
|
||||
+ str(len(report))
|
||||
)
|
||||
if success:
|
||||
output.print(
|
||||
'\n\x1b[1;38;5;200;48;5;44m TEST SUCCESS: \x1b[0m'
|
||||
+ str(len(success))
|
||||
)
|
||||
if failures:
|
||||
output.print(
|
||||
'\n\x1b[1;38;5;15;48;5;196m TEST FAIL: \x1b[0m'
|
||||
+ str(len(failures))
|
||||
)
|
||||
|
||||
if failures:
|
||||
console_script.exit_code = 1
|
||||
|
||||
|
||||
class ConsoleScript(cli2.ConsoleScript):
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.shlaxfile = None
|
||||
shlaxfile = sys.argv.pop(1) if len(sys.argv) > 1 else ''
|
||||
if os.path.exists(shlaxfile.split('::')[0]):
|
||||
self.shlaxfile = Shlaxfile()
|
||||
self.shlaxfile.parse(shlaxfile)
|
||||
for name, action in self.shlaxfile.actions.items():
|
||||
async def cb(*args, **kwargs):
|
||||
return await Localhost(action)(*args, **kwargs)
|
||||
self[name] = cli2.Callable(
|
||||
name,
|
||||
cb,
|
||||
color=getattr(action, 'color', cli2.YELLOW),
|
||||
)
|
||||
return super().__call__(*args, **kwargs)
|
||||
|
||||
def call(self, command):
|
||||
args = self.parser.funcargs
|
||||
kwargs = self.parser.funckwargs
|
||||
breakpoint()
|
||||
return command(*args, **kwargs)
|
||||
|
||||
def call(self, command):
|
||||
try:
|
||||
return super().call(command)
|
||||
except WrongResult as e:
|
||||
print(e)
|
||||
self.exit_code = e.proc.rc
|
||||
except ShlaxException as e:
|
||||
print(e)
|
||||
self.exit_code = 1
|
||||
|
||||
|
||||
cli = ConsoleScript(__doc__).add_module('shlax.cli')
|
||||
@@ -0,0 +1,21 @@
|
||||
import yaml
|
||||
|
||||
from shlax import *
|
||||
|
||||
|
||||
class GitLabCIConfig(Script):
|
||||
async def __call__(self, *args, write=True, **kwargs):
|
||||
await super().__call__(*args, **kwargs)
|
||||
self.kwargs = kwargs
|
||||
for name, definition in self.context.items():
|
||||
self.kwargs[name] = definition
|
||||
output = yaml.dump(self.kwargs)
|
||||
print(output)
|
||||
if write:
|
||||
with open('.gitlab-ci.yml', 'w+') as f:
|
||||
f.write(output)
|
||||
|
||||
|
||||
class Job(Action):
|
||||
async def __call__(self, *args, **kwargs):
|
||||
self.context[self.args[0]] = self.kwargs
|
||||
@@ -0,0 +1,22 @@
|
||||
class ShlaxException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Mistake(ShlaxException):
|
||||
pass
|
||||
|
||||
|
||||
class WrongResult(ShlaxException):
|
||||
def __init__(self, proc):
|
||||
self.proc = proc
|
||||
|
||||
msg = f'FAIL exit with {proc.rc} ' + proc.args[0]
|
||||
|
||||
if not proc.debug or 'cmd' not in str(proc.debug):
|
||||
msg += '\n' + proc.cmd
|
||||
|
||||
if not proc.debug or 'out' not in str(proc.debug):
|
||||
msg += '\n' + proc.out
|
||||
msg += '\n' + proc.err
|
||||
|
||||
super().__init__(msg)
|
||||
@@ -0,0 +1,57 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
class Image:
|
||||
ENV_TAGS = (
|
||||
# gitlab
|
||||
'CI_COMMIT_SHORT_SHA',
|
||||
'CI_COMMIT_REF_NAME',
|
||||
'CI_COMMIT_TAG',
|
||||
# CircleCI
|
||||
'CIRCLE_SHA1',
|
||||
'CIRCLE_TAG',
|
||||
'CIRCLE_BRANCH',
|
||||
# contributions welcome here
|
||||
)
|
||||
|
||||
PATTERN = re.compile(
|
||||
'^((?P<backend>[a-z]*)://)?((?P<registry>[^/]*[.][^/]*)/)?((?P<repository>[^:]+))?(:(?P<tags>.*))?$' # noqa
|
||||
, re.I
|
||||
)
|
||||
|
||||
def __init__(self, arg=None, format=None, backend=None, registry=None, repository=None, tags=None):
|
||||
self.arg = arg
|
||||
self.format = format
|
||||
self.backend = backend
|
||||
self.registry = registry
|
||||
self.repository = repository
|
||||
self.tags = tags or []
|
||||
|
||||
match = re.match(self.PATTERN, arg)
|
||||
if match:
|
||||
for k, v in match.groupdict().items():
|
||||
if getattr(self, k):
|
||||
continue
|
||||
if not v:
|
||||
continue
|
||||
if k == 'tags':
|
||||
v = v.split(',')
|
||||
setattr(self, k, v)
|
||||
|
||||
# docker.io currently has issues with oci format
|
||||
self.format = format or 'oci'
|
||||
if self.registry == 'docker.io':
|
||||
self.format = 'docker'
|
||||
|
||||
# figure tags from CI vars
|
||||
for name in self.ENV_TAGS:
|
||||
value = os.getenv(name)
|
||||
if value:
|
||||
self.tags.append(value)
|
||||
|
||||
# filter out tags which resolved to None
|
||||
self.tags = [t for t in self.tags if t]
|
||||
|
||||
# default tag by default ...
|
||||
if not self.tags:
|
||||
self.tags = ['latest']
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
"""
|
||||
Asynchronous process execution wrapper.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from colorama import Fore, Back, Style
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
from .exceptions import WrongResult
|
||||
import pygments
|
||||
from pygments import lexers
|
||||
from pygments.formatters import TerminalFormatter
|
||||
from pygments.formatters import Terminal256Formatter
|
||||
|
||||
|
||||
class Output:
|
||||
colors = (
|
||||
'\x1b[1;36;45m',
|
||||
'\x1b[1;36;41m',
|
||||
'\x1b[1;36;40m',
|
||||
'\x1b[1;37;45m',
|
||||
'\x1b[1;32m',
|
||||
'\x1b[1;37;44m',
|
||||
)
|
||||
def __init__(self):
|
||||
self.prefixes = dict()
|
||||
self.prefix_length = 0
|
||||
|
||||
def __call__(self, line, prefix, highlight=True, flush=True):
|
||||
if prefix and prefix not in self.prefixes:
|
||||
self.prefixes[prefix] = (
|
||||
self.colors[len([*self.prefixes.keys()]) - 1]
|
||||
)
|
||||
if len(prefix) > self.prefix_length:
|
||||
self.prefix_length = len(prefix)
|
||||
|
||||
prefix_color = self.prefixes[prefix] if prefix else ''
|
||||
prefix_padding = '.' * (self.prefix_length - len(prefix) - 2) if prefix else ''
|
||||
if prefix_padding:
|
||||
prefix_padding = ' ' + prefix_padding + ' '
|
||||
|
||||
sys.stdout.buffer.write((
|
||||
(
|
||||
prefix_color
|
||||
+ prefix_padding
|
||||
+ prefix
|
||||
+ ' '
|
||||
+ Back.RESET
|
||||
+ Style.RESET_ALL
|
||||
+ Fore.LIGHTBLACK_EX
|
||||
+ '| '
|
||||
+ Style.RESET_ALL
|
||||
if prefix
|
||||
else ''
|
||||
)
|
||||
+ self.highlight(line, highlight)
|
||||
).encode('utf8'))
|
||||
|
||||
if flush:
|
||||
sys.stdout.flush()
|
||||
|
||||
def cmd(self, line, prefix):
|
||||
self(
|
||||
Fore.LIGHTBLACK_EX
|
||||
+ '+ '
|
||||
+ Style.RESET_ALL
|
||||
+ self.highlight(line, 'bash'),
|
||||
prefix,
|
||||
highlight=False
|
||||
)
|
||||
|
||||
def print(self, content):
|
||||
self(
|
||||
content,
|
||||
prefix=None,
|
||||
highlight=False
|
||||
)
|
||||
|
||||
def highlight(self, line, highlight=True):
|
||||
line = line.decode('utf8') if isinstance(line, bytes) else line
|
||||
if not highlight or (
|
||||
'\x1b[' in line
|
||||
or '\033[' in line
|
||||
or '\\e[' in line
|
||||
):
|
||||
return line
|
||||
elif isinstance(highlight, str):
|
||||
lexer = lexers.get_lexer_by_name(highlight)
|
||||
else:
|
||||
lexer = lexers.get_lexer_by_name('python')
|
||||
formatter = Terminal256Formatter(
|
||||
style=os.getenv('PODCTL_STYLE', 'fruity'))
|
||||
return pygments.highlight(line, lexer, formatter)
|
||||
|
||||
|
||||
output = Output()
|
||||
|
||||
|
||||
class PrefixStreamProtocol(asyncio.subprocess.SubprocessStreamProtocol):
|
||||
"""
|
||||
Internal subprocess stream protocol to add a prefix in front of output to
|
||||
make asynchronous output readable.
|
||||
"""
|
||||
|
||||
def __init__(self, prefix, *args, **kwargs):
|
||||
self.debug = kwargs.get('debug', True)
|
||||
self.prefix = prefix
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def pipe_data_received(self, fd, data):
|
||||
if (self.debug is True or 'out' in str(self.debug)) and fd in (1, 2):
|
||||
output(data, self.prefix, flush=False)
|
||||
sys.stdout.flush()
|
||||
super().pipe_data_received(fd, data)
|
||||
|
||||
|
||||
def protocol_factory(prefix):
|
||||
def _p():
|
||||
return PrefixStreamProtocol(
|
||||
prefix,
|
||||
limit=asyncio.streams._DEFAULT_LIMIT,
|
||||
loop=asyncio.events.get_event_loop()
|
||||
)
|
||||
return _p
|
||||
|
||||
|
||||
class Proc:
|
||||
"""
|
||||
Subprocess wrapper.
|
||||
|
||||
Example usage::
|
||||
|
||||
proc = Proc('find', '/', prefix='containername')
|
||||
|
||||
await proc() # execute
|
||||
|
||||
print(proc.out) # stdout
|
||||
print(proc.err) # stderr
|
||||
print(proc.rc) # return code
|
||||
"""
|
||||
test = False
|
||||
|
||||
def __init__(self, *args, prefix=None, raises=True, debug=True):
|
||||
self.debug = debug if not self.test else False
|
||||
self.cmd = ' '.join(args)
|
||||
self.args = args
|
||||
self.prefix = prefix
|
||||
self.raises = raises
|
||||
self.called = False
|
||||
self.communicated = False
|
||||
self.out_raw = b''
|
||||
self.err_raw = b''
|
||||
self.out = ''
|
||||
self.err = ''
|
||||
self.rc = None
|
||||
|
||||
@staticmethod
|
||||
def split(*args):
|
||||
args = [str(a) for a in args]
|
||||
if len(args) == 1:
|
||||
if isinstance(args[0], (list, tuple)):
|
||||
args = args[0]
|
||||
else:
|
||||
args = ['sh', '-euc', ' '.join(args)]
|
||||
return args
|
||||
|
||||
async def __call__(self, wait=True):
|
||||
if self.called:
|
||||
raise Exception('Already called: ' + self.cmd)
|
||||
|
||||
if self.debug is True or 'cmd' in str(self.debug):
|
||||
output.cmd(self.cmd, self.prefix)
|
||||
|
||||
if self.test:
|
||||
if self.test is True:
|
||||
type(self).test = []
|
||||
self.test.append(self.args)
|
||||
return self
|
||||
|
||||
loop = asyncio.events.get_event_loop()
|
||||
transport, protocol = await loop.subprocess_exec(
|
||||
protocol_factory(self.prefix), *self.args)
|
||||
self.proc = asyncio.subprocess.Process(transport, protocol, loop)
|
||||
self.called = True
|
||||
|
||||
if wait:
|
||||
await self.wait()
|
||||
|
||||
return self
|
||||
|
||||
async def communicate(self):
|
||||
self.out_raw, self.err_raw = await self.proc.communicate()
|
||||
self.out = self.out_raw.decode('utf8').strip()
|
||||
self.err = self.err_raw.decode('utf8').strip()
|
||||
self.rc = self.proc.returncode
|
||||
self.communicated = True
|
||||
return self
|
||||
|
||||
async def wait(self):
|
||||
if self.test:
|
||||
return self
|
||||
if not self.called:
|
||||
await self()
|
||||
if not self.communicated:
|
||||
await self.communicate()
|
||||
if self.raises and self.proc.returncode:
|
||||
raise WrongResult(self)
|
||||
return self
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
import json
|
||||
return json.loads(self.out)
|
||||
|
||||
def mock():
|
||||
"""Context manager for testing purpose."""
|
||||
cls = Proc
|
||||
class Mock:
|
||||
def __enter__(_):
|
||||
cls.test = True
|
||||
def __exit__(_, exc_type, exc_value, traceback):
|
||||
cls.test = False
|
||||
return Mock()
|
||||
@@ -0,0 +1,27 @@
|
||||
import importlib
|
||||
import os
|
||||
|
||||
from .actions.base import Action
|
||||
|
||||
|
||||
class Shlaxfile:
|
||||
def __init__(self, actions=None, tests=None):
|
||||
self.actions = actions or {}
|
||||
self.tests = tests or {}
|
||||
self.paths = []
|
||||
|
||||
def parse(self, path):
|
||||
spec = importlib.util.spec_from_file_location('shlaxfile', path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
for name, value in mod.__dict__.items():
|
||||
if isinstance(value, Action):
|
||||
value.name = name
|
||||
self.actions[name] = value
|
||||
elif callable(value) and getattr(value, '__name__', '').startswith('test_'):
|
||||
self.tests[value.__name__] = value
|
||||
self.paths.append(path)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self.paths[0]
|
||||
@@ -0,0 +1,2 @@
|
||||
from .asyn import Async
|
||||
from .script import Script
|
||||
@@ -0,0 +1,11 @@
|
||||
import asyncio
|
||||
|
||||
from .script import Script
|
||||
|
||||
|
||||
class Async(Script):
|
||||
async def __call__(self, *args, **kwargs):
|
||||
return asyncio.gather(*[
|
||||
procs.append(action(*args, **kwargs))
|
||||
for action in self.actions
|
||||
])
|
||||
@@ -0,0 +1,87 @@
|
||||
import copy
|
||||
import os
|
||||
|
||||
from ..exceptions import WrongResult
|
||||
from ..actions.base import Action
|
||||
from ..proc import Proc
|
||||
|
||||
|
||||
class Actions(list):
|
||||
def __init__(self, owner, actions):
|
||||
self.owner = owner
|
||||
super().__init__()
|
||||
for action in actions:
|
||||
self.append(action)
|
||||
|
||||
def append(self, value):
|
||||
value.parent = self.owner
|
||||
value.status = 'pending'
|
||||
super().append(value)
|
||||
|
||||
|
||||
class Script(Action):
|
||||
root = '/'
|
||||
contextualize = ['shargs', 'exec', 'rexec', 'env', 'which']
|
||||
|
||||
def __init__(self, *actions, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.actions = Actions(self, actions)
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
for action in self.actions:
|
||||
try:
|
||||
await action(*args, **kwargs)
|
||||
except WrongResult as e:
|
||||
print(e)
|
||||
action.status = 'fail'
|
||||
break
|
||||
else:
|
||||
if action.status == 'running':
|
||||
action.status = 'success'
|
||||
|
||||
def shargs(self, *args, **kwargs):
|
||||
user = kwargs.pop('user', None)
|
||||
kwargs['debug'] = True
|
||||
args = [str(arg) for arg in args if args is not None]
|
||||
|
||||
if args and ' ' in args[0]:
|
||||
if len(args) == 1:
|
||||
args = ['sh', '-euc', args[0]]
|
||||
else:
|
||||
args = ['sh', '-euc'] + list(args)
|
||||
|
||||
if user == 'root':
|
||||
args = ['sudo'] + args
|
||||
elif user:
|
||||
args = ['sudo', '-u', user] + args
|
||||
|
||||
if self.parent:
|
||||
return self.parent.shargs(*args, **kwargs)
|
||||
else:
|
||||
return args, kwargs
|
||||
|
||||
async def exec(self, *args, **kwargs):
|
||||
args, kwargs = self.shargs(*args, **kwargs)
|
||||
proc = await Proc(*args, **kwargs)()
|
||||
if kwargs.get('wait', True):
|
||||
await proc.wait()
|
||||
return proc
|
||||
|
||||
async def rexec(self, *args, **kwargs):
|
||||
kwargs['user'] = 'root'
|
||||
return await self.exec(*args, **kwargs)
|
||||
|
||||
async def env(self, name):
|
||||
return (await self.exec('echo $' + name)).out
|
||||
|
||||
async def which(self, *cmd):
|
||||
"""
|
||||
Return the first path to the cmd in the container.
|
||||
|
||||
If cmd argument is a list then it will try all commands.
|
||||
"""
|
||||
for path in (await self.env('PATH')).split(':'):
|
||||
for c in cmd:
|
||||
p = os.path.join(self.root, path[1:], c)
|
||||
if os.path.exists(p):
|
||||
return p[len(str(self.root)):]
|
||||
@@ -0,0 +1,3 @@
|
||||
from .buildah import Buildah
|
||||
from .localhost import Localhost
|
||||
from .ssh import Ssh
|
||||
@@ -0,0 +1,125 @@
|
||||
import asyncio
|
||||
import os
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from ..proc import Proc, output
|
||||
from ..image import Image
|
||||
from .localhost import Localhost
|
||||
|
||||
|
||||
class Buildah(Localhost):
|
||||
"""
|
||||
The build script iterates over visitors and runs the build functions, it
|
||||
also provides wrappers around the buildah command.
|
||||
"""
|
||||
contextualize = Localhost.contextualize + ['mnt', 'ctr']
|
||||
|
||||
def __init__(self, base, *args, commit=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.base = base
|
||||
self.mounts = dict()
|
||||
self.ctr = None
|
||||
self.mnt = None
|
||||
self.commit = commit
|
||||
|
||||
def shargs(self, *args, user=None, buildah=True, **kwargs):
|
||||
if not buildah:
|
||||
return super().shargs(*args, user=user, **kwargs)
|
||||
|
||||
_args = ['buildah', 'run']
|
||||
if user:
|
||||
_args += ['--user', user]
|
||||
_args += [self.ctr, '--', 'sh', '-euc']
|
||||
return super().shargs(
|
||||
*(
|
||||
_args
|
||||
+ [' '.join([str(a) for a in args])]
|
||||
),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'Base({self.base})'
|
||||
|
||||
async def config(self, line):
|
||||
"""Run buildah config."""
|
||||
return await self.exec(f'buildah config {line} {self.ctr}')
|
||||
|
||||
async def copy(self, src, dst):
|
||||
"""Run buildah copy to copy a file from host into container."""
|
||||
return await self.exec(f'buildah copy {self.ctr} {src} {dst}')
|
||||
|
||||
async def mount(self, src, dst):
|
||||
"""Mount a host directory into the container."""
|
||||
target = self.mnt / str(dst)[1:]
|
||||
await super().exec(f'mkdir -p {src} {target}')
|
||||
await super().exec(f'mount -o bind {src} {target}')
|
||||
self.mounts[src] = dst
|
||||
|
||||
async def umounts(self):
|
||||
"""Unmount all mounted directories from the container."""
|
||||
for src, dst in self.mounts.items():
|
||||
await super().exec('umount', self.mnt / str(dst)[1:])
|
||||
|
||||
async def umount(self):
|
||||
"""Unmount the buildah container with buildah unmount."""
|
||||
if self.ctr:
|
||||
await super().exec(f'buildah unmount {self.ctr}')
|
||||
|
||||
async def which(self, *cmd):
|
||||
"""
|
||||
Return the first path to the cmd in the container.
|
||||
|
||||
If cmd argument is a list then it will try all commands.
|
||||
"""
|
||||
paths = (await self.env('PATH')).split(':')
|
||||
for path in paths:
|
||||
for c in cmd:
|
||||
p = os.path.join(self.mnt, path[1:], c)
|
||||
if os.path.exists(p):
|
||||
return p[len(str(self.mnt)):]
|
||||
|
||||
def __repr__(self):
|
||||
return f'Build'
|
||||
|
||||
async def __call__(self, *args, debug=False, **kwargs):
|
||||
if Proc.test or os.getuid() == 0 or self.parent.parent:
|
||||
self.ctr = (await self.exec('buildah', 'from', self.base, buildah=False)).out
|
||||
self.mnt = Path((await self.exec('buildah', 'mount', self.ctr, buildah=False)).out)
|
||||
|
||||
result = await super().__call__(*args, **kwargs)
|
||||
#await self.umounts()
|
||||
#await self.umount()
|
||||
await self.exec('buildah', 'rm', self.ctr, raises=False, buildah=False)
|
||||
return result
|
||||
|
||||
from shlax.cli import cli
|
||||
# restart under buildah unshare environment
|
||||
argv = [
|
||||
'buildah', 'unshare',
|
||||
sys.argv[0], # current script location
|
||||
]
|
||||
if debug is True:
|
||||
argv.append('-d')
|
||||
elif isinstance(debug, str):
|
||||
argv.append('-d=' + debug)
|
||||
argv += [
|
||||
cli.shlaxfile.path,
|
||||
cli.parser.command.name, # script name ?
|
||||
]
|
||||
output(' '.join(argv), 'EXECUTION', flush=True)
|
||||
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
shlex.join(argv),
|
||||
stderr=sys.stderr,
|
||||
stdin=sys.stdin,
|
||||
stdout=sys.stdout,
|
||||
)
|
||||
await proc.communicate()
|
||||
cli.exit_code = await proc.wait()
|
||||
@@ -0,0 +1,9 @@
|
||||
import os
|
||||
|
||||
from shlax.proc import Proc
|
||||
|
||||
from ..strategies.script import Script
|
||||
|
||||
|
||||
class Localhost(Script):
|
||||
root = '/'
|
||||
@@ -0,0 +1,17 @@
|
||||
import os
|
||||
|
||||
from shlax.proc import Proc
|
||||
|
||||
from .localhost import Localhost
|
||||
|
||||
|
||||
class Ssh(Localhost):
|
||||
root = '/'
|
||||
|
||||
def __init__(self, host, *args, **kwargs):
|
||||
self.host = host
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def shargs(self, *args, **kwargs):
|
||||
args, kwargs = super().shargs(*args, **kwargs)
|
||||
return (['ssh', self.host] + list(args)), kwargs
|
||||
Reference in New Issue
Block a user