2016-06-06 21:55:10 +03:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
|
|
|
|
from __future__ import absolute_import, print_function, unicode_literals
|
|
|
|
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import tarfile
|
2016-11-11 05:05:52 +03:00
|
|
|
from io import BytesIO
|
2016-06-06 21:55:10 +03:00
|
|
|
|
|
|
|
from taskgraph.util import docker
|
2017-02-17 06:04:48 +03:00
|
|
|
from taskgraph.util.taskcluster import (
|
|
|
|
find_task_id,
|
|
|
|
get_artifact_url,
|
Bug 1432390 - Use zstandard and requests modules instead of spawning curl | zstd in docker.load_image. r=dustin
The zstd command we spawn, if available at all, might be the wrong
version: zstd changed its stream format in an incompatible way at some
point, and the version shipped in e.g. Ubuntu 16.04 uses the old format,
while the version taskcluster relies on uses the new format.
Relying on gps's zstandard library allows to ensure we use the right
version. Another advantage is that we can trivially pip install it in a
virtualenv if it isn't available on the system running the command.
If we're ridding ourselves of the subprocess spawning for zstd, we might
as well cover curl as well. Especially considering the error handling
when subprocesses are involved is not trivial, such that the current
error handling code is actually broken and leads to dead-lock
conditions, when, for example, curl is still waiting for the python side
to read data, but the python side is not reading data anymore because
an exception was thrown in the tar reading loop.
--HG--
extra : rebase_source : 054c37cfaa68bf475b37545ebaa99144584b93d4
2018-01-24 05:18:13 +03:00
|
|
|
get_session,
|
2017-02-17 06:04:48 +03:00
|
|
|
)
|
2017-11-10 03:08:41 +03:00
|
|
|
from taskgraph.util.cached_tasks import cached_index_path
|
2017-02-17 05:56:12 +03:00
|
|
|
from . import GECKO
|
2016-06-06 21:55:10 +03:00
|
|
|
|
|
|
|
|
2016-11-11 05:05:52 +03:00
|
|
|
def load_image_by_name(image_name, tag=None):
|
2016-10-20 15:55:34 +03:00
|
|
|
context_path = os.path.join(GECKO, 'taskcluster', 'docker', image_name)
|
2016-07-22 22:46:06 +03:00
|
|
|
context_hash = docker.generate_context_hash(GECKO, context_path, image_name)
|
2016-06-06 21:55:10 +03:00
|
|
|
|
2017-11-10 03:08:41 +03:00
|
|
|
index_path = cached_index_path(
|
2017-11-10 03:15:29 +03:00
|
|
|
trust_domain='gecko',
|
|
|
|
level=3,
|
|
|
|
cache_type='docker-images.v1',
|
|
|
|
cache_name=image_name,
|
|
|
|
digest=context_hash,
|
|
|
|
)
|
2017-02-17 06:04:48 +03:00
|
|
|
task_id = find_task_id(index_path)
|
2016-06-06 21:55:10 +03:00
|
|
|
|
2017-02-17 06:04:48 +03:00
|
|
|
return load_image_by_task_id(task_id, tag)
|
2016-06-06 21:55:10 +03:00
|
|
|
|
|
|
|
|
2016-11-11 05:05:52 +03:00
|
|
|
def load_image_by_task_id(task_id, tag=None):
|
2017-02-17 06:04:48 +03:00
|
|
|
artifact_url = get_artifact_url(task_id, 'public/image.tar.zst')
|
2016-11-11 05:05:52 +03:00
|
|
|
result = load_image(artifact_url, tag)
|
|
|
|
print("Found docker image: {}:{}".format(result['image'], result['tag']))
|
|
|
|
if tag:
|
|
|
|
print("Re-tagged as: {}".format(tag))
|
|
|
|
else:
|
|
|
|
tag = '{}:{}'.format(result['image'], result['tag'])
|
|
|
|
print("Try: docker run -ti --rm {} bash".format(tag))
|
|
|
|
return True
|
2016-07-29 22:45:25 +03:00
|
|
|
|
|
|
|
|
2017-12-24 01:51:29 +03:00
|
|
|
def build_context(name, outputFile, args=None):
|
2016-11-07 22:26:27 +03:00
|
|
|
"""Build a context.tar for image with specified name.
|
|
|
|
"""
|
|
|
|
if not name:
|
|
|
|
raise ValueError('must provide a Docker image name')
|
|
|
|
if not outputFile:
|
|
|
|
raise ValueError('must provide a outputFile')
|
|
|
|
|
2017-12-24 01:58:08 +03:00
|
|
|
image_dir = docker.image_path(name)
|
2016-11-07 22:26:27 +03:00
|
|
|
if not os.path.isdir(image_dir):
|
|
|
|
raise Exception('image directory does not exist: %s' % image_dir)
|
|
|
|
|
2017-12-24 01:51:29 +03:00
|
|
|
docker.create_context_tar(GECKO, image_dir, outputFile, "", args)
|
2016-11-07 22:26:27 +03:00
|
|
|
|
|
|
|
|
2018-01-25 07:36:47 +03:00
|
|
|
def build_image(name, tag, args=None):
|
2016-07-29 22:45:25 +03:00
|
|
|
"""Build a Docker image of specified name.
|
|
|
|
|
|
|
|
Output from image building process will be printed to stdout.
|
|
|
|
"""
|
2016-07-29 22:59:46 +03:00
|
|
|
if not name:
|
|
|
|
raise ValueError('must provide a Docker image name')
|
|
|
|
|
2017-12-24 01:58:08 +03:00
|
|
|
image_dir = docker.image_path(name)
|
2016-07-29 22:59:46 +03:00
|
|
|
if not os.path.isdir(image_dir):
|
|
|
|
raise Exception('image directory does not exist: %s' % image_dir)
|
|
|
|
|
2018-01-25 07:36:47 +03:00
|
|
|
tag = tag or docker.docker_image(name, by_tag=True)
|
2016-07-29 23:06:10 +03:00
|
|
|
|
2018-01-24 09:55:31 +03:00
|
|
|
buf = BytesIO()
|
|
|
|
docker.stream_context_tar(GECKO, image_dir, buf, '', args)
|
|
|
|
docker.post_to_docker(buf.getvalue(), '/build', nocache=1, t=tag)
|
2016-07-29 23:06:10 +03:00
|
|
|
|
2016-07-29 23:22:06 +03:00
|
|
|
print('Successfully built %s and tagged with %s' % (name, tag))
|
|
|
|
|
2016-07-29 23:06:10 +03:00
|
|
|
if tag.endswith(':latest'):
|
|
|
|
print('*' * 50)
|
|
|
|
print('WARNING: no VERSION file found in image directory.')
|
|
|
|
print('Image is not suitable for deploying/pushing.')
|
|
|
|
print('Create an image suitable for deploying/pushing by creating')
|
|
|
|
print('a VERSION file in the image directory.')
|
|
|
|
print('*' * 50)
|
2016-11-11 05:05:52 +03:00
|
|
|
|
|
|
|
|
|
|
|
def load_image(url, imageName=None, imageTag=None):
|
|
|
|
"""
|
|
|
|
Load docker image from URL as imageName:tag, if no imageName or tag is given
|
|
|
|
it will use whatever is inside the zstd compressed tarball.
|
|
|
|
|
|
|
|
Returns an object with properties 'image', 'tag' and 'layer'.
|
|
|
|
"""
|
2018-05-10 06:13:28 +03:00
|
|
|
import zstandard as zstd
|
Bug 1432390 - Use zstandard and requests modules instead of spawning curl | zstd in docker.load_image. r=dustin
The zstd command we spawn, if available at all, might be the wrong
version: zstd changed its stream format in an incompatible way at some
point, and the version shipped in e.g. Ubuntu 16.04 uses the old format,
while the version taskcluster relies on uses the new format.
Relying on gps's zstandard library allows to ensure we use the right
version. Another advantage is that we can trivially pip install it in a
virtualenv if it isn't available on the system running the command.
If we're ridding ourselves of the subprocess spawning for zstd, we might
as well cover curl as well. Especially considering the error handling
when subprocesses are involved is not trivial, such that the current
error handling code is actually broken and leads to dead-lock
conditions, when, for example, curl is still waiting for the python side
to read data, but the python side is not reading data anymore because
an exception was thrown in the tar reading loop.
--HG--
extra : rebase_source : 054c37cfaa68bf475b37545ebaa99144584b93d4
2018-01-24 05:18:13 +03:00
|
|
|
|
2016-11-11 05:05:52 +03:00
|
|
|
# If imageName is given and we don't have an imageTag
|
|
|
|
# we parse out the imageTag from imageName, or default it to 'latest'
|
|
|
|
# if no imageName and no imageTag is given, 'repositories' won't be rewritten
|
|
|
|
if imageName and not imageTag:
|
|
|
|
if ':' in imageName:
|
|
|
|
imageName, imageTag = imageName.split(':', 1)
|
|
|
|
else:
|
|
|
|
imageTag = 'latest'
|
|
|
|
|
2018-01-24 08:25:09 +03:00
|
|
|
info = {}
|
|
|
|
|
|
|
|
def download_and_modify_image():
|
|
|
|
# This function downloads and edits the downloaded tar file on the fly.
|
|
|
|
# It emits chunked buffers of the editted tar file, as a generator.
|
Bug 1432390 - Use zstandard and requests modules instead of spawning curl | zstd in docker.load_image. r=dustin
The zstd command we spawn, if available at all, might be the wrong
version: zstd changed its stream format in an incompatible way at some
point, and the version shipped in e.g. Ubuntu 16.04 uses the old format,
while the version taskcluster relies on uses the new format.
Relying on gps's zstandard library allows to ensure we use the right
version. Another advantage is that we can trivially pip install it in a
virtualenv if it isn't available on the system running the command.
If we're ridding ourselves of the subprocess spawning for zstd, we might
as well cover curl as well. Especially considering the error handling
when subprocesses are involved is not trivial, such that the current
error handling code is actually broken and leads to dead-lock
conditions, when, for example, curl is still waiting for the python side
to read data, but the python side is not reading data anymore because
an exception was thrown in the tar reading loop.
--HG--
extra : rebase_source : 054c37cfaa68bf475b37545ebaa99144584b93d4
2018-01-24 05:18:13 +03:00
|
|
|
print("Downloading from {}".format(url))
|
|
|
|
# get_session() gets us a requests.Session set to retry several times.
|
|
|
|
req = get_session().get(url, stream=True)
|
|
|
|
req.raise_for_status()
|
2016-11-11 05:05:52 +03:00
|
|
|
|
2018-05-11 20:10:41 +03:00
|
|
|
with zstd.ZstdDecompressor().stream_reader(req.raw) as ifh:
|
|
|
|
|
|
|
|
tarin = tarfile.open(
|
|
|
|
mode='r|',
|
|
|
|
fileobj=ifh,
|
|
|
|
bufsize=zstd.DECOMPRESSION_RECOMMENDED_OUTPUT_SIZE)
|
|
|
|
|
|
|
|
# Stream through each member of the downloaded tar file individually.
|
|
|
|
for member in tarin:
|
|
|
|
# Non-file members only need a tar header. Emit one.
|
|
|
|
if not member.isfile():
|
|
|
|
yield member.tobuf(tarfile.GNU_FORMAT)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Open stream reader for the member
|
|
|
|
reader = tarin.extractfile(member)
|
|
|
|
|
|
|
|
# If member is `repositories`, we parse and possibly rewrite the
|
|
|
|
# image tags.
|
|
|
|
if member.name == 'repositories':
|
|
|
|
# Read and parse repositories
|
|
|
|
repos = json.loads(reader.read())
|
|
|
|
reader.close()
|
|
|
|
|
|
|
|
# If there is more than one image or tag, we can't handle it
|
|
|
|
# here.
|
|
|
|
if len(repos.keys()) > 1:
|
|
|
|
raise Exception('file contains more than one image')
|
|
|
|
info['image'] = image = repos.keys()[0]
|
|
|
|
if len(repos[image].keys()) > 1:
|
|
|
|
raise Exception('file contains more than one tag')
|
|
|
|
info['tag'] = tag = repos[image].keys()[0]
|
|
|
|
info['layer'] = layer = repos[image][tag]
|
|
|
|
|
|
|
|
# Rewrite the repositories file
|
|
|
|
data = json.dumps({imageName or image: {imageTag or tag: layer}})
|
|
|
|
reader = BytesIO(data)
|
|
|
|
member.size = len(data)
|
|
|
|
|
|
|
|
# Emit the tar header for this member.
|
|
|
|
yield member.tobuf(tarfile.GNU_FORMAT)
|
|
|
|
# Then emit its content.
|
|
|
|
remaining = member.size
|
|
|
|
while remaining:
|
|
|
|
length = min(remaining,
|
|
|
|
zstd.DECOMPRESSION_RECOMMENDED_OUTPUT_SIZE)
|
|
|
|
buf = reader.read(length)
|
|
|
|
remaining -= len(buf)
|
|
|
|
yield buf
|
|
|
|
# Pad to fill a 512 bytes block, per tar format.
|
|
|
|
remainder = member.size % 512
|
|
|
|
if remainder:
|
|
|
|
yield '\0' * (512 - remainder)
|
2016-11-11 05:05:52 +03:00
|
|
|
|
|
|
|
reader.close()
|
|
|
|
|
2018-01-24 08:25:09 +03:00
|
|
|
docker.post_to_docker(download_and_modify_image(), '/images/load', quiet=0)
|
2016-11-11 05:05:52 +03:00
|
|
|
|
|
|
|
# Check that we found a repositories file
|
2018-01-24 08:25:09 +03:00
|
|
|
if not info.get('image') or not info.get('tag') or not info.get('layer'):
|
2016-11-11 05:05:52 +03:00
|
|
|
raise Exception('No repositories file found!')
|
|
|
|
|
2018-01-24 08:25:09 +03:00
|
|
|
return info
|