Extracted shlax from podctl

This commit is contained in:
jpic
2020-02-14 20:14:56 +01:00
parent c78874a7e0
commit 2e94928aef
51 changed files with 750 additions and 940 deletions
+6
View File
@@ -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
+5
View File
@@ -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
+81
View File
@@ -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)
+87
View File
@@ -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})'
+169
View File
@@ -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})'
+6
View File
@@ -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))
+16
View File
@@ -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
View File
@@ -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')
+21
View File
@@ -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
+22
View File
@@ -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)
+57
View File
@@ -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
View File
@@ -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()
+27
View File
@@ -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]
+2
View File
@@ -0,0 +1,2 @@
from .asyn import Async
from .script import Script
+11
View File
@@ -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
])
+87
View File
@@ -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)):]
+3
View File
@@ -0,0 +1,3 @@
from .buildah import Buildah
from .localhost import Localhost
from .ssh import Ssh
+125
View File
@@ -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()
+9
View File
@@ -0,0 +1,9 @@
import os
from shlax.proc import Proc
from ..strategies.script import Script
class Localhost(Script):
root = '/'
+17
View File
@@ -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