from app import app from util.streamlayerformat import StreamLayerMerger from formats.tarimageformatter import TarImageFormatter import json import re # pylint: disable=bad-continuation class ACIImage(TarImageFormatter): """ Image formatter which produces an ACI-compatible TAR. """ # pylint: disable=too-many-arguments 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 '/', # TODO(jschorr): Use the commented version once rocket has upgraded to 0.3.0. #"environment": [{"name": key, "value": value} # for (key, value) in [e.split('=') for e in config.get('Env')]], "environment": {key: value for (key, value) in [e.split('=') for e in config.get('Env')]}, "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)