kopia lustrzana https://github.com/jupyterhub/repo2docker
Merge pull request #213 from willingc/docstring-conda
Add docstrings and minor style fixes for application files and JuliaBuildPackpull/271/head
commit
05671879f5
|
@ -1,9 +1,11 @@
|
|||
from .app import Repo2Docker
|
||||
|
||||
|
||||
def main():
|
||||
f = Repo2Docker()
|
||||
f.initialize()
|
||||
f.start()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -7,43 +7,46 @@ Usage:
|
|||
|
||||
python -m repo2docker https://github.com/you/your-repo
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import argparse
|
||||
import tempfile
|
||||
from pythonjsonlogger import jsonlogger
|
||||
import escapism
|
||||
import json
|
||||
import sys
|
||||
import logging
|
||||
import os
|
||||
import pwd
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
from traitlets.config import Application
|
||||
from traitlets import Unicode, List, default, Any, Dict, Int
|
||||
import docker
|
||||
from docker.utils import kwargs_from_env
|
||||
from docker.errors import DockerException
|
||||
import escapism
|
||||
from pythonjsonlogger import jsonlogger
|
||||
|
||||
import subprocess
|
||||
from traitlets import Any, Dict, Int, List, Unicode, default
|
||||
from traitlets.config import Application
|
||||
|
||||
from . import __version__
|
||||
from .buildpacks import (
|
||||
PythonBuildPack, DockerBuildPack, LegacyBinderDockerBuildPack,
|
||||
CondaBuildPack, JuliaBuildPack, Python2BuildPack, BaseImage,
|
||||
RBuildPack
|
||||
)
|
||||
from .utils import execute_cmd, ByteSpecification, maybe_cleanup, is_valid_docker_image_name, validate_and_generate_port_mapping
|
||||
from . import __version__
|
||||
|
||||
from .utils import (
|
||||
execute_cmd, ByteSpecification, maybe_cleanup, is_valid_docker_image_name,
|
||||
validate_and_generate_port_mapping
|
||||
)
|
||||
|
||||
|
||||
class Repo2Docker(Application):
|
||||
"""An application for converting git repositories to docker images"""
|
||||
name = 'jupyter-repo2docker'
|
||||
version = __version__
|
||||
description = __doc__
|
||||
|
||||
@default('log_level')
|
||||
def _default_log_level(self):
|
||||
"""The application's default log level"""
|
||||
return logging.INFO
|
||||
|
||||
git_workdir = Unicode(
|
||||
|
@ -51,7 +54,7 @@ class Repo2Docker(Application):
|
|||
config=True,
|
||||
allow_none=True,
|
||||
help="""
|
||||
Working directory to check out git repositories to.
|
||||
Working directory to use for check out of git repositories.
|
||||
|
||||
The default is to use the system's temporary directory. Should be
|
||||
somewhere ephemeral, such as /tmp.
|
||||
|
@ -70,7 +73,7 @@ class Repo2Docker(Application):
|
|||
],
|
||||
config=True,
|
||||
help="""
|
||||
Ordered list of BuildPacks to try to use to build a git repository.
|
||||
Ordered list of BuildPacks to try when building a git repository.
|
||||
"""
|
||||
)
|
||||
|
||||
|
@ -78,7 +81,7 @@ class Repo2Docker(Application):
|
|||
PythonBuildPack,
|
||||
config=True,
|
||||
help="""
|
||||
The build pack to use when no buildpacks are found
|
||||
The default build pack to use when no other buildpacks are found.
|
||||
"""
|
||||
)
|
||||
|
||||
|
@ -97,13 +100,15 @@ class Repo2Docker(Application):
|
|||
help="""
|
||||
Volumes to mount when running the container.
|
||||
|
||||
Only used when running, not during build!
|
||||
Only used when running, not during build process!
|
||||
|
||||
Should be a key value pair, with the key being the volume source &
|
||||
value being the destination. Both can be relative - sources are
|
||||
resolved relative to the current working directory on the host,
|
||||
destination is resolved relative to the working directory of the image -
|
||||
($HOME by default)
|
||||
Use a key-value pair, with the key being the volume source &
|
||||
value being the destination volume.
|
||||
|
||||
Both source and destination can be relative. Source is resolved
|
||||
relative to the current working directory on the host, and
|
||||
destination is resolved relative to the working directory of the
|
||||
image - ($HOME by default)
|
||||
""",
|
||||
config=True
|
||||
)
|
||||
|
@ -133,11 +138,11 @@ class Repo2Docker(Application):
|
|||
help="""
|
||||
Username of the user to create inside the built image.
|
||||
|
||||
Should be a username that is not currently used by anything in the image,
|
||||
and should conform to the restrictions on user names for Linux.
|
||||
Should be a username that is not currently used by anything in the
|
||||
image, and should conform to the restrictions on user names for Linux.
|
||||
|
||||
Defaults to username of currently running user, since that is the most
|
||||
common case when running r2d manually.
|
||||
common case when running repo2docker manually.
|
||||
""",
|
||||
config=True
|
||||
)
|
||||
|
@ -160,6 +165,7 @@ class Repo2Docker(Application):
|
|||
)
|
||||
|
||||
def fetch(self, url, ref, checkout_path):
|
||||
"""Check out a repo using url and ref to the checkout_path location"""
|
||||
try:
|
||||
for line in execute_cmd(['git', 'clone', '--recursive', url, checkout_path],
|
||||
capture=self.json_logs):
|
||||
|
@ -182,7 +188,11 @@ class Repo2Docker(Application):
|
|||
|
||||
def validate_image_name(self, image_name):
|
||||
"""
|
||||
Validate image_name read by argparse contains only lowercase characters
|
||||
Validate image_name read by argparse
|
||||
|
||||
Note: Container names must start with an alphanumeric character and
|
||||
can then use _ . or - in addition to alphanumeric.
|
||||
[a-zA-Z0-9][a-zA-Z0-9_.-]+
|
||||
|
||||
Args:
|
||||
image_name (string): argument read by the argument parser
|
||||
|
@ -191,15 +201,21 @@ class Repo2Docker(Application):
|
|||
unmodified image_name
|
||||
|
||||
Raises:
|
||||
ArgumentTypeError: if image_name contains characters that are not lowercase
|
||||
ArgumentTypeError: if image_name contains characters that do not
|
||||
meet the logic that container names must start
|
||||
with an alphanumeric character and can then
|
||||
use _ . or - in addition to alphanumeric.
|
||||
[a-zA-Z0-9][a-zA-Z0-9_.-]+
|
||||
"""
|
||||
|
||||
if not is_valid_docker_image_name(image_name):
|
||||
msg = "%r is not a valid docker image name. Image name can contain only lowercase characters." % image_name
|
||||
msg = ("%r is not a valid docker image name. Image name"
|
||||
"must start with an alphanumeric character and"
|
||||
"can then use _ . or - in addition to alphanumeric." % image_name)
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
return image_name
|
||||
|
||||
def get_argparser(self):
|
||||
"""Get arguments that may be used by repo2docker"""
|
||||
argparser = argparse.ArgumentParser()
|
||||
argparser.add_argument(
|
||||
'--config',
|
||||
|
@ -268,7 +284,8 @@ class Repo2Docker(Application):
|
|||
'--publish', '-p',
|
||||
dest='ports',
|
||||
action='append',
|
||||
help='Specify port mappings for the image. Needs a command to run in the container.'
|
||||
help=('Specify port mappings for the image. Needs a command to '
|
||||
'run in the container.')
|
||||
)
|
||||
|
||||
argparser.add_argument(
|
||||
|
@ -336,6 +353,7 @@ class Repo2Docker(Application):
|
|||
extra=dict(phase='failed'))
|
||||
|
||||
def initialize(self, argv=None):
|
||||
"""Init repo2docker configuration before start"""
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
args = self.get_argparser().parse_args(argv)
|
||||
|
@ -401,10 +419,12 @@ class Repo2Docker(Application):
|
|||
self.run = False
|
||||
self.push = False
|
||||
|
||||
# check against self.run and not args.run as self.run is false on --no-build
|
||||
# check against self.run and not args.run as self.run is false on
|
||||
# --no-build
|
||||
if args.volumes and not self.run:
|
||||
# Can't mount if we aren't running
|
||||
print("To Mount volumes with -v, you also need to run the container")
|
||||
print('To Mount volumes with -v, you also need to run the '
|
||||
'container')
|
||||
sys.exit(1)
|
||||
|
||||
for v in args.volumes:
|
||||
|
@ -414,15 +434,18 @@ class Repo2Docker(Application):
|
|||
self.run_cmd = args.cmd
|
||||
|
||||
if args.all_ports and not self.run:
|
||||
print('To publish user defined port mappings, the container must also be run')
|
||||
print('To publish user defined port mappings, the container must '
|
||||
'also be run')
|
||||
sys.exit(1)
|
||||
|
||||
if args.ports and not self.run:
|
||||
print('To publish user defined port mappings, the container must also be run')
|
||||
print('To publish user defined port mappings, the container must '
|
||||
'also be run')
|
||||
sys.exit(1)
|
||||
|
||||
if args.ports and not self.run_cmd:
|
||||
print('To publish user defined port mapping, user must specify the command to run in the container')
|
||||
print('To publish user defined port mapping, user must specify '
|
||||
'the command to run in the container')
|
||||
sys.exit(1)
|
||||
|
||||
self.ports = validate_and_generate_port_mapping(args.ports)
|
||||
|
@ -437,12 +460,14 @@ class Repo2Docker(Application):
|
|||
self.build_memory_limit = args.build_memory_limit
|
||||
|
||||
if args.environment and not self.run:
|
||||
print("To specify environment variables, you also need to run the container")
|
||||
print('To specify environment variables, you also need to run '
|
||||
'the container')
|
||||
sys.exit(1)
|
||||
|
||||
self.environment = args.environment
|
||||
|
||||
def push_image(self):
|
||||
"""Push docker image to registry"""
|
||||
client = docker.APIClient(version='auto', **kwargs_from_env())
|
||||
# Build a progress setup for each layer, and only emit per-layer
|
||||
# info every 1.5s
|
||||
|
@ -465,6 +490,7 @@ class Repo2Docker(Application):
|
|||
last_emit_time = time.time()
|
||||
|
||||
def run_image(self):
|
||||
"""Run docker container from built image"""
|
||||
client = docker.from_env(version='auto')
|
||||
if not self.run_cmd:
|
||||
port = str(self._get_free_port())
|
||||
|
@ -533,8 +559,8 @@ class Repo2Docker(Application):
|
|||
s.close()
|
||||
return port
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start execution of repo2docker"""
|
||||
# Check if r2d can connect to docker daemon
|
||||
if self.build:
|
||||
try:
|
||||
|
@ -542,7 +568,8 @@ class Repo2Docker(Application):
|
|||
**kwargs_from_env())
|
||||
del client
|
||||
except DockerException as e:
|
||||
print("Docker client initialization error. Check if docker is running on the host.")
|
||||
print("Docker client initialization error. Check if docker is"
|
||||
" running on the host.")
|
||||
print(e)
|
||||
if self.log_level == logging.DEBUG:
|
||||
raise e
|
||||
|
@ -560,11 +587,7 @@ class Repo2Docker(Application):
|
|||
# cleanup if things go wrong
|
||||
with maybe_cleanup(checkout_path, self.cleanup_checkout):
|
||||
if self.repo_type == 'remote':
|
||||
self.fetch(
|
||||
self.repo,
|
||||
self.ref,
|
||||
checkout_path
|
||||
)
|
||||
self.fetch(self.repo, self.ref, checkout_path)
|
||||
|
||||
os.chdir(checkout_path)
|
||||
|
||||
|
@ -588,7 +611,9 @@ class Repo2Docker(Application):
|
|||
}
|
||||
self.log.info('Using %s builder\n', bp.__class__.__name__,
|
||||
extra=dict(phase='building'))
|
||||
for l in picked_buildpack.build(self.output_image_spec, self.build_memory_limit, build_args):
|
||||
|
||||
for l in picked_buildpack.build(self.output_image_spec,
|
||||
self.build_memory_limit, build_args):
|
||||
if 'stream' in l:
|
||||
self.log.info(l['stream'],
|
||||
extra=dict(phase='building'))
|
||||
|
|
|
@ -1,18 +1,36 @@
|
|||
"""
|
||||
Generates a variety of Dockerfiles based on an input matrix
|
||||
"""
|
||||
"""Generates a Dockerfile based on an input matrix for Julia"""
|
||||
import os
|
||||
from .conda import CondaBuildPack
|
||||
|
||||
|
||||
class JuliaBuildPack(CondaBuildPack):
|
||||
"""
|
||||
Julia + Conda build pack
|
||||
Julia build pack which uses conda.
|
||||
|
||||
The Julia build pack always uses the parent, `CondaBuildPack`,
|
||||
since Julia does not work with Python virtual environments.
|
||||
See https://github.com/JuliaPy/PyCall.jl/issues/410
|
||||
|
||||
Julia does not work with Virtual Envs,
|
||||
see https://github.com/JuliaPy/PyCall.jl/issues/410
|
||||
"""
|
||||
def get_env(self):
|
||||
"""Get additional environment settings for Julia and Jupyter
|
||||
|
||||
Returns:
|
||||
an ordered list of environment setting tuples
|
||||
|
||||
The tuples contain a string of the environment variable name and
|
||||
a string of the environment setting:
|
||||
- `JULIA_PATH`: base path where all Julia Binaries and libraries
|
||||
will be installed
|
||||
- `JULIA_HOME`: path where all Julia Binaries will be installed
|
||||
- `JULIA_PKGDIR`: path where all Julia libraries will be installed
|
||||
- `JULIA_VERSION`: default version of julia to be installed
|
||||
- `JUPYTER`: environment variable required by IJulia to point to
|
||||
the `jupyter` executable
|
||||
|
||||
For example, a tuple may be `('JULIA_VERSION', '0.6.0')`.
|
||||
|
||||
"""
|
||||
return super().get_env() + [
|
||||
('JULIA_PATH', '${APP_BASE}/julia'),
|
||||
('JULIA_HOME', '${JULIA_PATH}/bin'),
|
||||
|
@ -22,9 +40,26 @@ class JuliaBuildPack(CondaBuildPack):
|
|||
]
|
||||
|
||||
def get_path(self):
|
||||
return super().get_path() + ['${JULIA_PATH}/bin']
|
||||
"""Adds path to Julia binaries to user's PATH.
|
||||
|
||||
Returns:
|
||||
an ordered list of path strings. The path to the Julia
|
||||
executable is added to the list.
|
||||
|
||||
"""
|
||||
return super().get_path() + ['${JULIA_HOME}']
|
||||
|
||||
def get_build_scripts(self):
|
||||
"""
|
||||
Return series of build-steps common to "ALL" Julia repositories
|
||||
|
||||
All scripts found here should be independent of contents of a
|
||||
particular repository.
|
||||
|
||||
This creates a directory with permissions for installing julia packages
|
||||
(from get_assemble_scripts).
|
||||
|
||||
"""
|
||||
return super().get_build_scripts() + [
|
||||
(
|
||||
"root",
|
||||
|
@ -52,11 +87,19 @@ class JuliaBuildPack(CondaBuildPack):
|
|||
]
|
||||
|
||||
def get_assemble_scripts(self):
|
||||
"""
|
||||
Return series of build-steps specific to "this" Julia repository
|
||||
|
||||
Precompile all Julia libraries found in the repository's REQUIRE
|
||||
file. The parent, CondaBuildPack, will add the build steps for
|
||||
any needed Python packages found in environment.yml.
|
||||
|
||||
"""
|
||||
require = self.binder_path('REQUIRE')
|
||||
return super().get_assemble_scripts() + [(
|
||||
"${NB_USER}",
|
||||
# Pre-compile all libraries if they've opted into it. `using {libraryname}` does the
|
||||
# right thing
|
||||
# Pre-compile all libraries if they've opted into it.
|
||||
# `using {libraryname}` does the right thing
|
||||
r"""
|
||||
cat "%(require)s" >> ${JULIA_PKGDIR}/v0.6/REQUIRE && \
|
||||
julia -e ' \
|
||||
|
@ -69,4 +112,14 @@ class JuliaBuildPack(CondaBuildPack):
|
|||
)]
|
||||
|
||||
def detect(self):
|
||||
return os.path.exists(self.binder_path('REQUIRE')) and super().detect()
|
||||
"""
|
||||
Check if current repo should be built with the Julia Build pack
|
||||
|
||||
super().detect() is not called in this function - it would return
|
||||
false unless an `environment.yml` is present and we do not want to
|
||||
require the presence of a `environment.yml` to use Julia.
|
||||
|
||||
Instead we just check if the path to `REQUIRE` exists
|
||||
|
||||
"""
|
||||
return os.path.exists(self.binder_path('REQUIRE'))
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
|
||||
from traitlets import Integer
|
||||
|
||||
|
||||
def execute_cmd(cmd, capture=False, **kwargs):
|
||||
"""
|
||||
Call given command, yielding output line by line if capture=True
|
||||
|
@ -18,8 +19,7 @@ def execute_cmd(cmd, capture=False, **kwargs):
|
|||
proc = subprocess.Popen(cmd, **kwargs)
|
||||
|
||||
if not capture:
|
||||
# not capturing output, let the subprocesses talk directly
|
||||
# to the terminal
|
||||
# not capturing output, let subprocesses talk directly to terminal
|
||||
ret = proc.wait()
|
||||
if ret != 0:
|
||||
raise subprocess.CalledProcessError(ret, cmd)
|
||||
|
@ -32,6 +32,7 @@ def execute_cmd(cmd, capture=False, **kwargs):
|
|||
buf = []
|
||||
|
||||
def flush():
|
||||
"""Flush next line of the buffer"""
|
||||
line = b''.join(buf).decode('utf8', 'replace')
|
||||
buf[:] = []
|
||||
return line
|
||||
|
@ -53,6 +54,7 @@ def execute_cmd(cmd, capture=False, **kwargs):
|
|||
|
||||
@contextmanager
|
||||
def maybe_cleanup(path, cleanup=False):
|
||||
"""Delete the directory at passed path if cleanup flag is True."""
|
||||
yield
|
||||
if cleanup:
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
@ -60,48 +62,57 @@ def maybe_cleanup(path, cleanup=False):
|
|||
|
||||
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']
|
||||
Validate the port mapping list and return a list of validated tuples.
|
||||
|
||||
Each entry in the passed port mapping list will be converted to a
|
||||
tuple with a containing a string with the format 'key:value' with the
|
||||
`key` being the container's port and the
|
||||
`value` being `None`, `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
|
||||
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
|
||||
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)
|
||||
One limitation of repo2docker is it cannot bind a
|
||||
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
|
||||
Valid port mappings are:
|
||||
- `127.0.0.1:90:900`
|
||||
- `:999` (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
|
||||
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)
|
||||
reg_regex = re.compile(r"""
|
||||
^(
|
||||
( # 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
|
||||
|
@ -120,84 +131,90 @@ def validate_and_generate_port_mapping(port_mapping):
|
|||
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
|
||||
|
||||
|
||||
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
|
||||
Determine if image name is valid for docker using strict pattern.
|
||||
|
||||
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
|
||||
The definition uses a stricter pattern than the docker default.
|
||||
|
||||
Args:
|
||||
image_name: string representing a docker image name
|
||||
|
||||
Returns:
|
||||
True if image_name is valid else False
|
||||
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:
|
||||
Note:
|
||||
This function has a stricter pattern than
|
||||
https://github.com/docker/distribution/blob/master/reference/regexp.go
|
||||
|
||||
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
|
||||
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
|
||||
reference_regex = re.compile(r"""
|
||||
^ # Anchored at start and end of string
|
||||
|
||||
( # Start capturing name
|
||||
( # Start capturing name
|
||||
|
||||
(?: # start grouping the optional registry domain name part
|
||||
(?: # 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>'
|
||||
(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9]) # lowercase only '<domain-name-component>'
|
||||
|
||||
(?: # start optional group
|
||||
(?: # 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>'
|
||||
# multiple repetitions of pattern '.<domain-name-component>'
|
||||
(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+
|
||||
|
||||
)? # end optional grouping part of the '.' separated domain name
|
||||
)? # end optional grouping part of the '.' separated domain name
|
||||
|
||||
(?::[0-9]+)?/ # '<domain-name>' followed by an optional '<port>' component followed by '/' literal
|
||||
(?::[0-9]+)?/ # '<domain-name>' followed by an optional '<port>' component followed by '/' literal
|
||||
|
||||
)? # end grouping the optional registry domain part
|
||||
)? # 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 <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 optional name components
|
||||
|
||||
(?: # start multiple repetitions
|
||||
(?: # 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>
|
||||
/ # 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>'
|
||||
)+ # multiple repetitions of the pattern '/<name-component><separator><name-component>'
|
||||
|
||||
)? # optionally have the above group
|
||||
)? # optionally have the above group
|
||||
|
||||
) # end capturing name
|
||||
) # 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)
|
||||
(?::([\w][\w.-]{0,127}))? # optional capture <tag-pattern>=':<tag>'
|
||||
# optionally capture <digest-pattern>='@<digest>'
|
||||
(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?
|
||||
$
|
||||
""", re.VERBOSE)
|
||||
|
||||
return reference_regex.match(image_name) is not None
|
||||
|
||||
|
@ -227,11 +244,11 @@ class ByteSpecification(Integer):
|
|||
|
||||
def validate(self, obj, value):
|
||||
"""
|
||||
Validate that the passed in value is a valid memory specification
|
||||
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 value is a pure int, it is taken as a byte value.
|
||||
If value has one of the unit suffixes, it is converted into the
|
||||
appropriate pure byte value.
|
||||
"""
|
||||
if isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
|
@ -239,9 +256,17 @@ class ByteSpecification(Integer):
|
|||
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))
|
||||
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))
|
||||
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])
|
||||
|
|
|
@ -34,8 +34,11 @@ def test_image_name_fail():
|
|||
builddir = os.path.dirname(__file__)
|
||||
image_name = 'Test/Invalid_name:1.0.0'
|
||||
args_list = ['--no-run', '--no-build', '--image-name', image_name]
|
||||
expected = "error: argument --image-name: %r is not a valid docker image name. " \
|
||||
"Image name can contain only lowercase characters." % image_name
|
||||
expected = (
|
||||
"%r is not a valid docker image name. Image name"
|
||||
"must start with an alphanumeric character and"
|
||||
"can then use _ . or - in addition to alphanumeric." % image_name
|
||||
)
|
||||
assert not validate_arguments(builddir, args_list, expected)
|
||||
|
||||
|
||||
|
@ -47,9 +50,11 @@ def test_image_name_underscore_fail():
|
|||
builddir = os.path.dirname(__file__)
|
||||
image_name = '_test/invalid_name:1.0.0'
|
||||
args_list = ['--no-run', '--no-build', '--image-name', image_name]
|
||||
expected = "error: argument --image-name: %r is not a valid docker image name. " \
|
||||
"Image name can contain only lowercase characters." % image_name
|
||||
|
||||
expected = (
|
||||
"%r is not a valid docker image name. Image name"
|
||||
"must start with an alphanumeric character and"
|
||||
"can then use _ . or - in addition to alphanumeric." % image_name
|
||||
)
|
||||
assert not validate_arguments(builddir, args_list, expected)
|
||||
|
||||
|
||||
|
@ -61,9 +66,11 @@ def test_image_name_double_dot_fail():
|
|||
builddir = os.path.dirname(__file__)
|
||||
image_name = 'test..com/invalid_name:1.0.0'
|
||||
args_list = ['--no-run', '--no-build', '--image-name', image_name]
|
||||
expected = "error: argument --image-name: %r is not a valid docker image name. " \
|
||||
"Image name can contain only lowercase characters." % image_name
|
||||
|
||||
expected = (
|
||||
"%r is not a valid docker image name. Image name"
|
||||
"must start with an alphanumeric character and"
|
||||
"can then use _ . or - in addition to alphanumeric." % image_name
|
||||
)
|
||||
assert not validate_arguments(builddir, args_list, expected)
|
||||
|
||||
|
||||
|
@ -76,8 +83,11 @@ def test_image_name_valid_restircted_registry_domain_name_fail():
|
|||
builddir = os.path.dirname(__file__)
|
||||
image_name = 'Test.com/valid_name:1.0.0'
|
||||
args_list = ['--no-run', '--no-build', '--image-name', image_name]
|
||||
expected = "error: argument --image-name: %r is not a valid docker image name. " \
|
||||
"Image name can contain only lowercase characters." % image_name
|
||||
expected = (
|
||||
"%r is not a valid docker image name. Image name"
|
||||
"must start with an alphanumeric character and"
|
||||
"can then use _ . or - in addition to alphanumeric." % image_name
|
||||
)
|
||||
|
||||
assert not validate_arguments(builddir, args_list, expected)
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue