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))