193 lines
5.6 KiB
Python
193 lines
5.6 KiB
Python
from app import app
|
|
from util.streamlayerformat import StreamLayerMerger
|
|
from formats.tarimageformatter import TarImageFormatter
|
|
|
|
import json
|
|
import re
|
|
|
|
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):
|
|
# ACI Format (.tar):
|
|
# manifest - The JSON manifest
|
|
# rootfs - The root file system
|
|
|
|
# Yield the manifest.
|
|
yield self.tar_file('manifest', self._build_manifest(namespace, repository, tag, layer_json,
|
|
synthetic_image_id))
|
|
|
|
# Yield the merged layer dtaa.
|
|
yield self.tar_folder('rootfs')
|
|
|
|
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": str(memory) + 'B'
|
|
}
|
|
|
|
def _isolate_swap(memory):
|
|
return {
|
|
"name": "memory/swap",
|
|
"value": str(memory) + 'B'
|
|
}
|
|
|
|
def _isolate_cpu(cpu):
|
|
return {
|
|
"name": "cpu/shares",
|
|
"value": str(cpu)
|
|
}
|
|
|
|
def _isolate_capabilities(capabilities_set_value):
|
|
capabilities_set = re.split(r'[\s,]', capabilities_set_value)
|
|
return {
|
|
"name": "capabilities/bounding-set",
|
|
"value": ' '.join(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. """
|
|
ports = []
|
|
|
|
for docker_port_definition in docker_config.get('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 _build_volumes(docker_config):
|
|
""" Builds the volumes definitions for the ACI. """
|
|
volumes = []
|
|
names = set()
|
|
|
|
def get_name(docker_volume_path):
|
|
parts = docker_volume_path.split('/')
|
|
name = ''
|
|
|
|
while True:
|
|
name = name + parts[-1]
|
|
parts = parts[0:-1]
|
|
if names.add(name):
|
|
break
|
|
|
|
name = '/' + name
|
|
|
|
return name
|
|
|
|
for docker_volume_path in docker_config.get('volumes', {}):
|
|
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]
|
|
|
|
manifest = {
|
|
"acKind": "ImageManifest",
|
|
"acVersion": "0.2.0",
|
|
"name": '%s/%s/%s/%s' % (hostname, namespace, repository, tag),
|
|
"labels": [
|
|
{
|
|
"name": "version",
|
|
"value": "1.0.0"
|
|
},
|
|
{
|
|
"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 [e.split('=') for e in config.get('Env', []) or []]],
|
|
"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)
|