2014-09-16 04:18:57 +00:00
|
|
|
import logging
|
|
|
|
import json
|
2014-09-19 16:22:54 +00:00
|
|
|
import hashlib
|
2014-09-16 04:18:57 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
from flask import redirect, Blueprint, abort, send_file, make_response
|
2014-09-16 04:18:57 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
from app import app, signer
|
2014-09-16 04:18:57 +00:00
|
|
|
from auth.auth import process_auth
|
|
|
|
from auth.permissions import ReadRepositoryPermission
|
|
|
|
from data import model
|
2014-09-18 21:26:40 +00:00
|
|
|
from data import database
|
2014-10-30 16:49:51 +00:00
|
|
|
from endpoints.trackhelper import track_and_log
|
2014-10-08 20:54:03 +00:00
|
|
|
from storage import Storage
|
2014-09-16 04:18:57 +00:00
|
|
|
|
2014-09-17 02:43:19 +00:00
|
|
|
from util.queuefile import QueueFile
|
|
|
|
from util.queueprocess import QueueProcess
|
2015-01-13 22:46:11 +00:00
|
|
|
from formats.squashed import SquashedDockerImage
|
|
|
|
from formats.aci import ACIImage
|
|
|
|
|
2014-09-16 04:18:57 +00:00
|
|
|
|
2015-02-02 21:38:58 +00:00
|
|
|
# pylint: disable=invalid-name
|
2014-09-16 04:18:57 +00:00
|
|
|
verbs = Blueprint('verbs', __name__)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2015-01-13 22:46:11 +00:00
|
|
|
def _open_stream(formatter, namespace, repository, tag, synthetic_image_id, image_json,
|
|
|
|
image_id_list):
|
2014-10-08 20:54:03 +00:00
|
|
|
store = Storage(app)
|
|
|
|
|
2014-11-06 22:50:48 +00:00
|
|
|
# For performance reasons, we load the full image list here, cache it, then disconnect from
|
|
|
|
# the database.
|
|
|
|
with database.UseThenDisconnect(app.config):
|
2014-11-13 20:13:44 +00:00
|
|
|
image_list = list(model.get_matching_repository_images(namespace, repository, image_id_list))
|
|
|
|
|
|
|
|
image_list.sort(key=lambda image: image_id_list.index(image.docker_image_id))
|
2014-11-05 17:27:38 +00:00
|
|
|
|
2014-09-19 16:22:54 +00:00
|
|
|
def get_next_image():
|
2014-11-06 22:50:48 +00:00
|
|
|
for current_image in image_list:
|
|
|
|
yield current_image
|
2014-09-19 16:22:54 +00:00
|
|
|
|
2014-09-17 02:43:19 +00:00
|
|
|
def get_next_layer():
|
2014-11-06 22:50:48 +00:00
|
|
|
for current_image_entry in image_list:
|
2014-09-17 02:43:19 +00:00
|
|
|
current_image_path = store.image_layer_path(current_image_entry.storage.uuid)
|
|
|
|
current_image_stream = store.stream_read_file(current_image_entry.storage.locations,
|
|
|
|
current_image_path)
|
2014-11-24 21:07:38 +00:00
|
|
|
|
2014-11-06 22:50:48 +00:00
|
|
|
current_image_id = current_image_entry.id
|
2015-02-02 21:38:58 +00:00
|
|
|
logger.debug('Returning image layer %s: %s', current_image_id, current_image_path)
|
2014-09-17 02:43:19 +00:00
|
|
|
yield current_image_stream
|
|
|
|
|
2015-01-13 22:46:11 +00:00
|
|
|
stream = formatter.build_stream(namespace, repository, tag, synthetic_image_id, image_json,
|
2014-09-19 16:22:54 +00:00
|
|
|
get_next_image, get_next_layer)
|
|
|
|
|
2014-09-17 02:43:19 +00:00
|
|
|
return stream.read
|
|
|
|
|
2014-09-18 21:26:40 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
def _sign_sythentic_image(verb, linked_storage_uuid, queue_file):
|
|
|
|
signature = None
|
|
|
|
try:
|
|
|
|
signature = signer.detached_sign(queue_file)
|
|
|
|
except:
|
|
|
|
logger.exception('Exception when signing %s image %s', verb, linked_storage_uuid)
|
|
|
|
return
|
|
|
|
|
2015-02-05 20:00:19 +00:00
|
|
|
# Setup the database (since this is a new process) and then disconnect immediately
|
|
|
|
# once the operation completes.
|
|
|
|
if not queue_file.raised_exception:
|
|
|
|
with database.UseThenDisconnect(app.config):
|
|
|
|
try:
|
|
|
|
derived = model.get_storage_by_uuid(linked_storage_uuid)
|
|
|
|
except model.InvalidImageException:
|
|
|
|
return
|
2015-02-04 20:29:24 +00:00
|
|
|
|
2015-02-05 20:00:19 +00:00
|
|
|
signature_entry = model.find_or_create_storage_signature(derived, signer.name)
|
|
|
|
signature_entry.signature = signature
|
|
|
|
signature_entry.uploading = False
|
|
|
|
signature_entry.save()
|
2015-02-04 20:29:24 +00:00
|
|
|
|
|
|
|
|
2015-01-13 22:46:11 +00:00
|
|
|
def _write_synthetic_image_to_storage(verb, linked_storage_uuid, linked_locations, queue_file):
|
2014-10-08 20:54:03 +00:00
|
|
|
store = Storage(app)
|
2014-10-08 17:43:12 +00:00
|
|
|
|
|
|
|
def handle_exception(ex):
|
2015-01-13 22:46:11 +00:00
|
|
|
logger.debug('Exception when building %s image %s: %s', verb, linked_storage_uuid, ex)
|
2014-11-24 21:07:38 +00:00
|
|
|
|
2014-11-06 22:50:48 +00:00
|
|
|
with database.UseThenDisconnect(app.config):
|
|
|
|
model.delete_derived_storage_by_uuid(linked_storage_uuid)
|
2014-10-08 17:43:12 +00:00
|
|
|
|
|
|
|
queue_file.add_exception_handler(handle_exception)
|
|
|
|
|
2014-09-18 21:26:40 +00:00
|
|
|
image_path = store.image_layer_path(linked_storage_uuid)
|
|
|
|
store.stream_write(linked_locations, image_path, queue_file)
|
2014-09-17 02:43:19 +00:00
|
|
|
queue_file.close()
|
2014-09-16 04:18:57 +00:00
|
|
|
|
2014-10-08 17:43:12 +00:00
|
|
|
if not queue_file.raised_exception:
|
2015-02-05 20:00:19 +00:00
|
|
|
# Setup the database (since this is a new process) and then disconnect immediately
|
|
|
|
# once the operation completes.
|
2014-11-06 22:50:48 +00:00
|
|
|
with database.UseThenDisconnect(app.config):
|
|
|
|
done_uploading = model.get_storage_by_uuid(linked_storage_uuid)
|
|
|
|
done_uploading.uploading = False
|
|
|
|
done_uploading.save()
|
2014-09-18 21:26:40 +00:00
|
|
|
|
|
|
|
|
2015-02-02 21:38:58 +00:00
|
|
|
# pylint: disable=too-many-locals
|
2015-02-04 20:29:24 +00:00
|
|
|
def _verify_repo_verb(store, namespace, repository, tag, verb, checker=None):
|
2014-09-16 04:18:57 +00:00
|
|
|
permission = ReadRepositoryPermission(namespace, repository)
|
2015-02-04 20:29:24 +00:00
|
|
|
|
2015-02-02 21:38:58 +00:00
|
|
|
# pylint: disable=no-member
|
2015-02-04 20:29:24 +00:00
|
|
|
if not permission.can() and not model.repository_is_public(namespace, repository):
|
|
|
|
abort(403)
|
|
|
|
|
|
|
|
# Lookup the requested tag.
|
|
|
|
try:
|
|
|
|
tag_image = model.get_tag_image(namespace, repository, tag)
|
|
|
|
except model.DataModelException:
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
# Lookup the tag's image and storage.
|
|
|
|
repo_image = model.get_repo_image_extended(namespace, repository, tag_image.docker_image_id)
|
|
|
|
if not repo_image:
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
# If there is a data checker, call it first.
|
|
|
|
uuid = repo_image.storage.uuid
|
|
|
|
image_json = None
|
|
|
|
|
|
|
|
if checker is not None:
|
|
|
|
image_json_data = store.get_content(repo_image.storage.locations, store.image_json_path(uuid))
|
|
|
|
image_json = json.loads(image_json_data)
|
|
|
|
|
|
|
|
if not checker(image_json):
|
|
|
|
logger.debug('Check mismatch on %s/%s:%s, verb %s', namespace, repository, tag, verb)
|
2014-09-16 04:18:57 +00:00
|
|
|
abort(404)
|
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
return (repo_image, tag_image, image_json)
|
2014-09-18 21:26:40 +00:00
|
|
|
|
2015-01-13 22:46:11 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# pylint: disable=too-many-locals
|
|
|
|
def _repo_verb_signature(namespace, repository, tag, verb, checker=None, **kwargs):
|
|
|
|
# Verify that the image exists and that we have access to it.
|
|
|
|
store = Storage(app)
|
|
|
|
result = _verify_repo_verb(store, namespace, repository, tag, verb, checker)
|
|
|
|
(repo_image, tag_image, image_json) = result
|
2015-01-13 22:46:11 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Lookup the derived image storage for the verb.
|
|
|
|
derived = model.find_derived_storage(repo_image.storage, verb)
|
|
|
|
if derived is None or derived.uploading:
|
|
|
|
abort(404)
|
2015-01-13 22:46:11 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Check if we have a valid signer configured.
|
|
|
|
if not signer.name:
|
|
|
|
abort(404)
|
2014-10-29 19:42:44 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Lookup the signature for the verb.
|
|
|
|
signature_entry = model.lookup_storage_signature(derived, signer.name)
|
|
|
|
if signature_entry is None:
|
|
|
|
abort(404)
|
2015-01-13 22:46:11 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Return the signature.
|
|
|
|
return make_response(signature_entry.signature)
|
2014-09-17 02:43:19 +00:00
|
|
|
|
2014-11-06 23:00:52 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# pylint: disable=too-many-locals
|
|
|
|
def _repo_verb(namespace, repository, tag, verb, formatter, sign=False, checker=None, **kwargs):
|
|
|
|
# Verify that the image exists and that we have access to it.
|
|
|
|
store = Storage(app)
|
|
|
|
result = _verify_repo_verb(store, namespace, repository, tag, verb, checker)
|
|
|
|
(repo_image, tag_image, image_json) = result
|
2014-09-17 02:43:19 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Log the action.
|
|
|
|
track_and_log('repo_verb', repo_image.repository, tag=tag, verb=verb, **kwargs)
|
2014-09-18 21:26:40 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Lookup/create the derived image storage for the verb.
|
|
|
|
derived = model.find_or_create_derived_storage(repo_image.storage, verb,
|
|
|
|
store.preferred_locations[0])
|
2014-09-19 16:22:54 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
if not derived.uploading:
|
|
|
|
logger.debug('Derived %s image %s exists in storage', verb, derived.uuid)
|
|
|
|
derived_layer_path = store.image_layer_path(derived.uuid)
|
|
|
|
download_url = store.get_direct_download_url(derived.locations, derived_layer_path)
|
|
|
|
if download_url:
|
|
|
|
logger.debug('Redirecting to download URL for derived %s image %s', verb, derived.uuid)
|
|
|
|
return redirect(download_url)
|
2014-09-19 16:22:54 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Close the database handle here for this process before we send the long download.
|
|
|
|
database.close_db_filter(None)
|
2014-10-21 21:40:57 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
logger.debug('Sending cached derived %s image %s', verb, derived.uuid)
|
|
|
|
return send_file(store.stream_read_file(derived.locations, derived_layer_path))
|
2014-09-17 02:43:19 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Load the ancestry for the image.
|
|
|
|
uuid = repo_image.storage.uuid
|
2014-09-18 21:26:40 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
logger.debug('Building and returning derived %s image %s', verb, derived.uuid)
|
|
|
|
ancestry_data = store.get_content(repo_image.storage.locations, store.image_ancestry_path(uuid))
|
|
|
|
full_image_list = json.loads(ancestry_data)
|
2014-09-17 02:43:19 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Load the image's JSON layer.
|
|
|
|
if not image_json:
|
|
|
|
image_json_data = store.get_content(repo_image.storage.locations, store.image_json_path(uuid))
|
|
|
|
image_json = json.loads(image_json_data)
|
2014-09-16 04:18:57 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Calculate a synthetic image ID.
|
|
|
|
synthetic_image_id = hashlib.sha256(tag_image.docker_image_id + ':' + verb).hexdigest()
|
|
|
|
|
|
|
|
def _cleanup():
|
|
|
|
# Close any existing DB connection once the process has exited.
|
2014-11-06 22:50:48 +00:00
|
|
|
database.close_db_filter(None)
|
2014-11-05 17:27:38 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Create a queue process to generate the data. The queue files will read from the process
|
|
|
|
# and send the results to the client and storage.
|
|
|
|
args = (formatter, namespace, repository, tag, synthetic_image_id, image_json, full_image_list)
|
|
|
|
queue_process = QueueProcess(_open_stream,
|
|
|
|
8 * 1024, 10 * 1024 * 1024, # 8K/10M chunk/max
|
|
|
|
args, finished=_cleanup)
|
2014-09-16 04:18:57 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
client_queue_file = QueueFile(queue_process.create_queue(), 'client')
|
|
|
|
storage_queue_file = QueueFile(queue_process.create_queue(), 'storage')
|
2015-01-13 22:46:11 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# If signing is required, add a QueueFile for signing the image as we stream it out.
|
|
|
|
signing_queue_file = None
|
|
|
|
if sign and signer.name:
|
|
|
|
signing_queue_file = QueueFile(queue_process.create_queue(), 'signing')
|
2015-01-13 22:46:11 +00:00
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
# Start building.
|
|
|
|
queue_process.run()
|
|
|
|
|
|
|
|
# Start the storage saving.
|
|
|
|
storage_args = (verb, derived.uuid, derived.locations, storage_queue_file)
|
|
|
|
QueueProcess.run_process(_write_synthetic_image_to_storage, storage_args, finished=_cleanup)
|
|
|
|
|
|
|
|
if sign and signer.name:
|
|
|
|
signing_args = (verb, derived.uuid, signing_queue_file)
|
|
|
|
QueueProcess.run_process(_sign_sythentic_image, signing_args, finished=_cleanup)
|
|
|
|
|
|
|
|
# Close the database handle here for this process before we send the long download.
|
|
|
|
database.close_db_filter(None)
|
|
|
|
|
|
|
|
# Return the client's data.
|
|
|
|
return send_file(client_queue_file)
|
|
|
|
|
|
|
|
|
|
|
|
def os_arch_checker(os, arch):
|
2015-01-13 22:46:11 +00:00
|
|
|
def checker(image_json):
|
|
|
|
# Verify the architecture and os.
|
|
|
|
operating_system = image_json.get('os', 'linux')
|
|
|
|
if operating_system != os:
|
|
|
|
return False
|
|
|
|
|
|
|
|
architecture = image_json.get('architecture', 'amd64')
|
|
|
|
if architecture != arch:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2015-02-04 20:29:24 +00:00
|
|
|
return checker
|
|
|
|
|
|
|
|
|
|
|
|
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/sig/<os>/<arch>/', methods=['GET'])
|
|
|
|
@process_auth
|
|
|
|
# pylint: disable=unused-argument
|
|
|
|
def get_aci_signature(server, namespace, repository, tag, os, arch):
|
|
|
|
return _repo_verb_signature(namespace, repository, tag, 'aci', checker=os_arch_checker(os, arch),
|
|
|
|
os=os, arch=arch)
|
|
|
|
|
|
|
|
|
|
|
|
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci/<os>/<arch>/', methods=['GET'])
|
|
|
|
@process_auth
|
|
|
|
# pylint: disable=unused-argument
|
|
|
|
def get_aci_image(server, namespace, repository, tag, os, arch):
|
2015-01-13 22:46:11 +00:00
|
|
|
return _repo_verb(namespace, repository, tag, 'aci', ACIImage(),
|
2015-02-04 20:29:24 +00:00
|
|
|
sign=True, checker=os_arch_checker(os, arch), os=os, arch=arch)
|
2015-01-13 22:46:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
@verbs.route('/squash/<namespace>/<repository>/<tag>', methods=['GET'])
|
|
|
|
@process_auth
|
|
|
|
def get_squashed_tag(namespace, repository, tag):
|
|
|
|
return _repo_verb(namespace, repository, tag, 'squash', SquashedDockerImage())
|
|
|
|
|