9474fb7833
We write the literal received JSON string into the image data, to ensure the checksums computed will always match
203 lines
7.3 KiB
Python
203 lines
7.3 KiB
Python
import logging
|
|
import json
|
|
import uuid
|
|
|
|
from collections import namedtuple
|
|
|
|
from flask import session
|
|
|
|
from data import model
|
|
from data.database import db_transaction
|
|
from data.registry_model import registry_model
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ManifestLayer = namedtuple('ManifestLayer', ['layer_id', 'v1_metadata_string', 'db_id'])
|
|
_BuilderState = namedtuple('_BuilderState', ['builder_id', 'images', 'tags', 'checksums'])
|
|
|
|
_SESSION_KEY = '__manifestbuilder'
|
|
|
|
|
|
def create_manifest_builder(repository_ref, storage):
|
|
""" Creates a new manifest builder for populating manifests under the specified repository
|
|
and returns it. Returns None if the builder could not be constructed.
|
|
"""
|
|
builder_id = str(uuid.uuid4())
|
|
builder = _ManifestBuilder(repository_ref, _BuilderState(builder_id, {}, {}, {}), storage)
|
|
builder._save_to_session()
|
|
return builder
|
|
|
|
|
|
def lookup_manifest_builder(repository_ref, builder_id, storage):
|
|
""" Looks up the manifest builder with the given ID under the specified repository and returns
|
|
it or None if none.
|
|
"""
|
|
builder_state_tuple = session.get(_SESSION_KEY)
|
|
if builder_state_tuple is None:
|
|
return None
|
|
|
|
builder_state = _BuilderState(*builder_state_tuple)
|
|
if builder_state.builder_id != builder_id:
|
|
return None
|
|
|
|
return _ManifestBuilder(repository_ref, builder_state, storage)
|
|
|
|
|
|
class _ManifestBuilder(object):
|
|
""" Helper class which provides an interface for bookkeeping the layers and configuration of
|
|
manifests being constructed.
|
|
"""
|
|
def __init__(self, repository_ref, builder_state, storage):
|
|
self._repository_ref = repository_ref
|
|
self._builder_state = builder_state
|
|
self._storage = storage
|
|
|
|
@property
|
|
def builder_id(self):
|
|
""" Returns the unique ID for this builder. """
|
|
return self._builder_state.builder_id
|
|
|
|
@property
|
|
def committed_tags(self):
|
|
""" Returns the tags committed by this builder, if any. """
|
|
return [registry_model.get_repo_tag(self._repository_ref, tag_name, include_legacy_image=True)
|
|
for tag_name in self._builder_state.tags.keys()]
|
|
|
|
def start_layer(self, layer_id, v1_metadata_string, location_name, calling_user,
|
|
temp_tag_expiration):
|
|
""" Starts a new layer with the given ID to be placed into a manifest. Returns the layer
|
|
started or None if an error occurred.
|
|
"""
|
|
# Ensure the repository still exists.
|
|
repository = model.repository.lookup_repository(self._repository_ref._db_id)
|
|
if repository is None:
|
|
return None
|
|
|
|
namespace_name = repository.namespace_user.username
|
|
repo_name = repository.name
|
|
|
|
try:
|
|
v1_metadata = json.loads(v1_metadata_string)
|
|
except ValueError:
|
|
logger.exception('Exception when trying to parse V1 metadata JSON for layer %s', layer_id)
|
|
return None
|
|
except TypeError:
|
|
logger.exception('Exception when trying to parse V1 metadata JSON for layer %s', layer_id)
|
|
return None
|
|
|
|
# Sanity check that the ID matches the v1 metadata.
|
|
if layer_id != v1_metadata['id']:
|
|
return None
|
|
|
|
# Ensure the parent already exists in the repository.
|
|
parent_id = v1_metadata.get('parent', None)
|
|
parent_image = None
|
|
|
|
if parent_id is not None:
|
|
parent_image = model.image.get_repo_image(namespace_name, repo_name, parent_id)
|
|
if parent_image is None:
|
|
return None
|
|
|
|
# Check to see if this layer already exists in the repository. If so, we can skip the creation.
|
|
existing_image = registry_model.get_legacy_image(self._repository_ref, layer_id)
|
|
if existing_image is not None:
|
|
self._builder_state.images[layer_id] = existing_image.id
|
|
self._save_to_session()
|
|
return ManifestLayer(layer_id, v1_metadata_string, existing_image.id)
|
|
|
|
with db_transaction():
|
|
# Otherwise, create a new legacy image and point a temporary tag at it.
|
|
created = model.image.find_create_or_link_image(layer_id, repository, calling_user, {},
|
|
location_name)
|
|
model.tag.create_temporary_hidden_tag(repository, created, temp_tag_expiration)
|
|
|
|
# Mark the image as uploading.
|
|
created.storage.uploading = True
|
|
created.storage.save()
|
|
|
|
# Save its V1 metadata.
|
|
command_list = v1_metadata.get('container_config', {}).get('Cmd', None)
|
|
command = json.dumps(command_list) if command_list else None
|
|
|
|
model.image.set_image_metadata(layer_id, namespace_name, repo_name,
|
|
v1_metadata.get('created'),
|
|
v1_metadata.get('comment'),
|
|
command, v1_metadata_string,
|
|
parent=parent_image)
|
|
|
|
# Save the changes to the builder.
|
|
self._builder_state.images[layer_id] = created.id
|
|
self._save_to_session()
|
|
|
|
return ManifestLayer(layer_id, v1_metadata_string, created.id)
|
|
|
|
def lookup_layer(self, layer_id):
|
|
""" Returns a layer with the given ID under this builder. If none exists, returns None. """
|
|
if layer_id not in self._builder_state.images:
|
|
return None
|
|
|
|
image = model.image.get_image_by_db_id(self._builder_state.images[layer_id])
|
|
if image is None:
|
|
return None
|
|
|
|
return ManifestLayer(layer_id, image.v1_json_metadata, image.id)
|
|
|
|
def assign_layer_blob(self, layer, blob, computed_checksums):
|
|
""" Assigns a blob to a layer. """
|
|
assert blob
|
|
|
|
repo_image = model.image.get_image_by_db_id(layer.db_id)
|
|
if repo_image is None:
|
|
return None
|
|
|
|
with db_transaction():
|
|
existing_storage = repo_image.storage
|
|
repo_image.storage = blob._db_id
|
|
repo_image.save()
|
|
existing_storage.delete_instance(recursive=True)
|
|
|
|
self._builder_state.checksums[layer.layer_id] = computed_checksums
|
|
self._save_to_session()
|
|
return True
|
|
|
|
def validate_layer_checksum(self, layer, checksum):
|
|
""" Returns whether the checksum for a layer matches that specified.
|
|
"""
|
|
return checksum in self.get_layer_checksums(layer)
|
|
|
|
def get_layer_checksums(self, layer):
|
|
""" Returns the registered defined for the layer, if any. """
|
|
return self._builder_state.checksums.get(layer.layer_id) or []
|
|
|
|
def save_precomputed_checksum(self, layer, checksum):
|
|
""" Saves a precomputed checksum for a layer. """
|
|
checksums = self._builder_state.checksums.get(layer.layer_id) or []
|
|
checksums.append(checksum)
|
|
self._builder_state.checksums[layer.layer_id] = checksums
|
|
self._save_to_session()
|
|
|
|
def commit_tag_and_manifest(self, tag_name, layer):
|
|
""" Commits a new tag + manifest for that tag to the repository with the given name,
|
|
pointing to the given layer.
|
|
"""
|
|
legacy_image = registry_model.get_legacy_image(self._repository_ref, layer.layer_id)
|
|
if legacy_image is None:
|
|
return None
|
|
|
|
tag = registry_model.retarget_tag(self._repository_ref, tag_name, legacy_image, self._storage)
|
|
if tag is None:
|
|
return None
|
|
|
|
self._builder_state.tags[tag_name] = tag._db_id
|
|
self._save_to_session()
|
|
return tag
|
|
|
|
def done(self):
|
|
""" Marks the manifest builder as complete and disposes of any state. This call is optional
|
|
and it is expected manifest builders will eventually time out if unused for an
|
|
extended period of time.
|
|
"""
|
|
session.pop(_SESSION_KEY, None)
|
|
|
|
def _save_to_session(self):
|
|
session[_SESSION_KEY] = self._builder_state
|