4ec3a6c231
This will ensure that no matter which signature we write for the generated ACI, it is correct for that image.
233 lines
6.7 KiB
Python
233 lines
6.7 KiB
Python
import json
|
|
import re
|
|
import calendar
|
|
|
|
from uuid import uuid4
|
|
|
|
from app import app
|
|
from util.registry.streamlayerformat import StreamLayerMerger
|
|
from formats.tarimageformatter import TarImageFormatter
|
|
|
|
|
|
ACNAME_REGEX = re.compile(r'[^a-z-]+')
|
|
|
|
|
|
class ACIImage(TarImageFormatter):
|
|
""" Image formatter which produces an ACI-compatible TAR.
|
|
"""
|
|
|
|
def stream_generator(self, namespace, repository, tag, synthetic_image_id,
|
|
layer_json, get_image_iterator, get_layer_iterator, get_image_json):
|
|
image_mtime = 0
|
|
created = next(get_image_iterator()).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 = self._build_manifest(namespace, repository, tag, layer_json, 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
|
|
|
|
@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 _get_docker_config_value(docker_config, key, default_value):
|
|
# Try the key itself.
|
|
result = docker_config.get(key)
|
|
if result is not None:
|
|
return result or default_value
|
|
|
|
# The the lowercase version of the key.
|
|
result = docker_config.get(key.lower())
|
|
if result is not None:
|
|
return result or default_value
|
|
|
|
return default_value
|
|
|
|
@staticmethod
|
|
def _build_ports(docker_config):
|
|
""" Builds the ports definitions for the ACI. """
|
|
ports = []
|
|
|
|
for docker_port_definition in ACIImage._get_docker_config_value(docker_config, 'Ports', []):
|
|
# Formats:
|
|
# port/tcp
|
|
# port/udp
|
|
# port
|
|
|
|
protocol = 'tcp'
|
|
port_number = -1
|
|
|
|
if '/' in docker_port_definition:
|
|
(port_number, protocol) = docker_port_definition.split('/')
|
|
else:
|
|
port_number = docker_port_definition
|
|
|
|
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):
|
|
return "volume-%s" % ACIImage._ac_name(docker_volume_path)
|
|
|
|
for docker_volume_path in ACIImage._get_docker_config_value(docker_config, 'Volumes', []):
|
|
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(namespace, repository, tag, docker_layer_data, synthetic_image_id):
|
|
""" Builds an ACI manifest from the docker layer data. """
|
|
|
|
config = docker_layer_data.get('config', {})
|
|
|
|
source_url = "%s://%s/%s/%s:%s" % (app.config['PREFERRED_URL_SCHEME'],
|
|
app.config['SERVER_HOSTNAME'],
|
|
namespace, repository, 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.get('Entrypoint', []) or []
|
|
exec_path = entrypoint + (config.get('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', [])
|
|
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(), repository.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": ACIImage._build_isolators(config),
|
|
"mountPoints": ACIImage._build_volumes(config),
|
|
"ports": ACIImage._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 json.dumps(manifest)
|