Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eef6241a7 | ||
|
|
879bb6f79e | ||
|
|
e733694f2d | ||
|
|
0ec287c977 | ||
|
|
833cef7527 | ||
|
|
28f555bcf3 | ||
|
|
363bdb1493 | ||
|
|
1373196eb5 | ||
|
|
da7b7191c9 | ||
|
|
1660acbcbb | ||
|
|
9c3790e438 | ||
|
|
f5c7e0b1a1 | ||
|
|
3039f75179 | ||
|
|
637d49e1ab | ||
|
|
ea4be19a86 | ||
|
|
befd01cb03 | ||
|
|
fdd0ff6532 | ||
|
|
074546bdda | ||
|
|
6a6e474a1e | ||
|
|
3eb0f22ef9 | ||
|
|
d68fdf8d5d | ||
|
|
2dc00dc2cd | ||
|
|
02e9ac6683 | ||
|
|
700d13876a | ||
|
|
a6f2c9fb07 | ||
|
|
407240e2a2 | ||
|
|
852f8551af |
+11
-6
@@ -2,15 +2,20 @@ build:
|
|||||||
cache:
|
cache:
|
||||||
key: cache
|
key: cache
|
||||||
paths: [.cache]
|
paths: [.cache]
|
||||||
image: quay.io/buildah/stable
|
image: yourlabs/buildah
|
||||||
script:
|
script:
|
||||||
- dnf install -y curl python38
|
- pip3 install -U --user -e .[cli]
|
||||||
- curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
- CACHE_DIR=$(pwd)/.cache python3 ./shlaxfile.py build
|
||||||
- python3.8 get-pip.py
|
|
||||||
- pip3.8 install -U --user -e .[cli]
|
|
||||||
- CACHE_DIR=$(pwd)/.cache ~/.local/bin/shlax ./shlaxfile.py build
|
|
||||||
stage: build
|
stage: build
|
||||||
|
|
||||||
|
build-itself:
|
||||||
|
cache:
|
||||||
|
key: cache
|
||||||
|
paths: [.cache]
|
||||||
|
image: shlax:$CI_COMMIT_SHORT_SHA
|
||||||
|
script: python3 ./shlaxfile.py build
|
||||||
|
stage: test
|
||||||
|
|
||||||
test:
|
test:
|
||||||
image: yourlabs/python
|
image: yourlabs/python
|
||||||
stage: build
|
stage: build
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# Shlax: Pythonic automation tool
|
# Shlax: Pythonic automation tool
|
||||||
|
|
||||||
Shlax is a Python framework for system automation, initially with the purpose
|
Shlax is a Python framework for system automation, initially with the purpose
|
||||||
of replacing docker, docker-compose and ansible with a single tool, with the
|
of replacing docker, docker-compose and ansible with a single tool with the
|
||||||
purpose of code-reuse. It may be viewed as "async fabric rewrite by a
|
purpose of code-reuse made possible by target abstraction.
|
||||||
megalomanic Django fanboy".
|
|
||||||
|
|
||||||
The pattern resolves around two moving parts: Actions and Targets.
|
The pattern resolves around two moving parts: Actions and Targets.
|
||||||
|
|
||||||
@@ -187,12 +186,13 @@ class Docker(Target):
|
|||||||
return await self.parent.exec(*['docker', 'exec', self.name] + args)
|
return await self.parent.exec(*['docker', 'exec', self.name] + args)
|
||||||
```
|
```
|
||||||
|
|
||||||
Don't worry about `self.parent` being set, it is enforced to `Localhost` if
|
This also means that you always need a parent with an exec implementation,
|
||||||
unset so that we always have something that actually spawns a process in the
|
there are two:
|
||||||
chain ;)
|
|
||||||
|
|
||||||
The result of that design is that the following use cases are open for
|
- Localhost, executes on localhost
|
||||||
business:
|
- Stub, for testing
|
||||||
|
|
||||||
|
The result of that design is that the following use cases are available:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# This action installs my favorite package on any distro
|
# This action installs my favorite package on any distro
|
||||||
@@ -215,14 +215,44 @@ Ssh(host='yourhost')(build)
|
|||||||
|
|
||||||
# Or on a server behingh a bastion:
|
# Or on a server behingh a bastion:
|
||||||
# ssh yourbastion ssh yourhost build exec apt install python3
|
# ssh yourbastion ssh yourhost build exec apt install python3
|
||||||
Ssh(host='bastion')(Ssh(host='yourhost')(build))
|
Localhost()(Ssh(host='bastion')(Ssh(host='yourhost')(build))
|
||||||
|
|
||||||
# That's going to do the same
|
# That's going to do the same
|
||||||
Ssh(
|
Localhost(Ssh(
|
||||||
Ssh(
|
Ssh(
|
||||||
build,
|
build,
|
||||||
host='yourhost'
|
host='yourhost'
|
||||||
),
|
),
|
||||||
host='bastion'
|
host='bastion'
|
||||||
)()
|
))()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
You can execute Shlax actions directly on the command line with the `shlax` CLI
|
||||||
|
command.
|
||||||
|
|
||||||
|
For your own Shlaxfiles, you can build your CLI with your favorite CLI
|
||||||
|
framework. If you decide to use `cli2`, then Shlax provides a thin layer on top
|
||||||
|
of it: Group and Command objects made for Shlax objects.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
yourcontainer = Container(
|
||||||
|
build=Buildah(
|
||||||
|
User('app', '/app', 1000),
|
||||||
|
Packages('python', 'unzip', 'findutils'),
|
||||||
|
Copy('setup.py', 'yourdir', '/app'),
|
||||||
|
base='archlinux',
|
||||||
|
commit='yourimage',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print(Group(doc=__doc__).load(yourcontainer).entry_point())
|
||||||
|
```
|
||||||
|
|
||||||
|
The above will execute a cli2 command with each method of yourcontainer as a
|
||||||
|
sub-command.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ setup(
|
|||||||
setup_requires='setupmeta',
|
setup_requires='setupmeta',
|
||||||
extras_require=dict(
|
extras_require=dict(
|
||||||
cli=[
|
cli=[
|
||||||
'cli2',
|
'cli2>=2.2.2',
|
||||||
],
|
],
|
||||||
test=[
|
test=[
|
||||||
'pytest',
|
'pytest',
|
||||||
@@ -24,7 +24,7 @@ setup(
|
|||||||
python_requires='>=3',
|
python_requires='>=3',
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'shlax = shlax.cli:cli',
|
'shlax = shlax.cli:cli.entry_point',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import asyncio
|
||||||
|
import binascii
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class Copy:
|
||||||
|
def __init__(self, *args):
|
||||||
|
self.src = args[:-1]
|
||||||
|
self.dst = args[-1]
|
||||||
|
|
||||||
|
def listfiles(self):
|
||||||
|
if getattr(self, '_listfiles', None):
|
||||||
|
return self._listfiles
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for src in self.src:
|
||||||
|
if os.path.isfile(src):
|
||||||
|
result.append(src)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(src):
|
||||||
|
if '__pycache__' in root:
|
||||||
|
continue
|
||||||
|
result += [
|
||||||
|
os.path.join(root, f)
|
||||||
|
for f in files
|
||||||
|
if not f.endswith('.pyc')
|
||||||
|
]
|
||||||
|
self._listfiles = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def __call__(self, target):
|
||||||
|
await target.mkdir(self.dst)
|
||||||
|
|
||||||
|
for path in self.listfiles():
|
||||||
|
if os.path.isdir(path):
|
||||||
|
await target.mkdir(os.path.join(self.dst, path))
|
||||||
|
elif '/' in path:
|
||||||
|
dirname = os.path.join(
|
||||||
|
self.dst,
|
||||||
|
'/'.join(path.split('/')[:-1])
|
||||||
|
)
|
||||||
|
await target.mkdir(dirname)
|
||||||
|
await target.copy(path, dirname)
|
||||||
|
else:
|
||||||
|
await target.copy(path, self.dst)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Copy({", ".join(self.src)}, {self.dst})'
|
||||||
|
|
||||||
|
async def cachekey(self):
|
||||||
|
async def chksum(path):
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
return (path, str(binascii.crc32(f.read())))
|
||||||
|
results = await asyncio.gather(*[chksum(f) for f in self.listfiles()])
|
||||||
|
return {path: chks for path, chks in results}
|
||||||
@@ -52,8 +52,9 @@ class Packages:
|
|||||||
|
|
||||||
installed = []
|
installed = []
|
||||||
|
|
||||||
def __init__(self, *packages):
|
def __init__(self, *packages, upgrade=True):
|
||||||
self.packages = []
|
self.packages = []
|
||||||
|
self.upgrade = upgrade
|
||||||
for package in packages:
|
for package in packages:
|
||||||
line = dedent(package).strip().replace('\n', ' ')
|
line = dedent(package).strip().replace('\n', ' ')
|
||||||
self.packages += line.split(' ')
|
self.packages += line.split(' ')
|
||||||
@@ -116,7 +117,8 @@ class Packages:
|
|||||||
|
|
||||||
self.cmds = self.mgrs[self.mgr]
|
self.cmds = self.mgrs[self.mgr]
|
||||||
await self.update(target)
|
await self.update(target)
|
||||||
await target.rexec(self.cmds['upgrade'])
|
if self.upgrade:
|
||||||
|
await target.rexec(self.cmds['upgrade'])
|
||||||
|
|
||||||
packages = []
|
packages = []
|
||||||
for package in self.packages:
|
for package in self.packages:
|
||||||
@@ -159,5 +161,5 @@ class Packages:
|
|||||||
async def pacman_setup(self, target):
|
async def pacman_setup(self, target):
|
||||||
return self.cache_root + '/pacman'
|
return self.cache_root + '/pacman'
|
||||||
|
|
||||||
def __repr__(self):
|
def __str__(self):
|
||||||
return f'Packages({self.packages})'
|
return f'Packages({self.packages}, upgrade={self.upgrade})'
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
from glob import glob
|
||||||
|
import os
|
||||||
|
from urllib import request
|
||||||
|
|
||||||
|
from .base import Action
|
||||||
|
|
||||||
|
|
||||||
|
class Pip(Action):
|
||||||
|
"""Pip abstraction layer."""
|
||||||
|
def __init__(self, *pip_packages):
|
||||||
|
self.pip_packages = pip_packages
|
||||||
|
|
||||||
|
async def __call__(self, target):
|
||||||
|
# ensure python presence
|
||||||
|
results = await target.which('python3', 'python')
|
||||||
|
if results:
|
||||||
|
python = results[0]
|
||||||
|
else:
|
||||||
|
raise Exception('Could not find pip nor python')
|
||||||
|
|
||||||
|
# ensure pip module presence
|
||||||
|
result = await target.exec(
|
||||||
|
python, '-m', 'pip',
|
||||||
|
raises=False, quiet=True
|
||||||
|
)
|
||||||
|
if result.rc != 0:
|
||||||
|
if not os.path.exists('get-pip.py'):
|
||||||
|
req = request.urlopen(
|
||||||
|
'https://bootstrap.pypa.io/get-pip.py'
|
||||||
|
)
|
||||||
|
content = req.read()
|
||||||
|
with open('get-pip.py', 'wb+') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
await target.copy('get-pip.py', '.')
|
||||||
|
await target.exec(python, 'get-pip.py')
|
||||||
|
|
||||||
|
# choose a cache directory
|
||||||
|
if 'CACHE_DIR' in os.environ:
|
||||||
|
cache = os.path.join(os.getenv('CACHE_DIR'), 'pip')
|
||||||
|
else:
|
||||||
|
cache = os.path.join(os.getenv('HOME'), '.cache', 'pip')
|
||||||
|
|
||||||
|
# and mount it
|
||||||
|
if getattr(target, 'mount', None):
|
||||||
|
# we are in a target which shares a mount command
|
||||||
|
await target.mount(cache, '/root/.cache/pip')
|
||||||
|
|
||||||
|
source = []
|
||||||
|
nonsource = []
|
||||||
|
for package in self.pip_packages:
|
||||||
|
if os.path.exists(package):
|
||||||
|
source.append(package)
|
||||||
|
else:
|
||||||
|
nonsource.append(package)
|
||||||
|
|
||||||
|
if nonsource:
|
||||||
|
await target.exec(
|
||||||
|
python, '-m', 'pip',
|
||||||
|
'install', '--upgrade',
|
||||||
|
*nonsource
|
||||||
|
)
|
||||||
|
|
||||||
|
if source:
|
||||||
|
await target.exec(
|
||||||
|
python, '-m', 'pip',
|
||||||
|
'install', '--upgrade', '--editable',
|
||||||
|
*source
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Pip({", ".join(self.pip_packages)})'
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .packages import Packages
|
||||||
|
|
||||||
|
|
||||||
|
class User:
|
||||||
|
"""
|
||||||
|
Create a user.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
User('app', '/app', getenv('_CONTAINERS_ROOTLESS_UID', 1000)),
|
||||||
|
|
||||||
|
_CONTAINERS_ROOTLESS_UID allows to get your UID during build, which happens
|
||||||
|
in buildah unshare.
|
||||||
|
"""
|
||||||
|
def __init__(self, username, home, uid):
|
||||||
|
self.username = username
|
||||||
|
self.home = home
|
||||||
|
self.uid = uid
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'User({self.username}, {self.home}, {self.uid})'
|
||||||
|
|
||||||
|
async def __call__(self, target):
|
||||||
|
result = await target.rexec('id', self.uid)
|
||||||
|
if result.rc == 0:
|
||||||
|
old = re.match('.*\(([^)]*)\).*', result.out).group(1)
|
||||||
|
await target.rexec(
|
||||||
|
'usermod',
|
||||||
|
'-d', self.home,
|
||||||
|
'-l', self.username,
|
||||||
|
old
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await target.rexec(
|
||||||
|
'useradd',
|
||||||
|
'-d', self.home,
|
||||||
|
'-u', self.uid,
|
||||||
|
self.username
|
||||||
|
)
|
||||||
+73
-33
@@ -5,6 +5,7 @@ Shlax executes mostly in 3 ways:
|
|||||||
- With the name of a module in shlax.repo: a community maintained shlaxfile
|
- With the name of a module in shlax.repo: a community maintained shlaxfile
|
||||||
"""
|
"""
|
||||||
import ast
|
import ast
|
||||||
|
import asyncio
|
||||||
import cli2
|
import cli2
|
||||||
import glob
|
import glob
|
||||||
import inspect
|
import inspect
|
||||||
@@ -13,43 +14,82 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
class ConsoleScript(cli2.ConsoleScript):
|
class Group(cli2.Group):
|
||||||
def __call__(self):
|
def __init__(self, *args, **kwargs):
|
||||||
repo = os.path.join(os.path.dirname(__file__), 'repo')
|
super().__init__(*args, **kwargs)
|
||||||
|
self.cmdclass = Command
|
||||||
if len(self.argv) > 1:
|
|
||||||
repofile = os.path.join(repo, sys.argv[1] + '.py')
|
|
||||||
if os.path.isfile(self.argv[1]):
|
|
||||||
self.argv = sys.argv[1:]
|
|
||||||
self.load_shlaxfile(sys.argv[1])
|
|
||||||
elif os.path.isfile(repofile):
|
|
||||||
self.argv = sys.argv[1:]
|
|
||||||
self.load_shlaxfile(repofile)
|
|
||||||
else:
|
|
||||||
raise Exception('File not found ' + sys.argv[1])
|
|
||||||
else:
|
|
||||||
available = glob.glob(os.path.join(repo, '*.py'))
|
|
||||||
return super().__call__()
|
|
||||||
|
|
||||||
|
|
||||||
def load_shlaxfile(self, path):
|
class TargetArgument(cli2.Argument):
|
||||||
with open(path) as f:
|
"""DSN of the target to execute on, localhost by default, TBI"""
|
||||||
src = f.read()
|
|
||||||
tree = ast.parse(src)
|
|
||||||
|
|
||||||
members = []
|
def __init__(self, cmd, param, doc=None, color=None, default=None):
|
||||||
for node in tree.body:
|
from shlax.targets.base import Target
|
||||||
if not isinstance(node, ast.Assign):
|
super().__init__(cmd, param, doc=self.__doc__, default=Target())
|
||||||
|
self.alias = ['target', 't']
|
||||||
|
|
||||||
|
|
||||||
|
class Command(cli2.Command):
|
||||||
|
def setargs(self):
|
||||||
|
cli2.arg('target', cls=TargetArgument)(self.target)
|
||||||
|
super().setargs()
|
||||||
|
# that works though, so I went for that
|
||||||
|
# self['target'] = TargetArgument(
|
||||||
|
# self,
|
||||||
|
# self.sig.parameters['target'],
|
||||||
|
# )
|
||||||
|
if 'actions' in self:
|
||||||
|
del self['actions']
|
||||||
|
|
||||||
|
def __call__(self, *argv):
|
||||||
|
super().__call__(*argv)
|
||||||
|
self['target'].value.output.results(self['target'].value)
|
||||||
|
|
||||||
|
|
||||||
|
class ActionCommand(cli2.Command):
|
||||||
|
def call(self, *args, **kwargs):
|
||||||
|
self.target = self.target(*args, **kwargs)
|
||||||
|
from shlax.targets.base import Target
|
||||||
|
return super().call(Target())
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleScript(Group):
|
||||||
|
def __call__(self, *argv):
|
||||||
|
self.load_actions()
|
||||||
|
#self.load_shlaxfiles() # wip
|
||||||
|
return super().__call__(*argv)
|
||||||
|
|
||||||
|
def load_shlaxfiles(self):
|
||||||
|
filesdir = os.path.dirname(__file__) + '/shlaxfiles/'
|
||||||
|
for filename in os.listdir(filesdir):
|
||||||
|
filepath = filesdir + filename
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
continue
|
continue
|
||||||
if not isinstance(node.value, ast.Call):
|
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
tree = ast.parse(f.read())
|
||||||
|
group = self.group(filename[:-3])
|
||||||
|
|
||||||
|
main = Group(doc=__doc__).load(shlax)
|
||||||
|
|
||||||
|
def load_actions(self):
|
||||||
|
actionsdir = os.path.dirname(__file__) + '/actions/'
|
||||||
|
for filename in os.listdir(actionsdir):
|
||||||
|
filepath = actionsdir + filename
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
continue
|
continue
|
||||||
members.append(node.targets[0].id)
|
with open(filepath, 'r') as f:
|
||||||
|
tree = ast.parse(f.read())
|
||||||
|
cls = [
|
||||||
|
node
|
||||||
|
for node in tree.body
|
||||||
|
if isinstance(node, ast.ClassDef)
|
||||||
|
]
|
||||||
|
if not cls:
|
||||||
|
continue
|
||||||
|
mod = importlib.import_module('shlax.actions.' + filename[:-3])
|
||||||
|
cls = getattr(mod, cls[0].name)
|
||||||
|
self.add(cls, name=filename[:-3], cmdclass=ActionCommand)
|
||||||
|
|
||||||
spec = importlib.util.spec_from_file_location('shlaxfile', sys.argv[1])
|
|
||||||
mod = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(mod)
|
|
||||||
for member in members:
|
|
||||||
from shlax.targets.localhost import Localhost
|
|
||||||
self[member] = cli2.Callable(member, Localhost(getattr(mod, member)))
|
|
||||||
|
|
||||||
cli = ConsoleScript(__doc__)
|
cli = ConsoleScript(doc=__doc__)
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from .image import Image
|
||||||
|
|
||||||
|
|
||||||
|
class Container:
|
||||||
|
def __init__(self, build=None, image=None):
|
||||||
|
self.build = build
|
||||||
|
self.image = self.build.image
|
||||||
|
prefix = os.getcwd().split('/')[-1]
|
||||||
|
repo = self.image.repository.replace('/', '-')
|
||||||
|
if prefix == repo:
|
||||||
|
self.name = repo
|
||||||
|
else:
|
||||||
|
self.name = '-'.join([prefix, repo])
|
||||||
|
|
||||||
|
async def start(self, target):
|
||||||
|
"""Start the container"""
|
||||||
|
await target.rexec(
|
||||||
|
'podman',
|
||||||
|
'run',
|
||||||
|
'--name',
|
||||||
|
self.name,
|
||||||
|
str(self.image),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def stop(self, target):
|
||||||
|
"""Start the container"""
|
||||||
|
await target.rexec('podman', 'stop', self.name)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Container(name={self.name}, image={self.image})'
|
||||||
+6
-22
@@ -1,20 +1,9 @@
|
|||||||
|
import copy
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
class Image:
|
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(
|
PATTERN = re.compile(
|
||||||
'^((?P<backend>[a-z]*)://)?((?P<registry>[^/]*[.][^/]*)/)?((?P<repository>[^:]+))?(:(?P<tags>.*))?$' # noqa
|
'^((?P<backend>[a-z]*)://)?((?P<registry>[^/]*[.][^/]*)/)?((?P<repository>[^:]+))?(:(?P<tags>.*))?$' # noqa
|
||||||
, re.I
|
, re.I
|
||||||
@@ -44,12 +33,6 @@ class Image:
|
|||||||
if self.registry == 'docker.io':
|
if self.registry == 'docker.io':
|
||||||
self.format = 'docker'
|
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
|
# filter out tags which resolved to None
|
||||||
self.tags = [t for t in self.tags if t]
|
self.tags = [t for t in self.tags if t]
|
||||||
|
|
||||||
@@ -57,10 +40,6 @@ class Image:
|
|||||||
if not self.tags:
|
if not self.tags:
|
||||||
self.tags = ['latest']
|
self.tags = ['latest']
|
||||||
|
|
||||||
async def __call__(self, action, *args, **kwargs):
|
|
||||||
args = list(args)
|
|
||||||
return await action.exec(*args, **self.kwargs)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.repository}:{self.tags[-1]}'
|
return f'{self.repository}:{self.tags[-1]}'
|
||||||
|
|
||||||
@@ -74,3 +53,8 @@ class Image:
|
|||||||
|
|
||||||
for tag in self.tags:
|
for tag in self.tags:
|
||||||
await action.exec('buildah', 'push', f'{self.repository}:{tag}')
|
await action.exec('buildah', 'push', f'{self.repository}:{tag}')
|
||||||
|
|
||||||
|
def layer(self, key):
|
||||||
|
layer = copy.deepcopy(self)
|
||||||
|
layer.tags = ['layer-' + key]
|
||||||
|
return layer
|
||||||
|
|||||||
+39
-18
@@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
from .colors import colors
|
from .colors import colors
|
||||||
|
|
||||||
@@ -25,15 +26,16 @@ class Output:
|
|||||||
def colorize(self, code, content):
|
def colorize(self, code, content):
|
||||||
return self.color(code) + content + self.color()
|
return self.color(code) + content + self.color()
|
||||||
|
|
||||||
def colorized(self):
|
def colorized(self, action):
|
||||||
if hasattr(self.subject, 'colorized'):
|
if hasattr(action, 'colorized'):
|
||||||
return self.subject.colorized(self.colors)
|
return action.colorized(self.colors)
|
||||||
|
elif isinstance(action, types.MethodType):
|
||||||
|
return f'{action.__self__}.{action.__name__}'
|
||||||
else:
|
else:
|
||||||
return str(self.subject)
|
return str(action)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
subject=None,
|
|
||||||
prefix=None,
|
prefix=None,
|
||||||
regexps=None,
|
regexps=None,
|
||||||
debug='cmd,visit,out',
|
debug='cmd,visit,out',
|
||||||
@@ -41,7 +43,6 @@ class Output:
|
|||||||
flush=None,
|
flush=None,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
self.subject = subject
|
|
||||||
self.prefix = prefix
|
self.prefix = prefix
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.prefix_length = 0
|
self.prefix_length = 0
|
||||||
@@ -114,59 +115,79 @@ class Output:
|
|||||||
|
|
||||||
return line
|
return line
|
||||||
|
|
||||||
def test(self):
|
def test(self, action):
|
||||||
self(''.join([
|
self(''.join([
|
||||||
self.colors['purplebold'],
|
self.colors['purplebold'],
|
||||||
'! TEST ',
|
'! TEST ',
|
||||||
self.colors['reset'],
|
self.colors['reset'],
|
||||||
self.colorized(),
|
self.colorized(action),
|
||||||
'\n',
|
'\n',
|
||||||
]))
|
]))
|
||||||
|
|
||||||
def clean(self):
|
def clean(self, action):
|
||||||
if self.debug:
|
if self.debug:
|
||||||
self(''.join([
|
self(''.join([
|
||||||
self.colors['bluebold'],
|
self.colors['bluebold'],
|
||||||
'+ CLEAN ',
|
'+ CLEAN ',
|
||||||
self.colors['reset'],
|
self.colors['reset'],
|
||||||
self.colorized(),
|
self.colorized(action),
|
||||||
'\n',
|
'\n',
|
||||||
]))
|
]))
|
||||||
|
|
||||||
def start(self):
|
def start(self, action):
|
||||||
if self.debug is True or 'visit' in str(self.debug):
|
if self.debug is True or 'visit' in str(self.debug):
|
||||||
self(''.join([
|
self(''.join([
|
||||||
self.colors['orangebold'],
|
self.colors['orangebold'],
|
||||||
'⚠ START ',
|
'⚠ START ',
|
||||||
self.colors['reset'],
|
self.colors['reset'],
|
||||||
self.colorized(),
|
self.colorized(action),
|
||||||
'\n',
|
'\n',
|
||||||
]))
|
]))
|
||||||
|
|
||||||
def success(self):
|
def info(self, text):
|
||||||
|
if self.debug is True or 'visit' in str(self.debug):
|
||||||
|
self(''.join([
|
||||||
|
self.colors['cyanbold'],
|
||||||
|
'➤ INFO ',
|
||||||
|
self.colors['reset'],
|
||||||
|
text,
|
||||||
|
'\n',
|
||||||
|
]))
|
||||||
|
|
||||||
|
def skip(self, action):
|
||||||
|
if self.debug is True or 'visit' in str(self.debug):
|
||||||
|
self(''.join([
|
||||||
|
self.colors['yellowbold'],
|
||||||
|
'↪️ SKIP ',
|
||||||
|
self.colors['reset'],
|
||||||
|
self.colorized(action),
|
||||||
|
'\n',
|
||||||
|
]))
|
||||||
|
|
||||||
|
def success(self, action):
|
||||||
if self.debug is True or 'visit' in str(self.debug):
|
if self.debug is True or 'visit' in str(self.debug):
|
||||||
self(''.join([
|
self(''.join([
|
||||||
self.colors['greenbold'],
|
self.colors['greenbold'],
|
||||||
'✔ SUCCESS ',
|
'✔ SUCCESS ',
|
||||||
self.colors['reset'],
|
self.colors['reset'],
|
||||||
self.colorized(),
|
self.colorized(action),
|
||||||
'\n',
|
'\n',
|
||||||
]))
|
]))
|
||||||
|
|
||||||
def fail(self, exception=None):
|
def fail(self, action, exception=None):
|
||||||
if self.debug is True or 'visit' in str(self.debug):
|
if self.debug is True or 'visit' in str(self.debug):
|
||||||
self(''.join([
|
self(''.join([
|
||||||
self.colors['redbold'],
|
self.colors['redbold'],
|
||||||
'✘ FAIL ',
|
'✘ FAIL ',
|
||||||
self.colors['reset'],
|
self.colors['reset'],
|
||||||
self.colorized(),
|
self.colorized(action),
|
||||||
'\n',
|
'\n',
|
||||||
]))
|
]))
|
||||||
|
|
||||||
def results(self):
|
def results(self, action):
|
||||||
success = 0
|
success = 0
|
||||||
fail = 0
|
fail = 0
|
||||||
for result in self.subject.results:
|
for result in action.results:
|
||||||
if result.status == 'success':
|
if result.status == 'success':
|
||||||
success += 1
|
success += 1
|
||||||
if result.status == 'failure':
|
if result.status == 'failure':
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import cli2
|
||||||
|
|
||||||
|
from shlax.targets.base import Target
|
||||||
|
from shlax.actions.parallel import Parallel
|
||||||
|
|
||||||
|
|
||||||
|
class Pod:
|
||||||
|
"""Help text"""
|
||||||
|
def __init__(self, **containers):
|
||||||
|
self.containers = containers
|
||||||
|
|
||||||
|
async def _call(self, target, method, *names):
|
||||||
|
methods = [
|
||||||
|
getattr(container, method)
|
||||||
|
for name, container in self.containers.items()
|
||||||
|
if not names or name in names
|
||||||
|
]
|
||||||
|
await target(Parallel(*methods))
|
||||||
|
|
||||||
|
async def build(self, target, *names):
|
||||||
|
"""Build container images"""
|
||||||
|
await self._call(target, 'build', *names)
|
||||||
|
|
||||||
|
async def start(self, target, *names):
|
||||||
|
"""Start container images"""
|
||||||
|
await self._call(target, 'start', *names)
|
||||||
+12
-9
@@ -32,21 +32,21 @@ class PrefixStreamProtocol(asyncio.subprocess.SubprocessStreamProtocol):
|
|||||||
make asynchronous output readable.
|
make asynchronous output readable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, output, *args, **kwargs):
|
def __init__(self, proc, *args, **kwargs):
|
||||||
self.output = output
|
self.proc = proc
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def pipe_data_received(self, fd, data):
|
def pipe_data_received(self, fd, data):
|
||||||
if self.output.debug is True or 'out' in str(self.output.debug):
|
if self.proc.output.debug is True or 'out' in str(self.proc.output.debug):
|
||||||
if fd in (1, 2):
|
if fd in (1, 2):
|
||||||
self.output(data)
|
self.proc.output(data)
|
||||||
super().pipe_data_received(fd, data)
|
super().pipe_data_received(fd, data)
|
||||||
|
|
||||||
|
|
||||||
def protocol_factory(output):
|
def protocol_factory(proc):
|
||||||
def _p():
|
def _p():
|
||||||
return PrefixStreamProtocol(
|
return PrefixStreamProtocol(
|
||||||
output,
|
proc,
|
||||||
limit=asyncio.streams._DEFAULT_LIMIT,
|
limit=asyncio.streams._DEFAULT_LIMIT,
|
||||||
loop=asyncio.events.get_event_loop()
|
loop=asyncio.events.get_event_loop()
|
||||||
)
|
)
|
||||||
@@ -69,8 +69,11 @@ class Proc:
|
|||||||
"""
|
"""
|
||||||
test = False
|
test = False
|
||||||
|
|
||||||
def __init__(self, *args, prefix=None, raises=True, output=None):
|
def __init__(self, *args, prefix=None, raises=True, output=None, quiet=False):
|
||||||
self.output = output or Output()
|
if quiet:
|
||||||
|
self.output = Output(debug=False)
|
||||||
|
else:
|
||||||
|
self.output = output or Output()
|
||||||
self.cmd = ' '.join(args)
|
self.cmd = ' '.join(args)
|
||||||
self.args = args
|
self.args = args
|
||||||
self.prefix = prefix
|
self.prefix = prefix
|
||||||
@@ -112,7 +115,7 @@ class Proc:
|
|||||||
|
|
||||||
loop = asyncio.events.get_event_loop()
|
loop = asyncio.events.get_event_loop()
|
||||||
transport, protocol = await loop.subprocess_exec(
|
transport, protocol = await loop.subprocess_exec(
|
||||||
protocol_factory(self.output), *self.args)
|
protocol_factory(self), *self.args)
|
||||||
self.proc = asyncio.subprocess.Process(transport, protocol, loop)
|
self.proc = asyncio.subprocess.Process(transport, protocol, loop)
|
||||||
self.called = True
|
self.called = True
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,16 @@ from .targets.buildah import Buildah
|
|||||||
from .targets.localhost import Localhost
|
from .targets.localhost import Localhost
|
||||||
from .targets.stub import Stub
|
from .targets.stub import Stub
|
||||||
|
|
||||||
|
from .actions.copy import Copy
|
||||||
from .actions.packages import Packages
|
from .actions.packages import Packages
|
||||||
from .actions.run import Run
|
from .actions.run import Run
|
||||||
|
from .actions.pip import Pip
|
||||||
|
from .actions.parallel import Parallel
|
||||||
|
from .actions.user import User
|
||||||
|
|
||||||
|
from .cli import Command, Group
|
||||||
|
|
||||||
|
from .container import Container
|
||||||
|
from .pod import Pod
|
||||||
|
|
||||||
|
from os import getenv, environ
|
||||||
|
|||||||
+100
-31
@@ -1,5 +1,9 @@
|
|||||||
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
from ..output import Output
|
from ..output import Output
|
||||||
from ..proc import Proc
|
from ..proc import Proc
|
||||||
@@ -7,21 +11,32 @@ from ..result import Result, Results
|
|||||||
|
|
||||||
|
|
||||||
class Target:
|
class Target:
|
||||||
def __init__(self, *actions, **options):
|
def __init__(self, *actions, root=None):
|
||||||
self.actions = actions
|
self.actions = actions
|
||||||
self.options = options
|
|
||||||
self.results = []
|
self.results = []
|
||||||
self.output = Output(self, **self.options)
|
self.output = Output()
|
||||||
self.parent = None
|
self.parent = None
|
||||||
|
self.root = root or os.getcwd()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'localhost'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
return self._parent or Target()
|
||||||
|
|
||||||
|
@parent.setter
|
||||||
|
def parent(self, value):
|
||||||
|
self._parent = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def caller(self):
|
def caller(self):
|
||||||
"""Traverse parents and return the top-levels Target."""
|
"""Traverse parents and return the top-levels Target."""
|
||||||
if not self.parent:
|
if not self._parent:
|
||||||
return self
|
return self
|
||||||
caller = self.parent
|
caller = self._parent
|
||||||
while caller.parent:
|
while caller._parent:
|
||||||
caller = caller.parent
|
caller = caller._parent
|
||||||
return caller
|
return caller
|
||||||
|
|
||||||
async def __call__(self, *actions, target=None):
|
async def __call__(self, *actions, target=None):
|
||||||
@@ -31,32 +46,35 @@ class Target:
|
|||||||
self.parent = target
|
self.parent = target
|
||||||
|
|
||||||
for action in actions or self.actions:
|
for action in actions or self.actions:
|
||||||
result = Result(self, action)
|
if await self.action(action, reraise=bool(actions)):
|
||||||
|
break
|
||||||
|
|
||||||
self.output = Output(action, **self.options)
|
async def action(self, action, reraise=False):
|
||||||
self.output.start()
|
result = Result(self, action)
|
||||||
try:
|
self.output.start(action)
|
||||||
await action(target=self)
|
try:
|
||||||
except Exception as e:
|
await action(target=self)
|
||||||
self.output.fail(e)
|
except Exception as e:
|
||||||
result.status = 'failure'
|
self.output.fail(action, e)
|
||||||
result.exception = e
|
result.status = 'failure'
|
||||||
if actions:
|
result.exception = e
|
||||||
# nested call, re-raise
|
if reraise:
|
||||||
raise
|
# nested call, re-raise
|
||||||
else:
|
raise
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
self.output.success()
|
import traceback
|
||||||
result.status = 'success'
|
traceback.print_exception(type(e), e, sys.exc_info()[2])
|
||||||
finally:
|
return True
|
||||||
self.caller.results.append(result)
|
else:
|
||||||
|
self.output.success(action)
|
||||||
|
result.status = 'success'
|
||||||
|
finally:
|
||||||
|
self.caller.results.append(result)
|
||||||
|
|
||||||
clean = getattr(action, 'clean', None)
|
clean = getattr(action, 'clean', None)
|
||||||
if clean:
|
if clean:
|
||||||
action.result = result
|
self.output.clean(action)
|
||||||
self.output.clean()
|
await clean(self, result)
|
||||||
await clean(self)
|
|
||||||
|
|
||||||
async def rexec(self, *args, **kwargs):
|
async def rexec(self, *args, **kwargs):
|
||||||
kwargs['user'] = 'root'
|
kwargs['user'] = 'root'
|
||||||
@@ -76,5 +94,56 @@ class Target:
|
|||||||
result.append(match.group(1))
|
result.append(match.group(1))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def shargs(self, *args, **kwargs):
|
||||||
|
user = kwargs.pop('user', None)
|
||||||
|
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
|
||||||
|
|
||||||
|
return args, kwargs
|
||||||
|
|
||||||
|
if self.parent:
|
||||||
|
return self.parent.shargs(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return args, kwargs
|
||||||
|
|
||||||
async def exec(self, *args, **kwargs):
|
async def exec(self, *args, **kwargs):
|
||||||
raise NotImplemented()
|
kwargs['output'] = self.output
|
||||||
|
args, kwargs = self.shargs(*args, **kwargs)
|
||||||
|
proc = await Proc(*args, **kwargs)()
|
||||||
|
if kwargs.get('wait', True):
|
||||||
|
await proc.wait()
|
||||||
|
return proc
|
||||||
|
|
||||||
|
@property
|
||||||
|
def root(self):
|
||||||
|
return self._root
|
||||||
|
|
||||||
|
@root.setter
|
||||||
|
def root(self, value):
|
||||||
|
self._root = Path(value or os.getcwd())
|
||||||
|
|
||||||
|
def path(self, path):
|
||||||
|
if str(path).startswith('/'):
|
||||||
|
path = str(path)[1:]
|
||||||
|
return self.root / path
|
||||||
|
|
||||||
|
async def mkdir(self, path):
|
||||||
|
if '_mkdir' not in self.__dict__:
|
||||||
|
self._mkdir = []
|
||||||
|
path = str(path)
|
||||||
|
if path not in self._mkdir:
|
||||||
|
await self.exec('mkdir', '-p', path)
|
||||||
|
self._mkdir.append(path)
|
||||||
|
|
||||||
|
async def copy(self, *args):
|
||||||
|
return await self.exec('cp', '-a', *args)
|
||||||
|
|||||||
+140
-35
@@ -1,3 +1,7 @@
|
|||||||
|
import asyncio
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -9,16 +13,17 @@ from ..proc import Proc
|
|||||||
|
|
||||||
|
|
||||||
class Buildah(Target):
|
class Buildah(Target):
|
||||||
|
"""Build container image with buildah"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
*actions,
|
*actions,
|
||||||
base=None, commit=None,
|
base=None, commit=None,
|
||||||
cmd=None,
|
cmd=None):
|
||||||
**options):
|
|
||||||
self.base = base or 'alpine'
|
self.base = base or 'alpine'
|
||||||
self.image = Image(commit) if commit else None
|
self.image = Image(commit) if commit else None
|
||||||
|
|
||||||
self.ctr = None
|
self.ctr = None
|
||||||
self.mnt = None
|
self.root = None
|
||||||
self.mounts = dict()
|
self.mounts = dict()
|
||||||
|
|
||||||
self.config = dict(
|
self.config = dict(
|
||||||
@@ -28,7 +33,7 @@ class Buildah(Target):
|
|||||||
# Always consider localhost as parent for now
|
# Always consider localhost as parent for now
|
||||||
self.parent = Target()
|
self.parent = Target()
|
||||||
|
|
||||||
super().__init__(*actions, **options)
|
super().__init__(*actions)
|
||||||
|
|
||||||
def is_runnable(self):
|
def is_runnable(self):
|
||||||
return Proc.test or os.getuid() == 0
|
return Proc.test or os.getuid() == 0
|
||||||
@@ -36,36 +41,118 @@ class Buildah(Target):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
if not self.is_runnable():
|
if not self.is_runnable():
|
||||||
return 'Replacing with: buildah unshare ' + ' '.join(sys.argv)
|
return 'Replacing with: buildah unshare ' + ' '.join(sys.argv)
|
||||||
return 'Buildah image builder'
|
return f'Buildah({self.image})'
|
||||||
|
|
||||||
async def __call__(self, *actions, target=None):
|
async def __call__(self, *actions, target=None):
|
||||||
self.parent = target
|
if target:
|
||||||
|
self.parent = target
|
||||||
|
|
||||||
if not self.is_runnable():
|
if not self.is_runnable():
|
||||||
os.execvp('buildah', ['buildah', 'unshare'] + sys.argv)
|
os.execvp('buildah', ['buildah', 'unshare'] + sys.argv)
|
||||||
# program has been replaced
|
# program has been replaced
|
||||||
|
|
||||||
|
layers = await self.layers()
|
||||||
|
keep = await self.cache_setup(layers, *actions)
|
||||||
|
keepnames = [*map(lambda x: 'localhost/' + str(x), keep)]
|
||||||
|
self.invalidate = [name for name in layers if name not in keepnames]
|
||||||
|
if self.invalidate:
|
||||||
|
self.output.info('Invalidating old layers')
|
||||||
|
await self.parent.exec(
|
||||||
|
'buildah', 'rmi', *self.invalidate, raises=False)
|
||||||
|
|
||||||
|
if actions:
|
||||||
|
actions = actions[len(keep):]
|
||||||
|
if not actions:
|
||||||
|
return self.output.success('Image up to date')
|
||||||
|
else:
|
||||||
|
self.actions = self.actions[len(keep):]
|
||||||
|
if not self.actions:
|
||||||
|
return self.output.success('Image up to date')
|
||||||
|
|
||||||
self.ctr = (await self.parent.exec('buildah', 'from', self.base)).out
|
self.ctr = (await self.parent.exec('buildah', 'from', self.base)).out
|
||||||
self.mnt = Path((await self.parent.exec('buildah', 'mount', self.ctr)).out)
|
self.root = Path((await self.parent.exec('buildah', 'mount', self.ctr)).out)
|
||||||
await super().__call__()
|
|
||||||
|
|
||||||
async def clean(self, target):
|
return await super().__call__(*actions)
|
||||||
for src, dst in self.mounts.items():
|
|
||||||
await self.parent.exec('umount', self.mnt / str(dst)[1:])
|
|
||||||
|
|
||||||
if self.result.status == 'success':
|
async def layers(self):
|
||||||
|
ret = set()
|
||||||
|
results = await self.parent.exec(
|
||||||
|
'buildah images --json',
|
||||||
|
quiet=True,
|
||||||
|
)
|
||||||
|
results = json.loads(results.out)
|
||||||
|
|
||||||
|
prefix = 'localhost/' + self.image.repository + ':layer-'
|
||||||
|
for result in results:
|
||||||
|
if not result.get('names', None):
|
||||||
|
continue
|
||||||
|
for name in result['names']:
|
||||||
|
if name.startswith(prefix):
|
||||||
|
ret.add(name)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def cache_setup(self, layers, *actions):
|
||||||
|
keep = []
|
||||||
|
self.image_previous = Image(self.base)
|
||||||
|
for action in actions or self.actions:
|
||||||
|
action_image = await self.action_image(action)
|
||||||
|
name = 'localhost/' + str(action_image)
|
||||||
|
if name in layers:
|
||||||
|
self.base = self.image_previous = action_image
|
||||||
|
keep.append(action_image)
|
||||||
|
self.output.skip(
|
||||||
|
f'Found layer for {action}: {action_image.tags[0]}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return keep
|
||||||
|
|
||||||
|
async def action_image(self, action):
|
||||||
|
prefix = str(self.image_previous)
|
||||||
|
for tag in self.image_previous.tags:
|
||||||
|
if tag.startswith('layer-'):
|
||||||
|
prefix = tag
|
||||||
|
break
|
||||||
|
if hasattr(action, 'cachekey'):
|
||||||
|
action_key = action.cachekey()
|
||||||
|
if asyncio.iscoroutine(action_key):
|
||||||
|
action_key = str(await action_key)
|
||||||
|
else:
|
||||||
|
action_key = str(action)
|
||||||
|
key = prefix + action_key
|
||||||
|
sha1 = hashlib.sha1(key.encode('ascii'))
|
||||||
|
return self.image.layer(sha1.hexdigest())
|
||||||
|
|
||||||
|
async def action(self, action, reraise=False):
|
||||||
|
stop = await super().action(action, reraise)
|
||||||
|
if not stop:
|
||||||
|
action_image = await self.action_image(action)
|
||||||
|
self.output.info(f'Commiting {action_image} for {action}')
|
||||||
|
await self.parent.exec(
|
||||||
|
'buildah',
|
||||||
|
'commit',
|
||||||
|
'--format=' + action_image.format,
|
||||||
|
self.ctr,
|
||||||
|
action_image,
|
||||||
|
)
|
||||||
|
self.image_previous = action_image
|
||||||
|
return stop
|
||||||
|
|
||||||
|
async def clean(self, target, result):
|
||||||
|
if self.ctr is not None:
|
||||||
|
for src, dst in self.mounts.items():
|
||||||
|
await self.parent.exec('umount', self.root / str(dst)[1:])
|
||||||
|
await self.parent.exec('buildah', 'umount', self.ctr)
|
||||||
|
await self.parent.exec('buildah', 'rm', self.ctr)
|
||||||
|
|
||||||
|
if result.status == 'success':
|
||||||
await self.commit()
|
await self.commit()
|
||||||
if os.getenv('BUILDAH_PUSH'):
|
if os.getenv('BUILDAH_PUSH'):
|
||||||
await self.image.push(target)
|
await self.image.push(target)
|
||||||
|
|
||||||
if self.mnt is not None:
|
|
||||||
await self.parent.exec('buildah', 'umount', self.ctr)
|
|
||||||
|
|
||||||
if self.ctr is not None:
|
|
||||||
await self.parent.exec('buildah', 'rm', self.ctr)
|
|
||||||
|
|
||||||
async def mount(self, src, dst):
|
async def mount(self, src, dst):
|
||||||
"""Mount a host directory into the container."""
|
"""Mount a host directory into the container."""
|
||||||
target = self.mnt / str(dst)[1:]
|
target = self.root / str(dst)[1:]
|
||||||
await self.parent.exec(f'mkdir -p {src} {target}')
|
await self.parent.exec(f'mkdir -p {src} {target}')
|
||||||
await self.parent.exec(f'mount -o bind {src} {target}')
|
await self.parent.exec(f'mount -o bind {src} {target}')
|
||||||
self.mounts[src] = dst
|
self.mounts[src] = dst
|
||||||
@@ -78,25 +165,43 @@ class Buildah(Target):
|
|||||||
_args += [' '.join([str(a) for a in args])]
|
_args += [' '.join([str(a) for a in args])]
|
||||||
return await self.parent.exec(*_args, **kwargs)
|
return await self.parent.exec(*_args, **kwargs)
|
||||||
|
|
||||||
async def commit(self):
|
async def commit(self, image=None):
|
||||||
if not self.image:
|
image = image or self.image
|
||||||
|
if not image:
|
||||||
return
|
return
|
||||||
|
|
||||||
for key, value in self.config.items():
|
if not image:
|
||||||
await self.parent.exec(f'buildah config --{key} "{value}" {self.ctr}')
|
# don't go through that if layer commit
|
||||||
|
for key, value in self.config.items():
|
||||||
|
await self.parent.exec(f'buildah config --{key} "{value}" {self.ctr}')
|
||||||
|
|
||||||
self.sha = (await self.exec(
|
ENV_TAGS = (
|
||||||
'buildah',
|
# gitlab
|
||||||
'commit',
|
'CI_COMMIT_SHORT_SHA',
|
||||||
'--format=' + self.image.format,
|
'CI_COMMIT_REF_NAME',
|
||||||
self.ctr,
|
'CI_COMMIT_TAG',
|
||||||
buildah=False,
|
# CircleCI
|
||||||
)).out
|
'CIRCLE_SHA1',
|
||||||
|
'CIRCLE_TAG',
|
||||||
|
'CIRCLE_BRANCH',
|
||||||
|
# contributions welcome here
|
||||||
|
)
|
||||||
|
|
||||||
if self.image.tags:
|
# figure tags from CI vars
|
||||||
tags = [f'{self.image.repository}:{tag}' for tag in self.image.tags]
|
for name in ENV_TAGS:
|
||||||
|
value = os.getenv(name)
|
||||||
|
if value:
|
||||||
|
self.image.tags.append(value)
|
||||||
|
|
||||||
|
if image.tags:
|
||||||
|
tags = [f'{image.repository}:{tag}' for tag in image.tags]
|
||||||
else:
|
else:
|
||||||
tags = [self.image.repository]
|
tags = [image.repository]
|
||||||
|
|
||||||
for tag in tags:
|
await self.parent.exec('buildah', 'tag', self.image_previous, *tags)
|
||||||
await self.parent.exec('buildah', 'tag', self.sha, tag)
|
|
||||||
|
async def mkdir(self, path):
|
||||||
|
return await self.parent.mkdir(self.path(path))
|
||||||
|
|
||||||
|
async def copy(self, *args):
|
||||||
|
return await self.parent.copy(*args[:-1], self.path(args[-1]))
|
||||||
|
|||||||
+13
-5
@@ -1,12 +1,20 @@
|
|||||||
#!/usr/bin/env shlax
|
#!/usr/bin/env python
|
||||||
"""
|
"""
|
||||||
Shlaxfile for shlax itself.
|
Shlaxfile for shlax itself.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from shlax.shortcuts import *
|
from shlax.shortcuts import *
|
||||||
|
|
||||||
build = Buildah(
|
shlax = Container(
|
||||||
Run('echo hi'),
|
build=Buildah(
|
||||||
Packages('python38'),
|
Packages('python38', 'buildah', 'unzip', 'findutils', upgrade=False),
|
||||||
base='quay.io/podman/stable',
|
Copy('setup.py', 'shlax', '/app'),
|
||||||
|
Pip('/app'),
|
||||||
|
base='quay.io/podman/stable',
|
||||||
|
commit='shlax',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print(Group(doc=__doc__).load(shlax).entry_point())
|
||||||
|
|||||||
@@ -25,9 +25,3 @@ def test_args(arg, expected):
|
|||||||
im = Image(arg)
|
im = Image(arg)
|
||||||
for k, v in expected.items():
|
for k, v in expected.items():
|
||||||
assert getattr(im, k) == v
|
assert getattr(im, k) == v
|
||||||
|
|
||||||
def test_args_env():
|
|
||||||
os.environ['IMAGE_TEST_ARGS_ENV'] = 'foo'
|
|
||||||
Image.ENV_TAGS = ['IMAGE_TEST_ARGS_ENV']
|
|
||||||
im = Image('re/po:x,y')
|
|
||||||
assert im.tags == ['x', 'y', 'foo']
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ async def test_parallel():
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_function():
|
async def test_function():
|
||||||
async def hello(target):
|
async def hello(target):
|
||||||
target.exec('hello')
|
await target.exec('hello')
|
||||||
await Stub()(hello)
|
await Stub()(hello)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user