1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
11 from taskgraph
.generator
import load_tasks_for_kind
12 from taskgraph
.parameters
import Parameters
13 from taskgraph
.util
.taskcluster
import get_artifact_url
, get_session
15 from gecko_taskgraph
.optimize
.strategies
import IndexSearch
16 from gecko_taskgraph
.util
import docker
21 def get_image_digest(image_name
):
23 level
=os
.environ
.get("MOZ_SCM_LEVEL", "3"),
26 tasks
= load_tasks_for_kind(params
, "docker-image")
27 task
= tasks
[f
"docker-image-{image_name}"]
28 return task
.attributes
["cached_task"]["digest"]
31 def load_image_by_name(image_name
, tag
=None):
32 params
= {"level": os
.environ
.get("MOZ_SCM_LEVEL", "3")}
33 tasks
= load_tasks_for_kind(params
, "docker-image")
34 task
= tasks
[f
"docker-image-{image_name}"]
36 task_id
= IndexSearch().should_replace_task(
37 task
, {}, deadline
, task
.optimization
.get("index-search", [])
40 if task_id
in (True, False):
42 "Could not find artifacts for a docker image "
43 "named `{image_name}`. Local commits and other changes "
44 "in your checkout may cause this error. Try "
45 "updating to a fresh checkout of mozilla-central "
46 "to download image.".format(image_name
=image_name
)
50 return load_image_by_task_id(task_id
, tag
)
53 def load_image_by_task_id(task_id
, tag
=None):
54 artifact_url
= get_artifact_url(task_id
, "public/image.tar.zst")
55 result
= load_image(artifact_url
, tag
)
56 print("Found docker image: {}:{}".format(result
["image"], result
["tag"]))
58 print(f
"Re-tagged as: {tag}")
60 tag
= "{}:{}".format(result
["image"], result
["tag"])
61 print(f
"Try: docker run -ti --rm {tag} bash")
65 def build_context(name
, outputFile
, args
=None):
66 """Build a context.tar for image with specified name."""
68 raise ValueError("must provide a Docker image name")
70 raise ValueError("must provide a outputFile")
72 image_dir
= docker
.image_path(name
)
73 if not os
.path
.isdir(image_dir
):
74 raise Exception("image directory does not exist: %s" % image_dir
)
76 docker
.create_context_tar(GECKO
, image_dir
, outputFile
, image_name
=name
, args
=args
)
79 def build_image(name
, tag
, args
=None):
80 """Build a Docker image of specified name.
82 Output from image building process will be printed to stdout.
85 raise ValueError("must provide a Docker image name")
87 image_dir
= docker
.image_path(name
)
88 if not os
.path
.isdir(image_dir
):
89 raise Exception("image directory does not exist: %s" % image_dir
)
91 tag
= tag
or docker
.docker_image(name
, by_tag
=True)
94 docker
.stream_context_tar(GECKO
, image_dir
, buf
, name
, args
)
95 docker
.post_to_docker(buf
.getvalue(), "/build", nocache
=1, t
=tag
)
97 print(f
"Successfully built {name} and tagged with {tag}")
99 if tag
.endswith(":latest"):
101 print("WARNING: no VERSION file found in image directory.")
102 print("Image is not suitable for deploying/pushing.")
103 print("Create an image suitable for deploying/pushing by creating")
104 print("a VERSION file in the image directory.")
108 def load_image(url
, imageName
=None, imageTag
=None):
110 Load docker image from URL as imageName:tag, if no imageName or tag is given
111 it will use whatever is inside the zstd compressed tarball.
113 Returns an object with properties 'image', 'tag' and 'layer'.
115 import zstandard
as zstd
117 # If imageName is given and we don't have an imageTag
118 # we parse out the imageTag from imageName, or default it to 'latest'
119 # if no imageName and no imageTag is given, 'repositories' won't be rewritten
120 if imageName
and not imageTag
:
122 imageName
, imageTag
= imageName
.split(":", 1)
128 def download_and_modify_image():
129 # This function downloads and edits the downloaded tar file on the fly.
130 # It emits chunked buffers of the editted tar file, as a generator.
131 print(f
"Downloading from {url}")
132 # get_session() gets us a requests.Session set to retry several times.
133 req
= get_session().get(url
, stream
=True)
134 req
.raise_for_status()
136 with zstd
.ZstdDecompressor().stream_reader(req
.raw
) as ifh
:
138 tarin
= tarfile
.open(
141 bufsize
=zstd
.DECOMPRESSION_RECOMMENDED_OUTPUT_SIZE
,
144 # Stream through each member of the downloaded tar file individually.
146 # Non-file members only need a tar header. Emit one.
147 if not member
.isfile():
148 yield member
.tobuf(tarfile
.GNU_FORMAT
)
151 # Open stream reader for the member
152 reader
= tarin
.extractfile(member
)
154 # If member is `repositories`, we parse and possibly rewrite the
156 if member
.name
== "repositories":
157 # Read and parse repositories
158 repos
= json
.loads(reader
.read())
161 # If there is more than one image or tag, we can't handle it
163 if len(repos
.keys()) > 1:
164 raise Exception("file contains more than one image")
165 info
["image"] = image
= list(repos
.keys())[0]
166 if len(repos
[image
].keys()) > 1:
167 raise Exception("file contains more than one tag")
168 info
["tag"] = tag
= list(repos
[image
].keys())[0]
169 info
["layer"] = layer
= repos
[image
][tag
]
171 # Rewrite the repositories file
172 data
= json
.dumps({imageName
or image
: {imageTag
or tag
: layer
}})
173 reader
= BytesIO(data
.encode("utf-8"))
174 member
.size
= len(data
)
176 # Emit the tar header for this member.
177 yield member
.tobuf(tarfile
.GNU_FORMAT
)
178 # Then emit its content.
179 remaining
= member
.size
181 length
= min(remaining
, zstd
.DECOMPRESSION_RECOMMENDED_OUTPUT_SIZE
)
182 buf
= reader
.read(length
)
183 remaining
-= len(buf
)
185 # Pad to fill a 512 bytes block, per tar format.
186 remainder
= member
.size
% 512
188 yield ("\0" * (512 - remainder
)).encode("utf-8")
192 docker
.post_to_docker(download_and_modify_image(), "/images/load", quiet
=0)
194 # Check that we found a repositories file
195 if not info
.get("image") or not info
.get("tag") or not info
.get("layer"):
196 raise Exception("No repositories file found!")