This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/image/appc/__init__.py

241 lines
6.8 KiB
Python

import json
import re
import calendar
from uuid import uuid4
from app import app
from util.registry.streamlayerformat import StreamLayerMerger
from util.dict_wrappers import JSONPathDict
from image.common import TarImageFormatter
ACNAME_REGEX = re.compile(r'[^a-z-]+')
class AppCImageFormatter(TarImageFormatter):
"""
Image formatter which produces an tarball according to the AppC specification.
"""
def stream_generator(self, repo_image, tag, synthetic_image_id, get_image_iterator,
get_layer_iterator):
image_mtime = 0
created = next(get_image_iterator()).v1_metadata.created
if created is not None:
image_mtime = calendar.timegm(created.utctimetuple())
# ACI Format (.tar):
# manifest - The JSON manifest
# rootfs - The root file system
# Yield the manifest.
manifest = json.dumps(DockerV1ToACIManifestTranslator.build_manifest(
tag,
repo_image,
synthetic_image_id
))
yield self.tar_file('manifest', manifest, mtime=image_mtime)
# Yield the merged layer dtaa.
yield self.tar_folder('rootfs', mtime=image_mtime)
layer_merger = StreamLayerMerger(get_layer_iterator, path_prefix='rootfs/')
for entry in layer_merger.get_generator():
yield entry
class DockerV1ToACIManifestTranslator(object):
@staticmethod
def _build_isolators(docker_config):
"""
Builds ACI isolator config from the docker config.
"""
def _isolate_memory(memory):
return {
"name": "memory/limit",
"value": {
"request": str(memory) + 'B',
}
}
def _isolate_swap(memory):
return {
"name": "memory/swap",
"value": {
"request": str(memory) + 'B',
}
}
def _isolate_cpu(cpu):
return {
"name": "cpu/shares",
"value": {
"request": str(cpu),
}
}
def _isolate_capabilities(capabilities_set_value):
capabilities_set = re.split(r'[\s,]', capabilities_set_value)
return {
"name": "os/linux/capabilities-retain-set",
"value": {
"set": capabilities_set,
}
}
mappers = {
'Memory': _isolate_memory,
'MemorySwap': _isolate_swap,
'CpuShares': _isolate_cpu,
'Cpuset': _isolate_capabilities
}
isolators = []
for config_key in mappers:
value = docker_config.get(config_key)
if value:
isolators.append(mappers[config_key](value))
return isolators
@staticmethod
def _build_ports(docker_config):
"""
Builds the ports definitions for the ACI.
Formats:
port/tcp
port/udp
port
"""
ports = []
exposed_ports = docker_config['ExposedPorts']
if exposed_ports is not None:
port_list = exposed_ports.keys()
else:
port_list = docker_config['Ports'] or docker_config['ports'] or []
for docker_port in port_list:
protocol = 'tcp'
port_number = -1
if '/' in docker_port:
(port_number, protocol) = docker_port.split('/')
else:
port_number = docker_port
try:
port_number = int(port_number)
ports.append({
"name": "port-%s" % port_number,
"port": port_number,
"protocol": protocol,
})
except ValueError:
pass
return ports
@staticmethod
def _ac_name(value):
sanitized = ACNAME_REGEX.sub('-', value.lower()).strip('-')
if sanitized == '':
return str(uuid4())
return sanitized
@staticmethod
def _build_volumes(docker_config):
""" Builds the volumes definitions for the ACI. """
volumes = []
def get_name(docker_volume_path):
volume_name = DockerV1ToACIManifestTranslator._ac_name(docker_volume_path)
return "volume-%s" % volume_name
volume_list = docker_config['Volumes'] or docker_config['volumes'] or []
for docker_volume_path in volume_list:
if not docker_volume_path:
continue
volumes.append({
"name": get_name(docker_volume_path),
"path": docker_volume_path,
"readOnly": False,
})
return volumes
@staticmethod
def build_manifest(repo_image, tag, synthetic_image_id):
""" Builds an ACI manifest of an existing repository image. """
docker_layer_data = JSONPathDict(repo_image.compat_metadata)
config = docker_layer_data['config'] or {}
namespace = repo_image.repository.namespace_name
repo_name = repo_image.repository.name
source_url = "%s://%s/%s/%s:%s" % (app.config['PREFERRED_URL_SCHEME'],
app.config['SERVER_HOSTNAME'],
namespace, repo_name, tag)
# ACI requires that the execution command be absolutely referenced. Therefore, if we find
# a relative command, we give it as an argument to /bin/sh to resolve and execute for us.
entrypoint = config['Entrypoint'] or []
exec_path = entrypoint + (config['Cmd'] or [])
if exec_path and not exec_path[0].startswith('/'):
exec_path = ['/bin/sh', '-c', '""%s""' % ' '.join(exec_path)]
# TODO(jschorr): ACI doesn't support : in the name, so remove any ports.
hostname = app.config['SERVER_HOSTNAME']
hostname = hostname.split(':', 1)[0]
# Calculate the environment variables.
docker_env_vars = config.get('Env', []) or []
env_vars = []
for var in docker_env_vars:
pieces = var.split('=')
if len(pieces) != 2:
continue
env_vars.append(pieces)
manifest = {
"acKind": "ImageManifest",
"acVersion": "0.6.1",
"name": '%s/%s/%s' % (hostname.lower(), namespace.lower(), repo_name.lower()),
"labels": [
{
"name": "version",
"value": tag,
},
{
"name": "arch",
"value": docker_layer_data.get('architecture', 'amd64')
},
{
"name": "os",
"value": docker_layer_data.get('os', 'linux')
}
],
"app": {
"exec": exec_path,
# Below, `or 'root'` is required to replace empty string from Dockerfiles.
"user": config.get('User', '') or 'root',
"group": config.get('Group', '') or 'root',
"eventHandlers": [],
"workingDirectory": config.get('WorkingDir', '') or '/',
"environment": [{"name": key, "value": value} for (key, value) in env_vars],
"isolators": DockerV1ToACIManifestTranslator._build_isolators(config),
"mountPoints": DockerV1ToACIManifestTranslator._build_volumes(config),
"ports": DockerV1ToACIManifestTranslator._build_ports(config),
"annotations": [
{"name": "created", "value": docker_layer_data.get('created', '')},
{"name": "homepage", "value": source_url},
{"name": "quay.io/derived-image", "value": synthetic_image_id},
]
},
}
return manifest