* call podman exec instead of using API: - build does not support progress - run looks like to lack equivalent dockerpty support * config: use full uri as short names are not supported on all OSes (like debian 11)
231 lines
7.1 KiB
Python
Executable File
231 lines
7.1 KiB
Python
Executable File
import sys
|
|
import os
|
|
import argparse
|
|
import getpass
|
|
import pathlib
|
|
import xdg.BaseDirectory
|
|
import yaml
|
|
import datetime
|
|
import pytz
|
|
import subprocess
|
|
from pprint import pprint
|
|
|
|
|
|
def find_config_file(user_conf):
|
|
conf_list = [
|
|
'runon.yaml',
|
|
'.runon.yaml',
|
|
os.path.join(xdg.BaseDirectory.xdg_config_home, 'runon', 'runon.yaml'),
|
|
os.path.join(xdg.BaseDirectory.xdg_config_home, 'runon', 'runon.default.yaml')
|
|
]
|
|
if user_conf:
|
|
conf_list = [ user_conf ]
|
|
for conf in conf_list:
|
|
if os.path.exists(conf):
|
|
return conf
|
|
return None
|
|
|
|
|
|
def read_yaml(conf_file):
|
|
try:
|
|
with open(conf_file, 'r') as file:
|
|
conf = yaml.safe_load(file)
|
|
conf['stamp'] = datetime.datetime.fromtimestamp(os.path.getmtime(conf_file), tz=pytz.UTC)
|
|
return conf
|
|
except yaml.YAMLError as e:
|
|
print(f'ERROR: bad configuration file:')
|
|
print(e)
|
|
sys.exit(1)
|
|
return conf
|
|
|
|
def list_osnames(conf_file):
|
|
conf = read_yaml(conf_file)
|
|
osnames = []
|
|
for key in conf:
|
|
if (type(conf[key]) is dict) and conf[key].get('image'):
|
|
osnames.append(key)
|
|
return osnames
|
|
|
|
|
|
def load_config(conf_file, osname):
|
|
user_vars = {
|
|
'osname': osname,
|
|
'user': getpass.getuser(),
|
|
'uid': os.getuid(),
|
|
'home': pathlib.Path.home(),
|
|
}
|
|
conf = read_yaml(conf_file)
|
|
osconf = conf.get(osname)
|
|
if not osconf:
|
|
print(f"ERROR: cannot find configuration for distribution {osname}")
|
|
sys.exit(1)
|
|
|
|
osconf['stamp'] = conf.get('stamp')
|
|
osconf['osname'] = osname
|
|
for k in [ 'dockerfile', 'packages', 'environment', 'binds' ]:
|
|
if osconf.get(k):
|
|
osconf[k] = [ s.format(**user_vars) for s in osconf[k]]
|
|
return osconf
|
|
|
|
|
|
def make_osname_link(binpath, osname):
|
|
link = os.path.join(os.path.dirname(binpath), osname)
|
|
try:
|
|
os.symlink('runon', link)
|
|
except FileExistsError:
|
|
pass
|
|
|
|
|
|
def build_image(conf, update, verbose):
|
|
osname = conf.get('osname')
|
|
image_name = 'runon-{}'.format(osname)
|
|
cache_dir = os.path.join(xdg.BaseDirectory.xdg_cache_home, 'runon')
|
|
cache_file = os.path.join(cache_dir, image_name)
|
|
|
|
if not update and os.path.exists(cache_file):
|
|
ts_image = datetime.datetime.fromtimestamp(os.path.getmtime(cache_file), tz=pytz.UTC)
|
|
ts_conf = conf.get('stamp')
|
|
if verbose:
|
|
print('config: {}'.format(ts_conf))
|
|
print('image: {}'.format(ts_image))
|
|
if ts_image and ts_image > ts_conf:
|
|
if verbose:
|
|
print('image: {} up-to-date'.format(image_name))
|
|
return image_name
|
|
|
|
image = conf.get('image')
|
|
dockerfile = conf.get('dockerfile')
|
|
pkginstall = conf.get('pkginstall')
|
|
packages = conf.get('packages')
|
|
if not dockerfile:
|
|
dockerfile = []
|
|
if image:
|
|
dockerfile.insert(0, 'FROM {}'.format(image))
|
|
if packages:
|
|
for p in packages:
|
|
dockerfile.append(pkginstall.format(p))
|
|
|
|
if verbose:
|
|
print('Dockerfile:')
|
|
for l in dockerfile:
|
|
print(' * {}'.format(l))
|
|
print('Building image {} ...'.format(image_name))
|
|
cmd = ['podman', 'build']
|
|
if update:
|
|
cmd.append('--no-cache')
|
|
cmd += ['-t', image_name, '-']
|
|
ret = subprocess.run(cmd,
|
|
input='\n'.join(dockerfile).encode('utf-8'),
|
|
stderr=subprocess.STDOUT, check=True)
|
|
|
|
if not os.path.exists(cache_dir):
|
|
os.mkdir(cache_dir)
|
|
with open(cache_file, 'w') as file:
|
|
if verbose:
|
|
print('cache: {}'.format(cache_file))
|
|
file.write('')
|
|
|
|
return image_name
|
|
|
|
|
|
def run_image(name, conf, command, verbose):
|
|
volumes = {
|
|
}
|
|
environment = {}
|
|
if conf.get('binds'):
|
|
for mnt in conf['binds']:
|
|
mnt = mnt.split(':')
|
|
if mnt[-1] in ['ro','rw']:
|
|
mode = mnt[-1]
|
|
del mnt[-1]
|
|
else:
|
|
mode = 'rw'
|
|
mnt = mnt[:2]
|
|
bind = mnt[-1]
|
|
vol = mnt[0]
|
|
if os.path.exists(vol):
|
|
volumes[vol] = { 'bind': bind, 'mode': mode }
|
|
if conf.get('environment'):
|
|
for v in conf['environment']:
|
|
e = v.split('=')
|
|
if len(e) == 1:
|
|
e.append(os.getenv(e[0]))
|
|
environment[e[0]] = e[1]
|
|
|
|
cmd = ['podman', 'run', '--rm', '--interactive', '--tty', '--userns=keep-id', '--net=host' ]
|
|
for e in environment:
|
|
cmd += [ '-e', '{}={}'.format(e, environment[e]) ]
|
|
for v in volumes:
|
|
cmd += [ '-v', ':'.join([v, volumes[v]['bind'], volumes[v]['mode']]) ]
|
|
cmd += [ '--workdir', os.getcwd() ]
|
|
cmd += [ name ]
|
|
if command:
|
|
cmd += command
|
|
if verbose:
|
|
print('executing: {}\n'.format(' '.join(cmd)))
|
|
ret = subprocess.run(cmd)
|
|
return ret.returncode
|
|
|
|
|
|
def main():
|
|
osname = None
|
|
run_name = os.path.basename(os.sys.argv[0])
|
|
if run_name == "runon":
|
|
pass
|
|
elif run_name.startswith("runon_"):
|
|
osname = run_name[len("runon_"):]
|
|
else:
|
|
osname = run_name
|
|
|
|
parser = argparse.ArgumentParser()
|
|
if osname:
|
|
parser.description = 'run commands on "{}" distribution'.format(osname)
|
|
else:
|
|
parser.description = 'run commands on any distribution'
|
|
parser.add_argument('osname',
|
|
help = 'distribution name to run on, '
|
|
'"list" to dump all available distributions, '
|
|
'"edit" to open the current config file in a text editor.')
|
|
parser.epilog = '(c) 2021 Gilles Grandou <gilles@grandou.net>'
|
|
|
|
parser.add_argument('-v', '--verbose', action='store_true',
|
|
help='verbose output')
|
|
parser.add_argument('-c', '--config',
|
|
help='specify config file')
|
|
parser.add_argument('-u', '--update', action='store_true',
|
|
help='force image update')
|
|
parser.add_argument('-l', '--link', action='store_true',
|
|
help='create a symlink to call "osname" as a shortcut to "runon osname"')
|
|
parser.add_argument('command', nargs='*', default=None,
|
|
help = 'command to execute')
|
|
|
|
args = parser.parse_args()
|
|
if osname:
|
|
args.osname = osname
|
|
|
|
conf_file = find_config_file(args.config)
|
|
if not conf_file:
|
|
print('ERROR: config file not found')
|
|
sys.exit(1)
|
|
|
|
if args.osname == 'list':
|
|
osnames = list_osnames(conf_file)
|
|
print('Available distributions:')
|
|
for o in sorted(osnames):
|
|
print(' {}'.format(o))
|
|
print()
|
|
return 0
|
|
elif args.osname == 'edit':
|
|
cmd = [ 'xdg-open', conf_file ]
|
|
ret = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL);
|
|
return 0
|
|
|
|
if args.link:
|
|
make_osname_link(sys.argv[0], args.osname)
|
|
|
|
conf = load_config(conf_file, args.osname)
|
|
image_name = build_image(conf, args.update, args.verbose)
|
|
ret = run_image(image_name, conf, args.command, args.verbose)
|
|
return ret
|
|
|