2017-11-01 20:15:27 +00:00
|
|
|
from contextlib import contextmanager
|
2017-05-24 23:33:35 +00:00
|
|
|
from functools import partial
|
2017-11-01 20:15:27 +00:00
|
|
|
import shutil
|
2017-05-16 01:54:51 +00:00
|
|
|
import subprocess
|
2017-12-23 00:17:32 +00:00
|
|
|
import re
|
2017-12-25 02:03:17 +00:00
|
|
|
import sys
|
2017-05-16 01:54:51 +00:00
|
|
|
|
2017-12-01 01:14:42 +00:00
|
|
|
from traitlets import Integer
|
2017-11-01 20:15:27 +00:00
|
|
|
|
2017-05-24 21:11:37 +00:00
|
|
|
def execute_cmd(cmd, capture=False, **kwargs):
|
2017-05-16 01:54:51 +00:00
|
|
|
"""
|
2017-05-24 21:11:37 +00:00
|
|
|
Call given command, yielding output line by line if capture=True
|
2017-05-16 01:54:51 +00:00
|
|
|
"""
|
2017-05-24 21:11:37 +00:00
|
|
|
if capture:
|
|
|
|
kwargs['stdout'] = subprocess.PIPE
|
|
|
|
kwargs['stderr'] = subprocess.STDOUT
|
|
|
|
|
|
|
|
proc = subprocess.Popen(cmd, **kwargs)
|
|
|
|
|
|
|
|
if not capture:
|
2017-11-13 07:26:58 +00:00
|
|
|
# not capturing output, let the subprocesses talk directly
|
|
|
|
# to the terminal
|
2017-05-24 21:11:37 +00:00
|
|
|
ret = proc.wait()
|
|
|
|
if ret != 0:
|
|
|
|
raise subprocess.CalledProcessError(ret, cmd)
|
|
|
|
return
|
2017-11-01 20:15:27 +00:00
|
|
|
|
2017-05-24 23:42:25 +00:00
|
|
|
# Capture output for logging.
|
|
|
|
# Each line will be yielded as text.
|
|
|
|
# This should behave the same as .readline(), but splits on `\r` OR `\n`,
|
|
|
|
# not just `\n`.
|
2017-05-24 23:33:35 +00:00
|
|
|
buf = []
|
2017-11-13 07:26:58 +00:00
|
|
|
|
2017-05-24 23:33:35 +00:00
|
|
|
def flush():
|
|
|
|
line = b''.join(buf).decode('utf8', 'replace')
|
|
|
|
buf[:] = []
|
|
|
|
return line
|
2017-11-01 20:15:27 +00:00
|
|
|
|
2017-05-24 23:33:35 +00:00
|
|
|
c_last = ''
|
2017-05-19 07:07:39 +00:00
|
|
|
try:
|
2017-05-24 23:33:35 +00:00
|
|
|
for c in iter(partial(proc.stdout.read, 1), b''):
|
|
|
|
if c_last == b'\r' and buf and c != b'\n':
|
|
|
|
yield flush()
|
|
|
|
buf.append(c)
|
|
|
|
if c == b'\n':
|
|
|
|
yield flush()
|
|
|
|
c_last = c
|
2017-05-19 07:07:39 +00:00
|
|
|
finally:
|
|
|
|
ret = proc.wait()
|
|
|
|
if ret != 0:
|
|
|
|
raise subprocess.CalledProcessError(ret, cmd)
|
2017-11-01 20:15:27 +00:00
|
|
|
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
def maybe_cleanup(path, cleanup=False):
|
|
|
|
yield
|
|
|
|
if cleanup:
|
|
|
|
shutil.rmtree(path, ignore_errors=True)
|
2017-12-01 01:14:42 +00:00
|
|
|
|
|
|
|
|
2017-12-25 02:03:17 +00:00
|
|
|
def validate_and_generate_port_mapping(port_mapping):
|
|
|
|
"""
|
|
|
|
Validate the port mapping list provided as argument and split into as dictionary of key being continer port and the
|
|
|
|
values being None, or 'host_port' or ['interface_ip','host_port']
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
port_mapping (list): List of strings of format 'host_port:container_port'
|
|
|
|
with optional tcp udp values and host network interface
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
List of validated tuples of form ('host_port:container_port') with optional tcp udp values and host network interface
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
Exception on invalid port mapping
|
|
|
|
|
|
|
|
Note:
|
|
|
|
One limitation cannot bind single container_port to multiple host_ports (docker-py supports this but repo2docker
|
|
|
|
does not)
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
Valid port mappings are
|
|
|
|
127.0.0.1:90:900
|
|
|
|
:999 - To match to any host port
|
|
|
|
999:999/tcp - bind 999 host port to 999 tcp container port
|
|
|
|
|
|
|
|
Invalid port mapping
|
|
|
|
127.0.0.1::999 --- even though docker accepts it
|
|
|
|
other invalid ip address combinations
|
|
|
|
"""
|
|
|
|
reg_regex = re.compile('''^(
|
|
|
|
( # or capturing group
|
|
|
|
(?: # start capturing ip address of network interface
|
|
|
|
(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} # first three parts
|
|
|
|
(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) # last part of the ip address
|
|
|
|
:(?:6553[0-5]|655[0-2][0-9]|65[0-4](\d){2}|6[0-4](\d){3}|[1-5](\d){4}|(\d){1,4})
|
|
|
|
)?
|
|
|
|
| # host ip with port or only port
|
|
|
|
(?:6553[0-5]|655[0-2][0-9]|65[0-4](\d){2}|6[0-4](\d){3}|[1-5](\d){4}|(\d){0,4})
|
|
|
|
)
|
|
|
|
:
|
|
|
|
(?:6553[0-5]|655[0-2][0-9]|65[0-4](\d){2}|6[0-4](\d){3}|[1-5](\d){4}|(\d){0,4})
|
|
|
|
(?:/udp|/tcp)?
|
|
|
|
)$''', re.VERBOSE)
|
|
|
|
ports = {}
|
|
|
|
if not port_mapping:
|
|
|
|
return None
|
|
|
|
for p in port_mapping:
|
|
|
|
if reg_regex.match(p) is None:
|
|
|
|
raise Exception('Invalid port mapping ' + str(p))
|
|
|
|
# Do a reverse split twice on the separator :
|
|
|
|
port_host = str(p).rsplit(':', 2)
|
|
|
|
host = None
|
|
|
|
if len(port_host) == 3:
|
|
|
|
# host, optional host_port and container port information given
|
|
|
|
host = port_host[0]
|
|
|
|
host_port = port_host[1]
|
|
|
|
container_port = port_host[2]
|
|
|
|
else:
|
|
|
|
host_port = port_host[0] if len(port_host[0]) > 0 else None
|
|
|
|
container_port = port_host[1]
|
|
|
|
|
|
|
|
|
|
|
|
if host is None:
|
|
|
|
ports[str(container_port)] = host_port
|
|
|
|
else:
|
|
|
|
ports[str(container_port)] = (host, host_port)
|
|
|
|
return ports
|
|
|
|
|
2017-12-23 03:45:16 +00:00
|
|
|
def is_valid_docker_image_name(image_name):
|
|
|
|
"""
|
|
|
|
Function that constructs a regex representing the docker image name and tests it against the given image_name
|
|
|
|
Reference Regex definition in https://github.com/docker/distribution/blob/master/reference/regexp.go
|
|
|
|
|
|
|
|
Args:
|
|
|
|
image_name: string representing a docker image name
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
True if image_name is valid else False
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
'test.Com/name:latest' is a valid tag
|
|
|
|
|
|
|
|
'Test/name:latest' is not a valid tag
|
|
|
|
|
|
|
|
Note:
|
|
|
|
|
|
|
|
This function has a stricter pattern than https://github.com/docker/distribution/blob/master/reference/regexp.go
|
|
|
|
|
|
|
|
This pattern will not allow cases like
|
|
|
|
'TEST.com/name:latest' though docker considers it a valid tag
|
|
|
|
"""
|
|
|
|
reference_regex = re.compile(r"""^ # Anchored at start and end of string
|
|
|
|
|
|
|
|
( # Start capturing name
|
|
|
|
|
|
|
|
(?: # start grouping the optional registry domain name part
|
|
|
|
|
|
|
|
(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9]) # lowercase only '<domain-name-component>'
|
|
|
|
|
|
|
|
(?: # start optional group
|
|
|
|
|
|
|
|
(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+ # multiple repetitions of pattern '.<domain-name-component>'
|
|
|
|
|
|
|
|
)? # end optional grouping part of the '.' separated domain name
|
|
|
|
|
|
|
|
(?::[0-9]+)?/ # '<domain-name>' followed by an optional '<port>' component followed by '/' literal
|
|
|
|
|
|
|
|
)? # end grouping the optional registry domain part
|
|
|
|
|
|
|
|
# start <name-pattern>
|
|
|
|
[a-z0-9]+ # must have a <name-component>
|
|
|
|
(?:
|
|
|
|
(?:(?:[\._]|__|[-]*)[a-z0-9]+)+ # repeat the pattern '<separator><name-component>'
|
|
|
|
)? # optionally have multiple repetitions of the above line
|
|
|
|
# end <name-pattern>
|
|
|
|
|
|
|
|
(?: # start optional name components
|
|
|
|
|
|
|
|
(?: # start multiple repetitions
|
|
|
|
|
|
|
|
/ # separate multiple name components by /
|
|
|
|
# start <name-pattern>
|
|
|
|
[a-z0-9]+ # must have a <name-component>
|
|
|
|
(?:
|
|
|
|
(?:(?:[\._]|__|[-]*)[a-z0-9]+)+ # repeat the pattern '<separator><name-component>'
|
|
|
|
)? # optionally have multiple repetitions of the above line
|
|
|
|
# end <name-pattern>
|
|
|
|
|
|
|
|
)+ # multiple repetitions of the pattern '/<name-component><separator><name-component>'
|
|
|
|
|
|
|
|
)? # optionally have the above group
|
|
|
|
|
|
|
|
) # end capturing name
|
|
|
|
|
|
|
|
(?::([\w][\w.-]{0,127}))? # optional capture <tag-pattern>=':<tag>'
|
|
|
|
(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})? # optionally capture <digest-pattern>='@<digest>'
|
|
|
|
$""",
|
|
|
|
re.VERBOSE)
|
|
|
|
|
|
|
|
return reference_regex.match(image_name) is not None
|
|
|
|
|
|
|
|
|
2017-12-01 01:14:42 +00:00
|
|
|
class ByteSpecification(Integer):
|
|
|
|
"""
|
|
|
|
Allow easily specifying bytes in units of 1024 with suffixes
|
|
|
|
|
|
|
|
Suffixes allowed are:
|
|
|
|
- K -> Kilobyte
|
|
|
|
- M -> Megabyte
|
|
|
|
- G -> Gigabyte
|
|
|
|
- T -> Terabyte
|
|
|
|
|
|
|
|
Stolen from JupyterHub
|
|
|
|
"""
|
|
|
|
|
|
|
|
UNIT_SUFFIXES = {
|
|
|
|
'K': 1024,
|
|
|
|
'M': 1024 * 1024,
|
|
|
|
'G': 1024 * 1024 * 1024,
|
|
|
|
'T': 1024 * 1024 * 1024 * 1024,
|
|
|
|
}
|
|
|
|
|
|
|
|
# Default to allowing None as a value
|
|
|
|
allow_none = True
|
|
|
|
|
|
|
|
def validate(self, obj, value):
|
|
|
|
"""
|
|
|
|
Validate that the passed in value is a valid memory specification
|
|
|
|
|
|
|
|
It could either be a pure int, when it is taken as a byte value.
|
|
|
|
If it has one of the suffixes, it is converted into the appropriate
|
|
|
|
pure byte value.
|
|
|
|
"""
|
|
|
|
if isinstance(value, (int, float)):
|
|
|
|
return int(value)
|
|
|
|
|
|
|
|
try:
|
|
|
|
num = float(value[:-1])
|
|
|
|
except ValueError:
|
|
|
|
raise TraitError('{val} is not a valid memory specification. Must be an int or a string with suffix K, M, G, T'.format(val=value))
|
|
|
|
suffix = value[-1]
|
|
|
|
if suffix not in self.UNIT_SUFFIXES:
|
|
|
|
raise TraitError('{val} is not a valid memory specification. Must be an int or a string with suffix K, M, G, T'.format(val=value))
|
|
|
|
else:
|
|
|
|
return int(float(num) * self.UNIT_SUFFIXES[suffix])
|