ftl/common/tar_to_dockerimage.py (114 lines of code) (raw):

# Copyright 2017 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """This package provides DockerImage for examining docker_build outputs.""" import json from containerregistry.client.v2_2 import docker_digest from containerregistry.client.v2_2 import docker_image from containerregistry.client.v2_2 import docker_http from containerregistry.transform.v2_2 import metadata as v2_2_metadata class FromFSImage(docker_image.DockerImage): """Interface for implementations that interact with Docker images.""" def __init__(self, blob_lst, u_layer_lst, overrides={}): digest_to_blob, digest_to_u_blob = self._gen_digest_to_blob_and_u_blob( blob_lst, u_layer_lst) self._digest_to_blob = digest_to_blob self._digest_to_u_blob = digest_to_u_blob self._diff_id_to_u_layer = self._gen_diff_id_to_u_layer(u_layer_lst) self._overrides = overrides self._manifest = None self._config_file = None def GetFirstBlob(self): for digest in self._digest_to_blob: return self._digest_to_blob[digest] def _gen_digest_to_blob_and_u_blob(self, blob_lst, u_layer_lst): digest_to_blob = {} digest_to_u_blob = {} for blob, u_layer in zip(blob_lst, u_layer_lst): digest = docker_digest.SHA256(blob) digest_to_blob[digest] = blob digest_to_u_blob[digest] = u_layer return digest_to_blob, digest_to_u_blob def _gen_diff_id_to_u_layer(self, u_layer_lst): diff_id_to_u_layer = {} for u_layer in u_layer_lst: diff_id_to_u_layer[docker_digest.SHA256(u_layer)] = u_layer return diff_id_to_u_layer def fs_layers(self): """The ordered collection of filesystem layers that comprise this image.""" manifest = json.loads(self.manifest()) return [x['digest'] for x in reversed(manifest['layers'])] def diff_ids(self): """The ordered list of uncompressed layer hashes (matches fs_layers).""" cfg = json.loads(self.config_file()) return list(reversed(cfg.get('rootfs', {}).get('diff_ids', []))) def config_blob(self): manifest = json.loads(self.manifest()) return manifest['config']['digest'] def blob_set(self): """The unique set of blobs that compose to create the filesystem.""" return set(self.fs_layers() + [self.config_blob()]) def digest(self): """The digest of the manifest.""" return docker_digest.SHA256(self.manifest()) def media_type(self): """The media type of the manifest.""" manifest = json.loads(self.manifest()) return manifest.get('mediaType', docker_http.OCI_MANIFEST_MIME) def manifest(self): """The JSON manifest referenced by the tag/digest. Returns: The raw json manifest """ if self._manifest is None: content = self.config_file().encode('utf-8') self._manifest = json.dumps( { 'schemaVersion': 2, 'mediaType': docker_http.MANIFEST_SCHEMA2_MIME, 'config': { 'mediaType': docker_http.CONFIG_JSON_MIME, 'size': len(content), 'digest': docker_digest.SHA256(content) }, 'layers': [{ 'mediaType': docker_http.LAYER_MIME, 'size': self.blob_size(digest), 'digest': digest } for digest in self._digest_to_blob] }, sort_keys=True) return self._manifest def config_file(self): """The raw blob string of the config file.""" if self._config_file is None: _PROCESSOR_ARCHITECTURE = 'amd64' _OPERATING_SYSTEM = 'linux' entrypoint = self._overrides.pop('Entrypoint', []) env = self._overrides.pop('Env', {}) exposed_ports = self._overrides.pop('ExposedPorts', {}) output = v2_2_metadata.Override( json.loads('{}'), v2_2_metadata.Overrides( author='Bazel', created_by='bazel build ...', layers=[k for k in self._diff_id_to_u_layer], entrypoint=entrypoint, env=env, ports=exposed_ports), architecture=_PROCESSOR_ARCHITECTURE, operating_system=_OPERATING_SYSTEM) output['rootfs'] = { 'diff_ids': [k for k in self._diff_id_to_u_layer] } if len(self._overrides) > 0: output.update(self._overrides) self._config_file = json.dumps(output, sort_keys=True) return self._config_file def blob_size(self, digest): """The byte size of the raw blob.""" return len(self.blob(digest)) def blob(self, digest): """The raw blob of the layer. Args: digest: the 'algo:digest' of the layer being addressed. Returns: The raw blob string of the layer. """ return self._digest_to_blob[digest] def uncompressed_blob(self, digest): """Same as blob() but uncompressed.""" return self._digest_to_u_blob[digest] def _diff_id_to_digest(self, diff_id): for (this_digest, this_diff_id) in zip(self.fs_layers(), self.diff_ids()): if this_diff_id == diff_id: return this_digest raise ValueError('Unmatched "diff_id": "%s"' % diff_id) def layer(self, diff_id): """Like `blob()`, but accepts the `diff_id` instead. The `diff_id` is the name for the digest of the uncompressed layer. Args: diff_id: the 'algo:digest' of the layer being addressed. Returns: The raw compressed blob string of the layer. """ return self.blob(self._diff_id_to_digest(diff_id)) def uncompressed_layer(self, diff_id): """Same as layer() but uncompressed.""" return self._diff_id_to_u_layer[diff_id] def __enter__(self): """Open the image for reading.""" def __exit__(self, unused_type, unused_value, unused_traceback): """Close the image.""" def __str__(self): """A human-readable representation of the image.""" return str(type(self))