Skip to content

Commit

Permalink
ccon-oci: Add 'delete' and 'event'
Browse files Browse the repository at this point in the history
Delete is currently used as an internal, post-stop hook in 'create',
but you could call it directly if the 'create' process dies before
running the hook.

Event is a wrapper around the inotify-pubsub commands [1].  The
listener will currently continue listening after container deletion in
--monitor mode (or if it hasn't seen a matching event yet).  Callers
who care about that should watch for 'stopped' and kill the watcher by
hand.

[1]: https://github.com/wking/inotify-pubsub
  • Loading branch information
wking committed Feb 26, 2018
1 parent 4a9bf78 commit 6b1a12a
Showing 1 changed file with 102 additions and 13 deletions.
115 changes: 102 additions & 13 deletions ccon-oci
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@

import argparse
import base64
import datetime
import inspect
import json
import os
import resource
import shutil
import subprocess
import sys
import textwrap
import unicodedata
Expand Down Expand Up @@ -413,23 +416,28 @@ def convert_config(basedir, config, runtime, state):
if hooks:
raise NotImplementedError(
'unparsed hooks: {}'.format(hooks))
if 'hooks' not in c:
c['hooks'] = {}
if 'pre-start' not in c['hooks']:
c['hooks']['pre-start'] = []
if 'post-stop' not in c['hooks']:
c['hooks']['post-stop'] = []
if rlimits:
if 'hooks' not in c:
c['hooks'] = {}
if 'pre-start' not in c['hooks']:
c['hooks']['pre-start'] = []
c['hooks']['pre-start'].append({
'args': [sys.argv[0], 'rlimits', json.dumps(rlimits)],
})
hostname = config.pop('hostname', None)
if hostname:
if 'hooks' not in c:
c['hooks'] = {}
if 'pre-start' not in c['hooks']:
c['hooks']['pre-start'] = []
c['hooks']['pre-start'].append({
'args': [sys.argv[0], 'hostname', hostname],
})
c['hooks']['pre-start'].append({
'args': [sys.argv[0], 'event', '--register', 'created', state['id']],
})
c['hooks']['post-stop'].extend([
{'args': [sys.argv[0], 'event', '--register', 'stopped', state['id']]},
{'args': [sys.argv[0], 'delete', state['id']]},
])
if 'linux' in config and not config['linux']:
config.pop('linux') # remove empty dictionary
if runtime and 'linux' in runtime and not runtime['linux']:
Expand Down Expand Up @@ -483,14 +491,18 @@ def _load_config(bundle='.', id=None):
}


def _socket_path(id):
id_nfc = unicodedata.normalize('NFC', id)
def _container_dir(container_id):
id_nfc = unicodedata.normalize('NFC', container_id)
id_utf8 = id_nfc.encode('UTF-8')
id_base64 = base64.b64encode(id_utf8, altchars=b'-_')
tmp_dir = os.environ.get('TMPDIR', '/tmp')
return os.path.join(tmp_dir, 'ccon-oci', id_base64.decode('UTF-8'))


def _socket_path(container_id):
return os.path.join(_container_dir(container_id=container_id), 'sock')


def create(bundle='.', id=None, verbose=False):
"""Create a container from a bundle directory.
"""
Expand All @@ -500,7 +512,7 @@ def create(bundle='.', id=None, verbose=False):
config_state = _load_config(bundle=bundle, id=id)
config = config_state['config']
state = config_state['state']
socket = _socket_path(state['id'])
socket = _socket_path(container_id=state['id'])
socket_dir = os.path.dirname(socket)
os.makedirs(socket_dir, exist_ok=True)
args.extend(['--socket', socket])
Expand All @@ -515,11 +527,24 @@ def create(bundle='.', id=None, verbose=False):
def start(id, verbose=False):
"""Start the user-specified code.
"""
socket = _socket_path(id)
socket = _socket_path(container_id=id)
args = ['ccon-cli', '--socket', socket, '--config-string', '']
if verbose:
args.append('--verbose')
os.execvp(args[0], args)
subprocess.check_call(args)
event(id=id, register='started')


def _failed_to_remove(func, path, exc_info):
sys.stderr.write('failed to remove {!r}\n'.format(path))


def delete(id):
"""Helper for releasing persistent container resources.
"""
sys.stdin.close()
container_dir = _container_dir(container_id=id)
shutil.rmtree(container_dir, onerror=_failed_to_remove)


def config(bundle='.', id=None):
Expand Down Expand Up @@ -554,6 +579,38 @@ def hook(state=None, hook=None, read_pid=False):
os.execvpe(hook['args'][0], hook['args'], env)


def event(id, register=None, monitor=False, event=None, historic=False):
"""Helper for registering container-status events.
"""
stdin_fileno = sys.stdin.fileno()
sys.stdin.close()
event_dir = os.path.join(_container_dir(container_id=id), 'event')
if register:
event = {
'type': register,
'id': id,
'timestamp': datetime.datetime.utcnow().isoformat() + 'Z'
}
# os.pipe opens with O_CLOEXEC since Python 3.4
read, write = os.pipe2(0)
os.write(write, json.dumps(event).encode('UTF-8'))
os.write(write, '\n'.encode('UTF-8'))
os.close(write)
os.dup2(read, stdin_fileno)
os.close(read)
args = ['inotify-pub', event_dir, register]
else: # subscribe
args = ['inotify-sub']
if monitor:
args.append('--monitor')
if event:
args.extend(['--event', event])
if historic:
args.append('--historic')
args.append(event_dir)
os.execvp(args[0], args)


def hostname(hostname):
"""Helper for setting the container hostname.
"""
Expand Down Expand Up @@ -611,6 +668,8 @@ def main():
('start', start),
('config', config),
('hook', hook),
('delete', delete),
('event', event),
('hostname', hostname),
('rlimits', rlimits),
]:
Expand All @@ -634,6 +693,10 @@ def main():
subparser.add_argument(
'id', metavar='ID',
help='Set the container ID to start.')
if command == 'delete':
subparser.add_argument(
'id', metavar='ID',
help='Set the container ID to delete.')
if command == 'hook':
subparser.add_argument(
'--state', metavar='JSON', required=True,
Expand All @@ -644,6 +707,32 @@ def main():
subparser.add_argument(
'--hook', metavar='JSON', required=True,
help='Ccon hook JSON for the user-specified hook.')
if command == 'event':
subparser.add_argument(
'id', metavar='ID',
help='Set the container ID to connect to.')
subparser.add_argument(
'--register', metavar='EVENT',
help='Register a container-status event (for internal use).')
subparser.add_argument(
'-m', '--monitor', action='store_true',
help=(
'Instead of exiting after receiving a single event, '
'execute indefinitely. The default behavior is to exit '
'after the first event occurs.'
))
subparser.add_argument(
'-e', '--event', metavar='EVENT',
help=(
'Listen for specific events only. If ommitted, all '
'events are listened for.'
))
subparser.add_argument(
'-H', '--historic', action='store_true',
help=(
'Replay historic events. If ommitted, only new events '
'are output.'
))
if command == 'hostname':
subparser.add_argument(
'hostname', metavar='HOSTNAME',
Expand Down

0 comments on commit 6b1a12a

Please sign in to comment.