149 Commits
Author SHA1 Message Date
jpic 2db971234a Trying to get somewhere with traefik module 2020-02-17 01:00:59 +01:00
jpic 41ec8db301 Magic command line 2020-02-16 21:34:41 +01:00
jpic 33c37f8e44 wip 2020-02-16 20:54:12 +01:00
jpic f5ab14d383 Trying to enrich shlax command in a scalable fashion 2020-02-16 20:53:49 +01:00
jpic 97255866f8 Remove old code 2020-02-16 20:14:20 +01:00
jpic ebcde95d5a Simplify gitlabci for now 2020-02-16 20:02:24 +01:00
jpic ee25255028 Prevent double clean 2020-02-16 19:57:44 +01:00
jpic ba87dea428 Container strategies swallows args 2020-02-16 19:41:20 +01:00
jpic 2e3b9d8e5b Avoid double push 2020-02-16 19:37:18 +01:00
jpic f4844b0e73 Fix login 2020-02-16 19:28:38 +01:00
jpic 3ea029efec Fix buildah login 2020-02-16 19:09:27 +01:00
jpic 92b62799f4 Fix pre-commit hook 2020-02-16 18:59:39 +01:00
jpic a374ac58b3 Gitlab update 2020-02-16 18:45:34 +01:00
jpic fe70c69b0c Update shlax in shlax 2020-02-16 18:39:50 +01:00
jpic 3152f83971 Cleanup success code 2020-02-16 18:34:15 +01:00
jpic a5f6c4fe74 Better to define script 2020-02-16 18:23:22 +01:00
jpic 3f4738f432 Gitlab cache 2020-02-16 18:21:04 +01:00
jpic 82f98b41b4 Enforce docker format 2020-02-16 18:18:07 +01:00
jpic 8be4a5c84f Debug output for shlax build 2020-02-16 18:15:05 +01:00
jpic 42595bfd10 Still sorting out test part 2020-02-16 18:12:18 +01:00
jpic b771e9bd1a Added Container strategy 2020-02-16 17:45:04 +01:00
jpic a720e22a50 Made ./shlaxfile.py build test push to do the right thing 2020-02-16 16:20:56 +01:00
jpic 8b145e6378 Add automatic build test 2020-02-16 16:12:12 +01:00
jpic 5b11100e0a Rename is_wrapper with is_runnable, rely on uid check 2020-02-16 15:35:02 +01:00
jpic 933c81b7a8 Break script on error 2020-02-16 15:34:44 +01:00
jpic a0cba02c25 Await asyncio jobs 2020-02-16 15:16:04 +01:00
jpic 440761fea2 Python3.8 on centos dance 2020-02-16 15:15:54 +01:00
jpic e20946a0f6 Optional upgrade, build async 2020-02-16 15:03:27 +01:00
jpic e0c902eb47 Buildah: support push arg 2020-02-16 14:47:03 +01:00
jpic 9736c1bb2b Add python3 yaml package 2020-02-16 14:42:48 +01:00
jpic 2ea37fb01d GitLabCI can now execute the same jobs it generates for gitlab 2020-02-16 14:36:03 +01:00
jpic 6924d39590 Add Docker target, recursive calls 2020-02-16 13:57:26 +01:00
jpic baf295f145 Fix prefix and bugfix CLI args landing in Output constructor 2020-02-15 22:39:14 +01:00
jpic f2bad27eb7 OMG such pretty output, such async copy by default 2020-02-15 21:31:12 +01:00
jpic a3a81eaca5 Clear out buildah wrapper public API and debug levels 2020-02-15 21:23:17 +01:00
jpic 2513c65433 Remove untested regexps 2020-02-15 21:21:36 +01:00
jpic 4fbae78057 Dont bother building buildah on buildah 2020-02-15 21:21:28 +01:00
jpic 5c38dc13b0 Add debug option back 2020-02-15 19:47:24 +01:00
jpic c84353c126 Clean GitLabCIConfig output 2020-02-15 19:41:27 +01:00
jpic 3c4f5f6d7e Remove most of gitlabci job 2020-02-15 19:38:57 +01:00
jpic 93de46b061 Add bold, perfect symbols 2020-02-15 19:09:24 +01:00
jpic 67aca9de44 No need for background in cmd output 2020-02-15 19:01:21 +01:00
jpic bd64a0fa3a Add colors script 2020-02-15 17:16:54 +01:00
jpic 884f96013e Buildah clean 2020-02-15 17:04:00 +01:00
jpic 7806e217ef Fix buildah clean 2020-02-15 17:01:52 +01:00
jpic c96d2494d2 Thread safety, cleaning 2020-02-15 16:59:36 +01:00
jpic 3afc739a1e Fix basic action outputs 2020-02-15 16:09:44 +01:00
jpic 638f0fa690 Fix cli that would play all scripts 2020-02-15 13:51:32 +01:00
jpic 1ceee0dee6 Remove leftover from repo import 2020-02-15 01:34:06 +01:00
jpic df88a1bc0e fixitall 2020-02-15 01:26:50 +01:00
jpic 984f09d704 Refactor output and call
Each action has its own output from now on.
2020-02-15 01:04:52 +01:00
jpic b703ed379e pullingmyleg 2020-02-14 22:15:32 +01:00
jpic 9f69b3474d srsly 2020-02-14 22:15:06 +01:00
jpic 3d1c2502bd Expose gitlabci commit hook 2020-02-14 22:11:47 +01:00
jpic d8ca3b7e2f Add test script 2020-02-14 22:08:48 +01:00
jpic ae03634682 Fix ci 2020-02-14 21:04:19 +01:00
jpic 9d7af0a04d Fix ci 2020-02-14 21:04:03 +01:00
jpic 6c1fc2685c Update ci 2020-02-14 20:53:39 +01:00
jpic c82aefc44a test 2020-02-14 20:43:24 +01:00
jpic 40187a3234 test 2020-02-14 20:43:11 +01:00
jpic f8432035b2 wip 2020-02-14 20:33:43 +01:00
jpic c8c281ebcf Test fix in CI env 2020-02-14 20:29:12 +01:00
jpic 56ce02c234 Import at top 2020-02-14 20:26:12 +01:00
jpic 3aa8503803 Support running inside CI 2020-02-14 20:25:11 +01:00
jpic 535aa797b5 Remove old example 2020-02-14 20:23:04 +01:00
jpic 2e94928aef Extracted shlax from podctl 2020-02-14 20:14:56 +01:00
jpic c78874a7e0 Output flushing 2020-02-13 15:13:23 +01:00
jpic 5f740f3e72 Package mgr detection works fine now 2020-02-13 14:46:23 +01:00
jpic 7926bd6a2e Disable push during test 2020-02-13 13:09:33 +01:00
jpic 88f49aa094 Run test in podctl image 2020-02-13 12:45:34 +01:00
jpic 96ddc4f474 Push repotags 2020-02-13 12:23:35 +01:00
jpic 7f3d6a685d wip 2020-02-13 05:43:15 +01:00
jpic 4366ee63f9 wip 2020-02-13 05:42:04 +01:00
jpic 6e8b2ec2a7 IMprove 2020-02-13 04:53:09 +01:00
jpic 00fa16052d qarm 2020-02-13 04:20:27 +01:00
jpic 0a863fe766 black 2020-02-13 04:18:22 +01:00
jpic 10ee871caa Attempt actually tagging 2020-02-13 04:13:16 +01:00
jpic 92e7f2def9 Tag one by one 2020-02-13 04:04:09 +01:00
jpic e80955936c Fix podctl build itself 2020-02-13 03:57:27 +01:00
jpic f235db33ca fugbix 2020-02-13 03:32:45 +01:00
jpic 482f9203f7 GitLab ci 2020-02-13 03:28:31 +01:00
jpic be8460b372 Implement test runner 2020-02-13 03:27:14 +01:00
jpic e7b059ab44 Got crazy on output coloration 2020-02-13 01:26:33 +01:00
jpic 40335a88a7 Add auto-unshare super smartness 2020-02-12 21:35:02 +01:00
jpic 3413fd8981 Now to rewrite the test framework and good to go ! 2020-02-12 19:34:30 +01:00
jpic 2e9a23eb5a Add up command, allow fine grain debug output 2020-02-12 13:19:44 +01:00
jpic 409b625309 Remove Run 2020-02-12 12:01:47 +01:00
jpic e469455bc0 Fixed help 2020-02-12 11:55:19 +01:00
jpic b08d5e1a34 Refactor script part 2020-02-12 11:53:50 +01:00
jpic 6abb061dc8 wip 2020-02-12 03:19:21 +01:00
jpic f52cc8971a Migrate the whole thing from bash script generation to async processes 2020-02-10 17:49:14 +01:00
jpic dc92b484e8 Asyncio rewrite 2020-02-03 05:25:04 +01:00
jpic 7753175d6c Porting to subprocess 2020-02-03 02:17:45 +01:00
jpic 65ea77cb5e Add DumbInit, Template modules, and refactor Packages 2020-02-03 02:17:18 +01:00
jpic 6ad388fc5c Cmd: auto switch to heredoc if bash detected 2020-01-31 21:56:37 +01:00
jpic 0408590c4f Dedent cmd 2020-01-31 21:56:13 +01:00
jpic 7e9af1fe35 Fix TEST_REWRITE 2020-01-31 21:50:41 +01:00
jpic 8ae4687cf2 Multi apt distro support 2020-01-31 20:32:56 +01:00
jpic 6547888291 Fix package name 2020-01-31 20:18:51 +01:00
jpic d624884955 Add find package 2020-01-31 20:14:18 +01:00
jpic 3168149acb Default to master 2020-01-31 20:09:43 +01:00
jpic 120b5e8778 Fix tag command 2020-01-31 20:04:04 +01:00
jpic d25d1cdb63 Login if pushing only 2020-01-31 19:57:17 +01:00
jpic 897e069a11 Auto-push in Commit if on CI 2020-01-31 19:49:57 +01:00
jpic c600f01801 Debug output for build job 2020-01-31 19:43:57 +01:00
jpic d47ab86873 Make test portable 2020-01-31 19:35:44 +01:00
jpic 9eb0f5ffaa Add TEST_REWRITE env var to rewrite tests 2020-01-31 19:29:41 +01:00
jpic 3515dd2590 Ported User command ?
Did i break other distros it used to work on ?

Does it still work with alpine ?
2020-01-31 19:29:41 +01:00
jpic 66a0d6fcef Add --no-install-recommends to apt 2020-01-31 19:26:55 +01:00
jpic 7544d43dbc Fix npm 2020-01-31 19:26:38 +01:00
jpic cd5c5aa65f Quote config values 2020-01-31 19:26:23 +01:00
jpic 0da90a547b Fix apt caching 2020-01-28 12:29:25 +01:00
jpic e732dc2cff Don't trace bash script unless --debug 2020-01-28 12:29:17 +01:00
jpic 31297f26e9 Allow building a single service 2020-01-28 12:29:04 +01:00
jpic 8b63b0e510 Fix copy 2020-01-28 02:35:00 +01:00
jpic 533d26e6cf Fix npm install 2020-01-28 02:34:28 +01:00
jpic cdb68a80e5 Support apt update 2020-01-28 02:16:22 +01:00
jpic 3ec981a1c1 Add Npm, Cmd and Mount commands 2020-01-28 02:05:07 +01:00
jpic 85d3b98b64 Support requirements 2020-01-28 02:05:07 +01:00
jpic 60015cb90d Apt get support 2020-01-28 02:05:07 +01:00
jpic 3e76bdb96e Copy: support multiple srcs 2020-01-28 02:05:07 +01:00
jpic 8ee415e642 1337 multiline bash support 2020-01-28 02:05:07 +01:00
jpic 47db32ad0c GitLab cache 2020-01-26 20:52:55 +01:00
jpic 4f405a8709 QA & test update 2020-01-26 19:57:41 +01:00
jpic c817784eab Allow to pre-set the manager, avoids running podman 2020-01-26 19:55:43 +01:00
jpic 3d0b984623 Display what visitor is playing 2020-01-26 19:55:28 +01:00
jpic a4b71b07f0 Remove un-needed prefixing space 2020-01-26 19:55:18 +01:00
jpic 895f0e164b Multiline Run scripts, and going pretty far for python 3.8 2020-01-26 19:54:59 +01:00
jpic c88aea9ba9 Try to short-circuit podman execution 2020-01-26 18:34:16 +01:00
jpic 2aa657d5ef Refactor User.build 2020-01-26 18:12:15 +01:00
jpic ab756be215 Fix dnf commands 2020-01-26 18:11:58 +01:00
jpic b42b43ebbd Fix exit code 2020-01-26 18:11:39 +01:00
jpic 52fd87f2a0 Support running inside a container 2020-01-26 18:11:18 +01:00
jpic d17fdce9f9 Fix unmounts 2020-01-26 18:10:44 +01:00
jpic aea1e2bb74 Use podman as base 2020-01-26 18:10:30 +01:00
jpic 60dbde87be Try as root inside the container 2020-01-26 13:11:57 +01:00
jpic 108b8e7927 Build new versions with themselves 2020-01-26 13:10:15 +01:00
jpic 9f48652a63 Remove wrong script 2020-01-26 12:58:48 +01:00
jpic 5b32e2a91b Add build job 2020-01-26 12:45:01 +01:00
jpic bb5510a934 Test fix 2020-01-26 12:44:34 +01:00
jpic d43affe9bc Commit: support login, push, parse repo ... 2020-01-26 12:42:16 +01:00
jpic ec791a0446 Do not swallow return code 2020-01-26 12:41:57 +01:00
jpic 7d14d488da Refactor and fix umounts 2020-01-26 12:41:39 +01:00
jpic 9475d8b9af Do not export none vars 2020-01-26 12:40:58 +01:00
jpic 48e4094636 Change Tag by Commit, give it some powers 2020-01-25 19:23:42 +01:00
jpic d984987ca9 Add dnf and yum support, build podctl in centos 2020-01-25 17:33:22 +01:00
jpic 7a99d4cbca Mock subprocess 2020-01-25 16:58:57 +01:00
jpic a866ba5a0d Refactor into visitor pattern 2020-01-25 16:52:37 +01:00
jpic d5d924dd06 Build script suppotrs packages 2020-01-24 19:26:43 +01:00
42 changed files with 1913 additions and 606 deletions
+10
View File
@@ -0,0 +1,10 @@
*.pyc
__pycache__
.cache/
.coverage
.eggs/
.podctl_build_django.sh
.podctl_build_podctl.sh
.setupmeta.version
.testmondata
*.egg-info
+13 -18
View File
@@ -1,20 +1,15 @@
image: yourlabs/python-arch build:
cache:
qa: key: cache
stage: test paths: [.cache]
script: flake8 image: yourlabs/shlax
script: pip install -U --user -e . && CACHE_DIR=$(pwd)/.cache ./shlaxfile.py -d
pytest: shlax build push
stage: test stage: build
script:
- pip install --user -e .
- pytest -vv --cov shlax --cov-report=xml:coverage.xml --junitxml=report.xml --cov-report=term-missing --strict tests
- CI_PROJECT_PATH=yourlabs/shlax CI_BUILD_REPO=https://github.com/yourlabs/cli2 codecov-bash -f coverage.xml
artifacts:
reports:
junit: report.xml
pypi: pypi:
stage: deploy image: yourlabs/python
script: pypi-release
only: [tags] only: [tags]
script: pypi-release
stage: deploy
test: {image: yourlabs/python, script: 'pip install -U --user -e .[test] && py.test
-svv tests', stage: build}
+14
View File
@@ -0,0 +1,14 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://yourlabs.io/oss/shlax
rev: master
hooks:
- id: shlaxfile-gitlabci
+5
View File
@@ -0,0 +1,5 @@
- id: shlaxfile-gitlabci
name: Regenerate .gitlab-ci.yml
description: Regenerate gitlabci
entry: ./shlaxfile.py gitlabci
language: python
-110
View File
@@ -1,110 +0,0 @@
Shlax: Beautiful Async Subprocess executor
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Why?
====
In Python we now have async subprocesses which allows to execute several
subprocesses at the same time. The purpose of this library is to:
- stream stderr and stdout in real time while capturing it,
- real time output must be prefixed for when you execute several commands at
the time so that you know which line is for which process, like with
docker-compose logs,
- output coloration in real time with regexps to make even more readable.
This code was copy/pasted between projects and finally extracted on its own.
Demo
====
.. image:: https://yourlabs.io/oss/shlax/-/raw/master/demo.png
You will find the demo script in demo.py in this repository.
Usage
=====
Basics
------
Basic example, this will both stream output and capture it:
.. code-block:: python
from shlax import Subprocess
proc = await Subprocess('echo hi').wait()
print(proc.rc, proc.out, proc.err, proc.out_raw, proc.err_raw)
Longer
------
If you want to start the command and wait for completion elsewhere then call
any of ``start()`` and ``wait()``, or both, explicitely:
.. code-block:: python
proc = Subprocess('echo hi')
await proc.start() # start the process
await proc.wait() # wait for completion
Proc alias
----------
Note that shlax defines an alias ``Proc`` to ``Subprocess`` so this also works:
.. code-block:: python
from shlax import Proc
proc = await Proc('echo hi').wait()
Quiet
-----
To disable real time output streaming use the ``quiet`` argument:
.. code-block:: python
proc = await Subprocess('echo hi', quiet=True).wait()
Prefix
------
Using prefixes, you can have real time outputs of parallel commands and at the
same time know which output belongs to which process:
.. code-block:: python
proc0 = Subprocess('find /', prefix='first')
proc1 = Subprocess('find /', prefix='second')
await asyncio.gather(proc0.wait(), proc1.wait())
Coloration and output patching
------------------------------
You can add coloration or patch real time output with regexps, note that it
will be applied line by line:
.. code-block:: python
import sys
regexps = {
'^(.*).py$': '{cyan}\\1',
}
await asyncio.gather(*[
Subprocess(
f'find {path}',
regexps=regexps,
).wait()
for path in sys.path
])
Where is the rest?
==================
Shlax used to be the name of a much more ambitious poc-project, that you can
still find in the ``OLD`` branch of this repository. It has been extracted in
two projects with clear boundaries, namely `sysplan
<https://yourlabs.io/oss/sysplan>`_ and `podplan
<https://yourlabs.io/oss/podplan>`_ which are still in alpha state, although
Shlax as it now, is feature complete and stable.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 990 KiB

-24
View File
@@ -1,24 +0,0 @@
import asyncio
from shlax import Subprocess
async def main():
colors = {
'^(.*).txt$': '{green}\\1.txt',
'^(.*).py$': '{bred}\\1.py',
}
await asyncio.gather(
Subprocess(
'for i in $(find .. | head); do echo $i; sleep .2; done',
regexps=colors,
prefix='parent',
).wait(),
Subprocess(
'for i in $(find . | head); do echo $i; sleep .3; done',
regexps=colors,
prefix='cwd',
).wait(),
)
asyncio.run(main())
+10 -1
View File
@@ -5,7 +5,11 @@ setup(
name='shlax', name='shlax',
versioning='dev', versioning='dev',
setup_requires='setupmeta', setup_requires='setupmeta',
install_requires=['cli2>=1.1.6'],
extras_require=dict( extras_require=dict(
full=[
'pyyaml',
],
test=[ test=[
'pytest', 'pytest',
'pytest-cov', 'pytest-cov',
@@ -17,6 +21,11 @@ setup(
url='https://yourlabs.io/oss/shlax', url='https://yourlabs.io/oss/shlax',
include_package_data=True, include_package_data=True,
license='MIT', license='MIT',
keywords='async subprocess', keywords='cli automation ansible',
python_requires='>=3', python_requires='>=3',
entry_points={
'console_scripts': [
'shlax = shlax.cli:cli',
],
},
) )
+7 -3
View File
@@ -1,3 +1,7 @@
from .subprocess import Subprocess # noqa from .actions import *
Proc = Subprocess # noqa from .image import Image
from .colors import colors, c # noqa from .strategies import *
from .output import Output
from .proc import Proc
from .targets import *
from .shlaxfile import Shlaxfile
+7
View File
@@ -0,0 +1,7 @@
from .copy import Copy
from .packages import Packages # noqa
from .base import Action # noqa
from .htpasswd import Htpasswd
from .run import Run # noqa
from .pip import Pip
from .service import Service
+225
View File
@@ -0,0 +1,225 @@
from copy import deepcopy
import functools
import inspect
import importlib
import sys
from ..output import Output
from ..exceptions import WrongResult
class Action:
parent = None
contextualize = []
regexps = {
r'([\w]+):': '{cyan}\\1{gray}:{reset}',
r'(^|\n)( *)\- ': '\\1\\2{red}-{reset} ',
}
options = dict(
debug=dict(
alias='d',
default='visit',
help='''
Display debug output. Supports values (combinable): cmd,out,visit
'''.strip(),
immediate=True,
),
)
def __init__(self, *args, doc=None, **kwargs):
self.args = args
self.kwargs = kwargs
self.call_args = []
self.call_kwargs = {}
self._doc = doc
self.menu = {
name: value
for name, value in kwargs.items()
if isinstance(value, Action)
}
@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):
if not self.parent:
return []
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(f'{type(self).__name__} has no {name}')
async def call(self, *args, **kwargs):
print(f'{self}.call(*args, **kwargs) not implemented')
sys.exit(1)
def output_factory(self, *args, **kwargs):
kwargs.setdefault('regexps', self.regexps)
return Output(**kwargs)
async def __call__(self, *args, **kwargs):
self.call_args = list(self.call_args) + list(args)
self.call_kwargs.update(kwargs)
self.output = self.output_factory(*args, **kwargs)
self.output_start()
self.status = 'running'
try:
result = await self.call(*args, **kwargs)
except Exception as e:
self.output_fail(e)
self.status = 'fail'
proc = getattr(e, 'proc', None)
if proc:
result = proc.rc
else:
raise
else:
self.output_success()
if self.status == 'running':
self.status = 'success'
finally:
clean = getattr(self, 'clean', None)
if clean:
self.output.clean(self)
await clean(*args, **kwargs)
return result
def output_start(self):
if self.kwargs.get('quiet', False):
return
self.output.start(self)
def output_fail(self, exception=None):
if self.kwargs.get('quiet', False):
return
self.output.fail(self, exception)
def output_success(self):
if self.kwargs.get('quiet', False):
return
self.output.success(self)
def __repr__(self):
return ' '.join([type(self).__name__] + list(self.args) + [
f'{k}={v}'
for k, v in self.kwargs.items()
])
def colorized(self):
return ' '.join([
self.output.colors['pink1']
+ type(self).__name__
+ self.output.colors['yellow']
] + list(self.args) + [
f'{self.output.colors["blue"]}{k}{self.output.colors["gray"]}={self.output.colors["green2"]}{v}'
for k, v in self.kwargs_output().items()
] + [self.output.colors['reset']])
def callable(self):
from ..targets import Localhost
async def cb(*a, **k):
from shlax.cli import cli
script = Localhost(self, quiet=True)
result = await script(*a, **k)
success = functools.reduce(
lambda a, b: a + b,
[1 for c in script.children() if c.status == 'success'] or [0])
if success:
script.output.success(f'{success} PASS')
failures = functools.reduce(
lambda a, b: a + b,
[1 for c in script.children() if c.status == 'fail'] or [0])
if failures:
script.output.fail(f'{failures} FAIL')
cli.exit_code = failures
return result
return cb
def kwargs_output(self):
return self.kwargs
def action(self, action, *args, **kwargs):
if isinstance(action, str):
import cli2
a = cli2.Callable.factory(action).target
if not a:
a = cli2.Callable.factory(
'.'.join(['shlax', action])
).target
if a:
action = a
p = action(*args, **kwargs)
for parent in self.parents():
if hasattr(parent, 'actions'):
break
p.parent = parent
if 'actions' not in self.__dict__:
# "mutate" to Strategy
from ..strategies.script import Actions
self.actions = Actions(self, [p])
return p
def bind(self, *args):
clone = deepcopy(self)
clone.call_args = args
return clone
+6
View File
@@ -0,0 +1,6 @@
from .base import Action
class Copy(Action):
async def call(self, *args, **kwargs):
await self.copy(*self.args)
+29
View File
@@ -0,0 +1,29 @@
import hashlib
import secrets
import string
from .base import Action
class Htpasswd(Action):
def __init__(self, path, user, *args, **kwargs):
self.path = path
self.user = user
super().__init__(*args, **kwargs)
async def call(self, *args, **kwargs):
found = False
htpasswd = await self.exec('cat', self.path, raises=False)
if htpasswd.rc == 0:
for line in htpasswd.out.split('\n'):
if line.startswith(self.user + ':'):
found = True
break
if not found:
self.password = ''.join(secrets.choice(
string.ascii_letters + string.digits
) for i in range(20))
hashed = hashlib.sha1(self.password.encode('utf8'))
line = f'{self.user}:\\$sha1\\${hashed.hexdigest()}'
await self.exec(f'echo {line} >> {self.path}')
+170
View File
@@ -0,0 +1,170 @@
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']
regexps = {
#r'Installing ([\w\d-]+)': '{cyan}\\1',
r'Installing': '{cyan}lol',
}
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(' ')
super().__init__(*packages, **kwargs)
@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.keys())
if mgr:
self.mgr = mgr[0].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()
if self.kwargs.get('upgrade', True):
await self.rexec(self.cmds['upgrade'])
self._packages_upgraded = True
packages = []
for package in self.packages:
if ',' in package:
parts = package.split(',')
package = parts[0]
if self.mgr in parts[1:]:
# include apt on apt
packages.append(package)
else:
packages.append(package)
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 -sf /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.rexec('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})'
+57
View File
@@ -0,0 +1,57 @@
from glob import glob
import os
from .base import Action
class Pip(Action):
def __init__(self, *pip_packages, pip=None, requirements=None):
self.requirements = requirements
super().__init__(*pip_packages, pip=pip, requirements=requirements)
async def call(self, *args, **kwargs):
pip = self.kwargs.get('pip', None)
if not pip:
pip = await self.which('pip3', 'pip', 'pip2')
if pip:
pip = pip[0]
else:
from .packages import Packages
action = self.action(
Packages,
'python3,apk', 'python3-pip,apt',
args=args, kwargs=kwargs
)
await action(*args, **kwargs)
pip = await self.which('pip3', 'pip', 'pip2')
if not pip:
raise Exception('Could not install a pip command')
else:
pip = pip[0]
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')
if getattr(self, 'mount', None):
# we are in a target which shares a mount command
await self.mount(cache, '/root/.cache/pip')
await self.exec(f'{pip} install --upgrade pip')
# https://github.com/pypa/pip/issues/5599
if 'pip' not in self.kwargs:
pip = 'python3 -m pip'
source = [p for p in self.args if p.startswith('/') or p.startswith('.')]
if source:
await self.exec(
f'{pip} install --upgrade --editable {" ".join(source)}'
)
nonsource = [p for p in self.args if not p.startswith('/')]
if nonsource:
await self.exec(f'{pip} install --upgrade {" ".join(nonsource)}')
if self.requirements:
await self.exec(f'{pip} install --upgrade -r {self.requirements}')
+18
View File
@@ -0,0 +1,18 @@
from ..targets.buildah import Buildah
from ..targets.docker import Docker
from .base import Action
class Run(Action):
async def call(self, *args, **kwargs):
image = self.kwargs.get('image', None)
if not image:
return await self.exec(*self.args, **self.kwargs)
if isinstance(image, Buildah):
breakpoint()
result = await self.action(image, *args, **kwargs)
return await Docker(
image=image,
).exec(*args, **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
])
+86
View File
@@ -0,0 +1,86 @@
'''
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 copy
import inspect
import importlib
import glob
import os
import sys
from .exceptions import *
from .shlaxfile import Shlaxfile
from .targets import Localhost
class ConsoleScript(cli2.ConsoleScript):
def __call__(self, *args, **kwargs):
self.shlaxfile = None
shlaxfile = sys.argv.pop(1) if len(sys.argv) > 1 else ''
if shlaxfile:
if not os.path.exists(shlaxfile):
try: # missing shlaxfile, what are we gonna do !!
mod = importlib.import_module('shlax.repo.' + shlaxfile)
except ImportError:
print('Could not find ' + shlaxfile)
self.exit_code = 1
return
shlaxfile = mod.__file__
self.shlaxfile = Shlaxfile()
self.shlaxfile.parse(shlaxfile)
self._doc = inspect.getdoc(mod)
if 'main' in self.shlaxfile.actions:
action = self.shlaxfile.actions['main']
for name, child in self.shlaxfile.actions['main'].menu.items():
self[name] = cli2.Callable(
name,
child.callable(),
options={
k: cli2.Option(name=k, **v)
for k, v in action.options.items()
},
color=getattr(action, 'color', cli2.YELLOW),
)
for name, action in self.shlaxfile.actions.items():
self[name] = cli2.Callable(
name,
action.callable(),
options={
k: cli2.Option(name=k, **v)
for k, v in action.options.items()
},
color=getattr(action, 'color', cli2.YELLOW),
doc=inspect.getdoc(getattr(action, name, None)) or action._doc,
)
else:
from shlax import repo
path = repo.__path__._path[0]
for shlaxfile in glob.glob(os.path.join(path, '*.py')):
name = shlaxfile.split('/')[-1].split('.')[0]
mod = importlib.import_module('shlax.repo.' + name)
self[name] = cli2.Callable(name, mod)
return super().__call__(*args, **kwargs)
def call(self, command):
kwargs = copy.copy(self.parser.funckwargs)
kwargs.update(self.parser.options)
try:
return command(*self.parser.funcargs, **kwargs)
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')
+66 -71
View File
@@ -1,73 +1,68 @@
theme = dict( colors = dict(
cyan='\033[38;5;51m', cyan='\u001b[38;5;51m',
cyan1='\033[38;5;87m', cyan1='\u001b[38;5;87m',
cyan2='\033[38;5;123m', cyan2='\u001b[38;5;123m',
cyan3='\033[38;5;159m', cyan3='\u001b[38;5;159m',
blue='\033[38;5;33m', blue='\u001b[38;5;33m',
blue1='\033[38;5;69m', blue1='\u001b[38;5;69m',
blue2='\033[38;5;75m', blue2='\u001b[38;5;75m',
blue3='\033[38;5;81m', blue3='\u001b[38;5;81m',
blue4='\033[38;5;111m', blue4='\u001b[38;5;111m',
blue5='\033[38;5;27m', blue5='\u001b[38;5;27m',
green='\033[38;5;10m', green='\u001b[38;5;10m',
green1='\033[38;5;2m', green1='\u001b[38;5;2m',
green2='\033[38;5;46m', green2='\u001b[38;5;46m',
green3='\033[38;5;47m', green3='\u001b[38;5;47m',
green4='\033[38;5;48m', green4='\u001b[38;5;48m',
green5='\033[38;5;118m', green5='\u001b[38;5;118m',
green6='\033[38;5;119m', green6='\u001b[38;5;119m',
green7='\033[38;5;120m', green7='\u001b[38;5;120m',
purple='\033[38;5;5m', purple='\u001b[38;5;5m',
purple1='\033[38;5;6m', purple1='\u001b[38;5;6m',
purple2='\033[38;5;13m', purple2='\u001b[38;5;13m',
purple3='\033[38;5;164m', purple3='\u001b[38;5;164m',
purple4='\033[38;5;165m', purple4='\u001b[38;5;165m',
purple5='\033[38;5;176m', purple5='\u001b[38;5;176m',
purple6='\033[38;5;145m', purple6='\u001b[38;5;145m',
purple7='\033[38;5;213m', purple7='\u001b[38;5;213m',
purple8='\033[38;5;201m', purple8='\u001b[38;5;201m',
red='\033[38;5;1m', red='\u001b[38;5;1m',
red1='\033[38;5;9m', red1='\u001b[38;5;9m',
red2='\033[38;5;196m', red2='\u001b[38;5;196m',
red3='\033[38;5;160m', red3='\u001b[38;5;160m',
red4='\033[38;5;197m', red4='\u001b[38;5;197m',
red5='\033[38;5;198m', red5='\u001b[38;5;198m',
red6='\033[38;5;199m', red6='\u001b[38;5;199m',
yellow='\033[38;5;226m', yellow='\u001b[38;5;226m',
yellow1='\033[38;5;227m', yellow1='\u001b[38;5;227m',
yellow2='\033[38;5;226m', yellow2='\u001b[38;5;226m',
yellow3='\033[38;5;229m', yellow3='\u001b[38;5;229m',
yellow4='\033[38;5;220m', yellow4='\u001b[38;5;220m',
yellow5='\033[38;5;230m', yellow5='\u001b[38;5;230m',
gray='\033[38;5;250m', gray='\u001b[38;5;250m',
gray1='\033[38;5;251m', gray1='\u001b[38;5;251m',
gray2='\033[38;5;252m', gray2='\u001b[38;5;252m',
gray3='\033[38;5;253m', gray3='\u001b[38;5;253m',
gray4='\033[38;5;254m', gray4='\u001b[38;5;254m',
gray5='\033[38;5;255m', gray5='\u001b[38;5;255m',
gray6='\033[38;5;249m', gray6='\u001b[38;5;249m',
pink='\033[38;5;197m', pink='\u001b[38;5;197m',
pink1='\033[38;5;198m', pink1='\u001b[38;5;198m',
pink2='\033[38;5;199m', pink2='\u001b[38;5;199m',
pink3='\033[38;5;200m', pink3='\u001b[38;5;200m',
pink4='\033[38;5;201m', pink4='\u001b[38;5;201m',
pink5='\033[38;5;207m', pink5='\u001b[38;5;207m',
pink6='\033[38;5;213m', pink6='\u001b[38;5;213m',
orange='\033[38;5;202m', orange='\u001b[38;5;202m',
orange1='\033[38;5;208m', orange1='\u001b[38;5;208m',
orange2='\033[38;5;214m', orange2='\u001b[38;5;214m',
orange3='\033[38;5;220m', orange3='\u001b[38;5;220m',
orange4='\033[38;5;172m', orange4='\u001b[38;5;172m',
orange5='\033[38;5;166m', orange5='\u001b[38;5;166m',
reset='\033[0m', reset='\u001b[0m',
) )
colors.update({
class Colors: k + 'bold': v.replace('[', '[1;')
def __init__(self, **theme): for k, v in colors.items()
for name, value in theme.items(): })
setattr(self, name, value)
setattr(self, f'b{name}', value.replace('[', '[1;'))
c = colors = Colors(**theme)
+37
View File
@@ -0,0 +1,37 @@
from copy import deepcopy
import yaml
from shlax import *
class GitLabCI(Script):
async def call(self, *args, write=True, **kwargs):
output = dict()
for key, value in self.kwargs.items():
if isinstance(value, dict):
output[key] = deepcopy(value)
image = output[key].get('image', 'alpine')
if hasattr(image, 'image'):
output[key]['image'] = image.image.repository + ':$CI_COMMIT_SHORT_SHA'
else:
output[key] = value
output = yaml.dump(output)
if kwargs['debug'] is True:
self.output(output)
if write:
with open('.gitlab-ci.yml', 'w+') as f:
f.write(output)
from shlax.cli import cli
for arg in args:
job = self.kwargs[arg]
_args = []
if not isinstance(job['image'], str):
image = str(job['image'].image)
else:
image = job['image']
await self.action('Docker', Run(job['script']), image=image)(*_args, **kwargs)
def colorized(self):
return type(self).__name__
+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)
+75
View File
@@ -0,0 +1,75 @@
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']
async def __call__(self, action, *args, **kwargs):
args = list(args)
return await action.exec(*args, **self.kwargs)
def __str__(self):
return f'{self.repository}:{self.tags[-1]}'
async def push(self, *args, **kwargs):
user = os.getenv('DOCKER_USER')
passwd = os.getenv('DOCKER_PASS')
action = kwargs.get('action', self)
if user and passwd:
action.output.cmd('buildah login -u ... -p ...' + self.registry)
await action.exec('buildah', 'login', '-u', user, '-p', passwd, self.registry or 'docker.io', debug=False)
for tag in self.tags:
await action.exec('buildah', 'push', f'{self.repository}:{tag}')
+149
View File
@@ -0,0 +1,149 @@
import re
import sys
from .colors import colors
class Output:
prefixes = dict()
colors = colors
prefix_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 color(self, code=None):
if not code:
return '\u001b[0m'
code = str(code)
return u"\u001b[38;5;" + code + "m"
def colorize(self, code, content):
return self.color(code) + content + self.color()
def __init__(self, prefix=None, regexps=None, debug=False, write=None, flush=None, **kwargs):
self.prefix = prefix
self.debug = debug
self.prefix_length = 0
self.regexps = regexps or dict()
self.write = write or sys.stdout.buffer.write
self.flush = flush or sys.stdout.flush
self.kwargs = kwargs
def prefix_line(self):
if self.prefix not in self.prefixes:
self.prefixes[self.prefix] = self.prefix_colors[len(self.prefixes)]
if len(self.prefix) > self.prefix_length:
self.prefix_length = len(self.prefix)
prefix_color = self.prefixes[self.prefix] if self.prefix else ''
prefix_padding = '.' * (self.prefix_length - len(self.prefix) - 2) if self.prefix else ''
if prefix_padding:
prefix_padding = ' ' + prefix_padding + ' '
return [
prefix_color,
prefix_padding,
self.prefix,
' ',
self.colors['reset'],
'| '
]
def __call__(self, line, highlight=True, flush=True):
line = [self.highlight(line) if highlight else line]
if self.prefix:
line = self.prefix_line() + line
line = ''.join(line)
self.write(line.encode('utf8'))
if flush:
self.flush()
def cmd(self, line):
self(
self.colorize(251, '+')
+ '\x1b[1;38;5;15m'
+ ' '
+ self.highlight(line, 'bash')
+ self.colors['reset']
+ '\n',
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
for regexp, colors in self.regexps.items():
line = re.sub(regexp, colors.format(**self.colors), line)
line = line + self.colors['reset']
return line
def test(self, action):
if self.debug is True:
self(''.join([
self.colors['purplebold'],
'! TEST ',
self.colors['reset'],
action.colorized(),
'\n',
]))
def clean(self, action):
if self.debug is True:
self(''.join([
self.colors['bluebold'],
'+ CLEAN ',
self.colors['reset'],
action.colorized(),
'\n',
]))
def start(self, action):
if self.debug is True or 'visit' in str(self.debug):
self(''.join([
self.colors['orangebold'],
'⚠ START ',
self.colors['reset'],
action.colorized(),
'\n',
]))
def success(self, action):
if self.debug is True or 'visit' in str(self.debug):
self(''.join([
self.colors['greenbold'],
'✔ SUCCESS ',
self.colors['reset'],
action.colorized() if hasattr(action, 'colorized') else str(action),
'\n',
]))
def fail(self, action, exception=None):
if self.debug is True or 'visit' in str(self.debug):
self(''.join([
self.colors['redbold'],
'✘ FAIL ',
self.colors['reset'],
action.colorized() if hasattr(action, 'colorized') else str(action),
'\n',
]))
+142
View File
@@ -0,0 +1,142 @@
"""
Asynchronous process execution wrapper.
"""
import asyncio
import os
import shlex
import sys
from .exceptions import WrongResult
from .output import 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, output, *args, **kwargs):
self.output = output
super().__init__(*args, **kwargs)
def pipe_data_received(self, fd, data):
if self.output.debug is True or 'out' in str(self.output.debug):
if fd in (1, 2):
self.output(data)
super().pipe_data_received(fd, data)
def protocol_factory(output):
def _p():
return PrefixStreamProtocol(
output,
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=None, output=None):
self.debug = debug if not self.test else False
self.output = output or Output()
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
def output_factory(self, *args, **kwargs):
args = tuple(self.prefix) + args
return Output(*args, kwargs)
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):
self.output.cmd(self.cmd)
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.output), *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()
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env shlax
"""
Manage a traefik container maintained by Shlax community.
"""
from shlax import *
main = Docker(
name='traefik',
image='traefik:v2.0.0',
networks=['web'],
command=[
'--entrypoints.web.address=:80',
'--providers.docker',
'--api',
],
ports=[
'80:80',
'443:443',
],
volumes=[
'/var/run/docker.sock:/var/run/docker.sock:ro',
'/etc/traefik/acme/:/etc/traefik/acme/',
'/etc/traefik/htpasswd:/htpasswd:ro',
],
labels=[
'traefik.http.routers.traefik.rule=Host(`{{ url.split("/")[2] }}`)',
'traefik.http.routers.traefik.service=api@internal',
'traefik.http.routers.traefik.entrypoints=web',
],
doc='Current traefik instance',
)
install = Script(
Htpasswd('./htpasswd', 'root'),
main.bind('up'),
doc='Deploy a Traefik instance',
)
up = main.bind('up')
rm = main.bind('rm')
+28
View File
@@ -0,0 +1,28 @@
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]
+3
View File
@@ -0,0 +1,3 @@
from .asyn import Async
from .script import Script
from .pod import Pod, Container
+11
View File
@@ -0,0 +1,11 @@
import asyncio
from .script import Script
class Async(Script):
async def call(self, *args, **kwargs):
return await asyncio.gather(*[
action(*args, **kwargs)
for action in self.actions
])
+44
View File
@@ -0,0 +1,44 @@
import os
from .script import Script
from ..image import Image
class Container(Script):
"""
Wolcome to crazy container control cli
Such wow
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault('start', dict())
super().__init__(*args, **kwargs)
async def call(self, *args, **kwargs):
if step('build'):
await self.kwargs['build'](**kwargs)
self.image = self.kwargs['build'].image
else:
self.image = kwargs.get('image', 'alpine')
if isinstance(self.image, str):
self.image = Image(self.image)
if step('install'):
await self.install(*args, **kwargs)
if step('test'):
self.output.test(self)
await self.action('Docker',
*self.kwargs['test'].actions,
image=self.image,
mount={'.': '/app'},
workdir='/app',
)(**kwargs)
if step('push'):
await self.image.push(action=self)
#name = kwargs.get('name', os.getcwd()).split('/')[-1]
class Pod(Script):
pass
+41
View File
@@ -0,0 +1,41 @@
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):
action = copy.deepcopy(value)
action.parent = self.owner
action.status = 'pending'
super().append(action)
class Script(Action):
contextualize = ['shargs', 'exec', 'rexec', 'env', 'which', 'copy']
def __init__(self, *actions, **kwargs):
self.home = kwargs.pop('home', os.getcwd())
super().__init__(**kwargs)
self.actions = Actions(self, actions)
async def call(self, *args, **kwargs):
for action in self.actions:
result = await action(*args, **kwargs)
if action.status != 'success':
break
def pollute(self, gbls):
for name, script in self.kwargs.items():
if not isinstance(script, Script):
continue
gbls[name] = script
-182
View File
@@ -1,182 +0,0 @@
import asyncio
import functools
import re
import shlex
import sys
from .colors import colors
class SubprocessProtocol(asyncio.SubprocessProtocol):
def __init__(self, proc):
self.proc = proc
self.output = bytearray()
def pipe_data_received(self, fd, data):
if fd == 1:
self.proc.stdout(data)
elif fd == 2:
self.proc.stderr(data)
def process_exited(self):
self.proc.exit_future.set_result(True)
class Subprocess:
colors = colors
# arbitrary list of colors
prefix_colors = (
colors.cyan,
colors.blue,
colors.green,
colors.purple,
colors.red,
colors.yellow,
colors.gray,
colors.pink,
colors.orange,
)
# class variables, meant to grow as new prefixes are discovered to ensure
# output alignment
prefixes = dict()
prefix_length = 0
def __init__(
self,
*args,
quiet=None,
prefix=None,
regexps=None,
write=None,
flush=None,
):
if len(args) == 1 and ' ' in args[0]:
args = ['sh', '-euc', args[0]]
self.args = args
self.quiet = quiet if quiet is not None else False
self.prefix = prefix
self.write = write or sys.stdout.buffer.write
self.flush = flush or sys.stdout.flush
self.started = False
self.waited = False
self.out_raw = bytearray()
self.err_raw = bytearray()
self.regexps = dict()
if regexps:
for search, replace in regexps.items():
if isinstance(search, str):
search = search.encode()
search = re.compile(search)
replace = replace.format(**self.colors.__dict__).encode()
self.regexps[search] = replace
async def start(self, wait=True):
if not self.quiet:
self.output(
self.colors.bgray.encode()
+ b'+ '
+ shlex.join([
arg.replace('\n', '\\n')
for arg in self.args
]).encode()
+ self.colors.reset.encode(),
highlight=False
)
# Get a reference to the event loop as we plan to use
# low-level APIs.
loop = asyncio.get_running_loop()
self.exit_future = asyncio.Future(loop=loop)
# Create the subprocess controlled by DateProtocol;
# redirect the standard output into a pipe.
self.transport, self.protocol = await loop.subprocess_exec(
lambda: SubprocessProtocol(self),
*self.args,
stdin=None,
)
self.started = True
async def wait(self, *args, **kwargs):
if not self.started:
await self.start()
if not self.waited:
# Wait for the subprocess exit using the process_exited()
# method of the protocol.
await self.exit_future
# Close the stdout pipe.
self.transport.close()
self.waited = True
return self
def stdout(self, data):
self.out_raw.extend(data)
if not self.quiet:
self.output(data)
def stderr(self, data):
self.err_raw.extend(data)
if not self.quiet:
self.output(data)
@functools.cached_property
def out(self):
return self.out_raw.decode().strip()
@functools.cached_property
def err(self):
return self.err_raw.decode().strip()
@functools.cached_property
def rc(self):
return self.transport.get_returncode()
def output(self, data, highlight=True, flush=True):
for line in data.strip().split(b'\n'):
line = [self.highlight(line) if highlight else line]
if self.prefix:
line = self.prefix_line() + line
line.append(b'\n')
line = b''.join(line)
self.write(line)
if flush:
self.flush()
def highlight(self, line, highlight=True):
if not highlight or (
b'\x1b[' in line
or b'\033[' in line
or b'\\e[' in line
):
return line
for search, replace in self.regexps.items():
line = re.sub(search, replace, line)
line = line + self.colors.reset.encode()
return line
def prefix_line(self):
if self.prefix not in self.prefixes:
self.prefixes[self.prefix] = self.prefix_colors[len(self.prefixes)]
if len(self.prefix) > self.prefix_length:
type(self).prefix_length = len(self.prefix)
return [
self.prefixes[self.prefix].encode(),
b' ' * (self.prefix_length - len(self.prefix)),
self.prefix.encode(),
b' ',
self.colors.reset.encode(),
b'| '
]
+4
View File
@@ -0,0 +1,4 @@
from .buildah import Buildah
from .docker import Docker
from .localhost import Localhost
from .ssh import Ssh
+156
View File
@@ -0,0 +1,156 @@
import asyncio
import os
import asyncio
from pathlib import Path
import signal
import shlex
import subprocess
import sys
import textwrap
from ..actions.base import Action
from ..exceptions import Mistake
from ..proc import Proc
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', 'mount', 'image']
def __init__(self, base, *args, commit=None, push=False, cmd=None, **kwargs):
if isinstance(base, Action):
args = [base] + list(args)
base = 'alpine' # default selection in case of mistake
super().__init__(*args, **kwargs)
self.base = base
self.mounts = dict()
self.ctr = None
self.mnt = None
self.image = Image(commit) if commit else None
self.config= dict(
cmd=cmd or 'sh',
)
def shargs(self, *args, user=None, buildah=True, **kwargs):
if not buildah or args[0].startswith('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}', buildah=False)
async def copy(self, *args):
"""Run buildah copy to copy a file from host into container."""
src = args[:-1]
dst = args[-1]
await self.mkdir(dst)
procs = []
for s in src:
if Path(s).is_dir():
target = self.mnt / s
if not target.exists():
await self.mkdir(target)
args = ['buildah', 'copy', self.ctr, s, Path(dst) / s]
else:
args = ['buildah', 'copy', self.ctr, s, dst]
procs.append(self.exec(*args, buildah=False))
return await asyncio.gather(*procs)
async def mount(self, src, dst):
"""Mount a host directory into the container."""
target = self.mnt / str(dst)[1:]
await self.exec(f'mkdir -p {src} {target}', buildah=False)
await self.exec(f'mount -o bind {src} {target}', buildah=False)
self.mounts[src] = dst
def is_runnable(self):
return (
Proc.test
or os.getuid() == 0
)
async def call(self, *args, **kwargs):
if self.is_runnable():
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)
return result
from shlax.cli import cli
debug = kwargs.get('debug', False)
# restart under buildah unshare environment
argv = [
'buildah', 'unshare',
sys.argv[0], # current script location
cli.shlaxfile.path, # current shlaxfile location
]
if debug is True:
argv.append('-d')
elif isinstance(debug, str) and debug:
argv.append('-d=' + debug)
argv += [
cli.parser.command.name, # script name ?
]
await self.exec(*argv)
async def commit(self):
if not self.image:
return
for key, value in self.config.items():
await self.exec(f'buildah config --{key} "{value}" {self.ctr}')
self.sha = (await self.exec(
'buildah',
'commit',
'--format=' + self.image.format,
self.ctr,
buildah=False,
)).out
if self.image.tags:
tags = [f'{self.image.repository}:{tag}' for tag in self.image.tags]
else:
tags = [self.image.repository]
for tag in tags:
await self.exec('buildah', 'tag', self.sha, tag, buildah=False)
async def clean(self, *args, **kwargs):
if self.is_runnable():
for src, dst in self.mounts.items():
await self.exec('umount', self.mnt / str(dst)[1:], buildah=False)
if self.status == 'success':
await self.commit()
if 'push' in args:
await self.image.push(action=self)
if self.mnt is not None:
await self.exec('buildah', 'umount', self.ctr, buildah=False)
if self.ctr is not None:
await self.exec('buildah', 'rm', self.ctr, buildah=False)
+99
View File
@@ -0,0 +1,99 @@
import asyncio
from pathlib import Path
import os
from ..image import Image
from .localhost import Localhost
class Docker(Localhost):
contextualize = Localhost.contextualize + ['mnt', 'ctr', 'mount']
def __init__(self, *args, **kwargs):
self.image = kwargs.get('image', 'alpine')
self.name = kwargs.get('name', os.getcwd().split('/')[-1])
if not isinstance(self.image, Image):
self.image = Image(self.image)
super().__init__(*args, **kwargs)
def shargs(self, *args, daemon=False, **kwargs):
if args[0] == 'docker':
return args, kwargs
extra = []
if 'user' in kwargs:
extra += ['--user', kwargs.pop('user')]
args, kwargs = super().shargs(*args, **kwargs)
if self.name:
executor = 'exec'
extra = [self.name]
return [self.kwargs.get('docker', 'docker'), executor, '-t'] + extra + list(args), kwargs
executor = 'run'
cwd = os.getcwd()
if daemon:
extra += ['-d']
extra = extra + ['-v', f'{cwd}:{cwd}', '-w', f'{cwd}']
return [self.kwargs.get('docker', 'docker'), executor, '-t'] + extra + [str(self.image)] + list(args), kwargs
async def call(self, *args, **kwargs):
def step(step):
return not args or step in args
# self.name = (
# await self.exec(
# 'docker', 'ps', '-aq', '--filter',
# 'name=' + self.name,
# raises=False
# )
# ).out.split('\n')[0]
if step('rm'):
await self.rm(*args, **kwargs)
if step('down') and self.name:
await self.exec('docker', 'down', '-f', self.name)
if step('up'):
await self.up(*args, **kwargs)
return await super().call(*args, **kwargs)
async def rm(self, *args, **kwargs):
return await self.exec('docker', 'rm', '-f', self.name)
async def down(self, *args, **kwargs):
"""Remove instance, except persistent data if any"""
if self.name:
self.name = (await self.exec('docker', 'start', self.name)).out
else:
self.name = (await self.exec('docker', 'run', '-d', '--name', self.name)).out
async def up(self, *args, **kwargs):
"""Perform start or run"""
if self.name:
self.name = (await self.exec('docker', 'start', self.name)).out
else:
self.id = (await self.exec('docker', 'run', '-d', '--name', self.name)).out
async def copy(self, *args):
src = args[:-1]
dst = args[-1]
await self.mkdir(dst)
procs = []
for s in src:
'''
if Path(s).is_dir():
await self.mkdir(s)
args = ['docker', 'copy', self.ctr, s, Path(dst) / s]
else:
args = ['docker', 'copy', self.ctr, s, dst]
'''
args = ['docker', 'cp', s, self.name + ':' + dst]
procs.append(self.exec(*args))
return await asyncio.gather(*procs)
+82
View File
@@ -0,0 +1,82 @@
import os
import re
from shlax.proc import Proc
from ..strategies.script import Script
class Localhost(Script):
root = '/'
contextualize = Script.contextualize + ['home']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.home = kwargs.pop('home', os.getcwd())
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
if self.parent:
return self.parent.shargs(*args, **kwargs)
else:
return args, kwargs
async def exec(self, *args, **kwargs):
if 'debug' not in kwargs:
kwargs['debug'] = getattr(self, 'call_kwargs', {}).get('debug', False)
kwargs.setdefault('output', self.output)
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 exists(self, *paths):
proc = await self.exec('type ' + ' '.join(cmd), raises=False)
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.
"""
proc = await self.exec('type ' + ' '.join(cmd), raises=False)
result = []
for res in proc.out.split('\n'):
match = re.match('([^ ]+) is ([^ ]+)$', res.strip())
if match:
result.append(match.group(1))
return result
async def copy(self, *args):
if args[-1].startswith('./'):
args = list(args)
args[-1] = self.home + '/' + args[-1][2:]
args = ['cp', '-rua'] + list(args)
return await self.exec(*args)
async def mount(self, *dirs):
pass
async def mkdir(self, *dirs):
return await self.exec(*['mkdir', '-p'] + list(dirs))
+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
Executable
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env shlax
from shlax.contrib.gitlab import *
PYTEST = 'py.test -svv tests'
test = Script(
Pip('.[test]'),
Run(PYTEST),
)
build = Buildah(
'quay.io/podman/stable',
Packages('python38', 'buildah', 'unzip', 'findutils', 'python3-yaml', upgrade=False),
Async(
# python3.8 on centos with pip dance ...
Run('''
curl -o setuptools.zip https://files.pythonhosted.org/packages/42/3e/2464120172859e5d103e5500315fb5555b1e908c0dacc73d80d35a9480ca/setuptools-45.1.0.zip
unzip setuptools.zip
mkdir -p /usr/local/lib/python3.8/site-packages/
sh -c "cd setuptools-* && python3.8 setup.py install"
easy_install-3.8 pip
echo python3.8 -m pip > /usr/bin/pip
chmod +x /usr/bin/pip
'''),
Copy('shlax/', 'setup.py', '/app'),
),
Pip('/app[full]'),
commit='docker.io/yourlabs/shlax',
workdir='/app',
)
shlax = Container(
build=build,
test=Script(Run('./shlaxfile.py -d test')),
)
gitlabci = GitLabCI(
test=dict(
stage='build',
script='pip install -U --user -e .[test] && ' + PYTEST,
image='yourlabs/python',
),
build=dict(
stage='build',
image='yourlabs/shlax',
script='pip install -U --user -e . && CACHE_DIR=$(pwd)/.cache ./shlaxfile.py -d shlax build push',
cache=dict(paths=['.cache'], key='cache'),
),
pypi=dict(
stage='deploy',
only=['tags'],
image='yourlabs/python',
script='pypi-release',
),
)
+53
View File
@@ -0,0 +1,53 @@
from shlax import *
inner = Run()
other = Run('ls')
middle = Buildah('alpine', inner, other)
outer = Localhost(middle)
middle = outer.actions[0]
other = middle.actions[1]
inner = middle.actions[0]
def test_action_init_args():
assert other.args == ('ls',)
def test_action_parent_autoset():
assert list(outer.actions) == [middle]
assert middle.parent == outer
assert inner.parent == middle
assert other.parent == middle
def test_action_context():
assert outer.context is inner.context
assert middle.context is inner.context
assert middle.context is outer.context
assert other.context is outer.context
def test_action_sibblings():
assert inner.sibblings() == [other]
assert inner.sibblings(lambda s: s.args[0] == 'ls') == [other]
assert inner.sibblings(lambda s: s.args[0] == 'foo') == []
assert inner.sibblings(type='run') == [other]
assert inner.sibblings(args=('ls',)) == [other]
def test_actions_parents():
assert other.parents() == [middle, outer]
assert other.parents(lambda p: p.base == 'alpine') == [middle]
assert inner.parents(type='localhost') == [outer]
assert inner.parents(type='buildah') == [middle]
def test_action_childrens():
assert middle.children() == [inner, other]
assert middle.children(lambda a: a.args[0] == 'ls') == [other]
assert outer.children() == [middle, inner, other]
def test_action_getattr():
assert other.exec == middle.exec
assert other.shargs == middle.shargs
-7
View File
@@ -1,7 +0,0 @@
import shlax
def test_colors():
assert shlax.colors.cyan == '\u001b[38;5;51m'
assert shlax.colors.bcyan == '\u001b[1;38;5;51m'
assert shlax.colors.reset == '\u001b[0m'
+33
View File
@@ -0,0 +1,33 @@
import pytest
import os
from shlax import Image
tests = {
'docker://a.b:1337/re/po:x,y': ('docker', 'a.b:1337', 're/po', 'x,y'),
'docker://a.b/re/po:x,y': ('docker', 'a.b', 're/po', 'x,y'),
'a.b:1337/re/po:x,y': (None, 'a.b:1337', 're/po', 'x,y'),
'a.b/re/po:x,y': (None, 'a.b', 're/po', 'x,y'),
're/po:x,y': (None, None, 're/po', 'x,y'),
're/po': (None, None, 're/po', 'latest'),
'docker://re/po': ('docker', None, 're/po', 'latest'),
'docker://re/po:x,y': ('docker', None, 're/po', 'x,y'),
}
@pytest.mark.parametrize(
'arg,expected', [(k, dict(
backend=v[0], registry=v[1], repository=v[2], tags=v[3].split(',')
)) for k, v in tests.items()]
)
def test_args(arg, expected):
Image.ENV_TAGS = []
im = Image(arg)
for k, v in expected.items():
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']
+24
View File
@@ -0,0 +1,24 @@
import pytest
from shlax import Output
class Write:
def __init__(self):
self.output = ''
def __call__(self, out):
self.output += out.decode('utf8')
@pytest.fixture
def write():
return Write()
def test_output_regexps(write):
output = Output(
regexps={'^(.*)$': '{red}\\1'},
write=write,
flush=lambda: None,
)
output('foo')
assert write.output.strip() == output.colors['red'] + 'foo' + output.colors['reset']
+58 -190
View File
@@ -1,197 +1,65 @@
import pytest import pytest
from unittest.mock import Mock, call
from shlax import Proc from unittest.mock import patch
from shlax import *
from shlax import proc
@pytest.mark.asyncio test_args_params = [
@pytest.mark.parametrize(
'args',
( (
['sh', '-c', 'echo hi'], Localhost(Run('echo hi')),
['echo hi'], [('sh', '-euc', 'echo hi')]
['sh -c "echo hi"'], ),
) (
Localhost(Run('echo hi', user='jimi')),
[('sudo', '-u', 'jimi', 'sh', '-euc', 'echo hi')]
),
(
Localhost(Run('echo hi', user='root')),
[('sudo', 'sh', '-euc', 'echo hi')]
),
(
Ssh('host', Run('echo hi', user='root')),
[('ssh', 'host', 'sudo', 'sh', '-euc', 'echo hi')]
),
(
Buildah('alpine', Run('echo hi')),
[
('buildah', 'from', 'alpine'),
('buildah', 'mount', ''),
('buildah', 'run', '', '--', 'sh', '-euc', 'echo hi'),
('buildah', 'umount', ''),
('buildah', 'rm', ''),
]
),
(
Buildah('alpine', Run('echo hi', user='root')),
[
('buildah', 'from', 'alpine'),
('buildah', 'mount', ''),
('buildah', 'run', '--user', 'root', '', '--', 'sh', '-euc', 'echo hi'),
('buildah', 'umount', ''),
('buildah', 'rm', ''),
]
),
(
Ssh('host', Buildah('alpine', Run('echo hi', user='root'))),
[
('ssh', 'host', 'buildah', 'from', 'alpine'),
('ssh', 'host', 'buildah', 'mount', ''),
('ssh', 'host', 'buildah', 'run', '--user', 'root', '', '--', 'sh', '-euc', 'echo hi'),
('ssh', 'host', 'buildah', 'umount', ''),
('ssh', 'host', 'buildah', 'rm', ''),
]
),
]
@pytest.mark.parametrize(
'script,commands',
test_args_params
) )
async def test_proc(args):
proc = Proc(*args, quiet=True)
assert not proc.waited
assert not proc.started
await proc.wait()
assert proc.waited
assert proc.started
assert proc.out == 'hi'
assert proc.err == ''
assert proc.out_raw == b'hi\n'
assert proc.err_raw == b''
assert proc.rc == 0
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_wait_unbound(): async def test_args(script, commands):
proc = await Proc('echo hi', quiet=True).wait() with Proc.mock():
assert proc.out == 'hi' await script()
assert commands == Proc.test
@pytest.mark.asyncio
async def test_rc_1():
proc = await Proc(
'NON EXISTING COMMAND',
quiet=True,
).wait()
assert proc.rc != 0
assert proc.err == 'sh: line 1: NON: command not found'
@pytest.mark.asyncio
async def test_prefix():
"""
Test output prefixes for when executing multiple commands in parallel.
"""
Proc.prefix_length = 0 # reset
write = Mock()
await Proc(
'echo hi',
write=write,
prefix='test_prefix',
).wait()
await Proc(
'echo hi',
write=write,
prefix='test_prefix_1'
).wait()
await Proc(
'echo hi',
write=write,
prefix='test_prefix',
).wait()
assert write.mock_calls == [
call(
Proc.prefix_colors[0].encode()
+ b'test_prefix '
+ Proc.colors.reset.encode()
+ b'| '
+ Proc.colors.bgray.encode()
+ b'+ sh -euc \'echo hi\''
+ Proc.colors.reset.encode()
+ b'\n'
),
call(
Proc.prefix_colors[0].encode()
+ b'test_prefix '
+ Proc.colors.reset.encode()
+ b'| hi'
+ Proc.colors.reset.encode()
+ b'\n'
),
call(
Proc.prefix_colors[1].encode()
+ b'test_prefix_1 '
+ Proc.colors.reset.encode()
+ b'| '
+ Proc.colors.bgray.encode()
+ b'+ sh -euc \'echo hi\''
+ Proc.colors.reset.encode()
+ b'\n'
),
call(
Proc.prefix_colors[1].encode()
# padding has been added because of output1
+ b'test_prefix_1 '
+ Proc.colors.reset.encode()
+ b'| hi'
+ Proc.colors.reset.encode()
+ b'\n'
),
call(
Proc.prefix_colors[0].encode()
# padding has been added because of output1
+ b' test_prefix '
+ Proc.colors.reset.encode()
+ b'| '
+ Proc.colors.bgray.encode()
+ b'+ sh -euc \'echo hi\''
+ Proc.colors.reset.encode()
+ b'\n'
),
call(
Proc.prefix_colors[0].encode()
# padding has been added because of output1
+ b' test_prefix '
+ Proc.colors.reset.encode()
+ b'| hi'
+ Proc.colors.reset.encode()
+ b'\n'
)
]
@pytest.mark.asyncio
async def test_prefix_multiline():
Proc.prefix_length = 0 # reset
proc = await Proc(
'echo -e "a\nb"',
write=Mock(),
prefix='test_prefix',
).wait()
assert proc.write.mock_calls == [
call(
Proc.prefix_colors[0].encode()
+ b'test_prefix '
+ Proc.colors.reset.encode()
+ b'| '
+ Proc.colors.bgray.encode()
+ b'+ sh -euc \'echo -e "a\\nb"\''
+ Proc.colors.reset.encode()
+ b'\n'
),
call(
Proc.prefix_colors[0].encode()
+ b'test_prefix '
+ Proc.colors.reset.encode()
+ b'| a'
+ Proc.colors.reset.encode()
+ b'\n'
),
call(
Proc.prefix_colors[0].encode()
# padding has been added because of output1
+ b'test_prefix '
+ Proc.colors.reset.encode()
+ b'| b'
+ Proc.colors.reset.encode()
+ b'\n'
),
]
@pytest.mark.asyncio
async def test_highlight():
"""
Test that we can color output with regexps.
"""
proc = await Proc(
'echo hi',
write=Mock(),
regexps={
r'h([\w\d-]+)': 'h{cyan}\\1',
}
).wait()
proc.write.assert_called_with(b'h\x1b[38;5;51mi\x1b[0m\n')
@pytest.mark.asyncio
async def test_highlight_if_not_colored():
"""
Test that coloration does not apply on output that is already colored.
"""
proc = await Proc(
'echo -e h"\\e[31m"i',
write=Mock(),
regexps={
r'h([\w\d-]+)': 'h{cyan}\\1',
}
).wait()
proc.write.assert_called_with(b'h\x1b[31mi\n')