import functools
import hashlib
import os
import re
import time
from copy import deepcopy
from dataclasses import dataclass
from typing import Callable
from voluptuous import All, Any, Extra, NotIn, Optional, Required
from taskgraph import MAX_DEPENDENCIES
from taskgraph.transforms.base import TransformSequence
from taskgraph.util.hash import hash_path
from taskgraph.util.keyed_by import evaluate_keyed_by
from taskgraph.util.schema import (
OptimizationSchema,
Schema,
optionally_keyed_by,
resolve_keyed_by,
taskref_or_string,
validate_schema,
)
from taskgraph.util.treeherder import split_symbol, treeherder_defaults
from taskgraph.util.workertypes import worker_type_implementation
from ..util import docker as dockerutil
from ..util.workertypes import get_worker_type
RUN_TASK = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "run-task", "run-task"
)
@functools.lru_cache(maxsize=None)
def _run_task_suffix():
return hash_path(RUN_TASK)[0:20]
task_description_schema = Schema(
{
Required("label"): str,
Required("description"): str,
Optional("attributes"): {str: object},
Optional("task-from"): str,
Optional("dependencies"): {
All(
str,
NotIn(
["self", "decision"],
"Can't use 'self` or 'decision' as dependency names.",
),
): object,
},
Optional("priority"): Any(
"highest",
"very-high",
"high",
"medium",
"low",
"very-low",
"lowest",
),
Optional("soft-dependencies"): [str],
Optional("if-dependencies"): [str],
Optional("requires"): Any("all-completed", "all-resolved"),
Optional("expires-after"): str,
Optional("deadline-after"): str,
Optional("routes"): [str],
Optional("scopes"): [str],
Optional("tags"): {str: str},
Optional("extra"): {str: object},
Optional("treeherder"): Any(
True,
{
"symbol": Optional(str),
"kind": Optional(Any("build", "test", "other")),
"tier": Optional(int),
"platform": Optional(str),
},
),
Optional("index"): {
"product": str,
"job-name": str,
"type": str,
"rank": Any(
"by-tier",
int,
"build_date",
),
},
Optional("run-on-projects"): optionally_keyed_by("build-platform", [str]),
Optional("run-on-tasks-for"): [str],
Optional("run-on-git-branches"): [str],
Optional("shipping-phase"): Any(
None,
"build",
"promote",
"push",
"ship",
),
Required("always-target"): bool,
Required("optimization"): OptimizationSchema,
"worker-type": str,
Required("needs-sccache"): bool,
Optional("worker"): {
Required("implementation"): str,
Extra: object,
},
}
)
TC_TREEHERDER_SCHEMA_URL = (
"https://github.com/taskcluster/taskcluster-treeherder/"
"blob/master/schemas/task-treeherder-config.yml"
)
UNKNOWN_GROUP_NAME = (
"Treeherder group {} (from {}) has no name; add it to taskcluster/config.yml"
)
V2_ROUTE_TEMPLATES = [
"index.{trust-domain}.v2.{project}.latest.{product}.{job-name}",
"index.{trust-domain}.v2.{project}.pushdate.{build_date_long}.{product}.{job-name}",
"index.{trust-domain}.v2.{project}.pushlog-id.{pushlog_id}.{product}.{job-name}",
"index.{trust-domain}.v2.{project}.revision.{branch_rev}.{product}.{job-name}",
]
TREEHERDER_ROUTE_ROOT = "tc-treeherder"
def get_branch_rev(config):
return config.params["head_rev"]
@functools.lru_cache(maxsize=None)
def get_default_priority(graph_config, project):
return evaluate_keyed_by(
graph_config["task-priority"], "Graph Config", {"project": project}
)
@functools.lru_cache(maxsize=None)
def get_default_deadline(graph_config, project):
return evaluate_keyed_by(
graph_config["task-deadline-after"], "Graph Config", {"project": project}
)
payload_builders = {}
@dataclass(frozen=True)
class PayloadBuilder:
schema: Schema
builder: Callable
def payload_builder(name, schema):
schema = Schema({Required("implementation"): name, Optional("os"): str}).extend(
schema
)
def wrap(func):
assert name not in payload_builders, f"duplicate payload builder name {name}"
payload_builders[name] = PayloadBuilder(schema, func) return func
return wrap
index_builders = {}
def index_builder(name):
def wrap(func):
assert name not in index_builders, f"duplicate index builder name {name}"
index_builders[name] = func
return func
return wrap
UNSUPPORTED_INDEX_PRODUCT_ERROR = """\
The index product {product} is not in the list of configured products in
`taskcluster/config.yml'.
"""
def verify_index(config, index):
product = index["product"]
if product not in config.graph_config["index"]["products"]:
raise Exception(UNSUPPORTED_INDEX_PRODUCT_ERROR.format(product=product))
@payload_builder(
"docker-worker",
schema={
Required("os"): "linux",
Required("docker-image"): Any(
str,
{"in-tree": str},
{"indexed": str},
),
Required("relengapi-proxy"): bool,
Required("chain-of-trust"): bool,
Required("taskcluster-proxy"): bool,
Required("allow-ptrace"): bool,
Required("loopback-video"): bool,
Required("loopback-audio"): bool,
Required("docker-in-docker"): bool, Required("privileged"): bool,
Optional("volumes"): [str],
Optional("caches"): [
{
"type": "persistent",
"name": str,
"mount-point": str,
Optional("skip-untrusted"): bool,
}
],
Optional("artifacts"): [
{
"type": Any("file", "directory", "volume"),
"path": str,
"name": str,
}
],
Required("env"): {str: taskref_or_string},
Optional("command"): [taskref_or_string],
Required("max-run-time"): int,
Optional("retry-exit-status"): [int],
Optional("purge-caches-exit-status"): [int],
Optional("skip-artifacts"): bool,
},
)
def build_docker_worker_payload(config, task, task_def):
worker = task["worker"]
level = int(config.params["level"])
image = worker["docker-image"]
if isinstance(image, dict):
if "in-tree" in image:
name = image["in-tree"]
docker_image_task = "docker-image-" + image["in-tree"]
assert "docker-image" not in task.get("dependencies", ()), (
"docker-image key in dependencies object is reserved"
)
task.setdefault("dependencies", {})["docker-image"] = docker_image_task
image = {
"path": "public/image.tar.zst",
"taskId": {"task-reference": "<docker-image>"},
"type": "task-image",
}
volumes = dockerutil.parse_volumes(name)
for v in sorted(volumes):
if v in worker["volumes"]:
raise Exception(
f"volume {v} already defined; "
"if it is defined in a Dockerfile, "
"it does not need to be specified in the "
"worker definition"
)
worker["volumes"].append(v)
elif "indexed" in image:
image = {
"path": "public/image.tar.zst",
"namespace": image["indexed"],
"type": "indexed-image",
}
else:
raise Exception("unknown docker image type")
features = {}
if worker.get("relengapi-proxy"):
features["relengAPIProxy"] = True
if worker.get("taskcluster-proxy"):
features["taskclusterProxy"] = True
if worker.get("allow-ptrace"):
features["allowPtrace"] = True
task_def["scopes"].append("docker-worker:feature:allowPtrace")
if worker.get("chain-of-trust"):
features["chainOfTrust"] = True
if worker.get("docker-in-docker"):
features["dind"] = True
if task.get("needs-sccache"):
features["taskclusterProxy"] = True
task_def["scopes"].append(
"assume:project:taskcluster:{trust_domain}:level-{level}-sccache-buckets".format(
trust_domain=config.graph_config["trust-domain"],
level=config.params["level"],
)
)
worker["env"]["USE_SCCACHE"] = "1"
worker["env"]["SCCACHE_IDLE_TIMEOUT"] = "0"
else:
worker["env"]["SCCACHE_DISABLE"] = "1"
capabilities = {}
for lo in "audio", "video":
if worker.get("loopback-" + lo):
capitalized = "loopback" + lo.capitalize()
devices = capabilities.setdefault("devices", {})
devices[capitalized] = True
task_def["scopes"].append("docker-worker:capability:device:" + capitalized)
if worker.get("privileged"):
capabilities["privileged"] = True
task_def["scopes"].append("docker-worker:capability:privileged")
task_def["payload"] = payload = {
"image": image,
"env": worker["env"],
}
if "command" in worker:
payload["command"] = worker["command"]
if "max-run-time" in worker:
payload["maxRunTime"] = worker["max-run-time"]
run_task = payload.get("command", [""])[0].endswith("run-task")
if run_task:
worker.setdefault("retry-exit-status", []).append(72)
worker.setdefault("purge-caches-exit-status", []).append(72)
payload["onExitStatus"] = {}
if "retry-exit-status" in worker:
payload["onExitStatus"]["retry"] = worker["retry-exit-status"]
if "purge-caches-exit-status" in worker:
payload["onExitStatus"]["purgeCaches"] = worker["purge-caches-exit-status"]
if "artifacts" in worker:
artifacts = {}
for artifact in worker["artifacts"]:
artifacts[artifact["name"]] = {
"path": artifact["path"],
"type": artifact["type"],
"expires": task_def["expires"], }
payload["artifacts"] = artifacts
if isinstance(worker.get("docker-image"), str):
out_of_tree_image = worker["docker-image"]
else:
out_of_tree_image = None
image = worker.get("docker-image", {}).get("in-tree")
if "caches" in worker:
caches = {}
cache_version = "v3"
if run_task:
suffix = f"{cache_version}-{_run_task_suffix()}"
if out_of_tree_image:
name_hash = hashlib.sha256(
out_of_tree_image.encode("utf-8")
).hexdigest()
suffix += name_hash[0:12]
else:
suffix += "-<docker-image>"
else:
suffix = cache_version
skip_untrusted = config.params.is_try() or level == 1
for cache in worker["caches"]:
if cache.get("skip-untrusted") and skip_untrusted:
continue
name = "{trust_domain}-level-{level}-{name}-{suffix}".format(
trust_domain=config.graph_config["trust-domain"],
level=config.params["level"],
name=cache["name"],
suffix=suffix,
)
caches[name] = cache["mount-point"]
task_def["scopes"].append({"task-reference": f"docker-worker:cache:{name}"})
if run_task:
payload["env"]["TASKCLUSTER_CACHES"] = ";".join(sorted(caches.values()))
payload["cache"] = {"task-reference": caches}
if run_task and worker.get("volumes"):
payload["env"]["TASKCLUSTER_VOLUMES"] = ";".join(sorted(worker["volumes"]))
if payload.get("cache") and skip_untrusted: payload["env"]["TASKCLUSTER_UNTRUSTED_CACHES"] = "1"
if features:
payload["features"] = features
if capabilities:
payload["capabilities"] = capabilities
check_caches_are_volumes(task)
@payload_builder(
"generic-worker",
schema={
Required("os"): Any("windows", "macosx", "linux", "linux-bitbar"),
Required("command"): Any(
[taskref_or_string],
[[taskref_or_string]], ),
Optional("artifacts"): [
{
"type": Any("file", "directory"),
"path": str,
Optional("name"): str,
}
],
Optional("mounts"): [
{
Optional("cache-name"): str,
Optional("content"): {
Optional("artifact"): str,
Optional("task-id"): taskref_or_string,
Optional("url"): str,
},
Optional("directory"): str,
Optional("file"): str,
Optional("format"): Any("rar", "tar.bz2", "tar.gz", "zip"),
}
],
Required("env"): {str: taskref_or_string},
Required("max-run-time"): int,
Optional("retry-exit-status"): [int],
Optional("purge-caches-exit-status"): [int],
Optional("os-groups"): [str],
Optional("run-as-administrator"): bool,
Optional("run-task-as-current-user"): bool,
Required("chain-of-trust"): bool,
Optional("taskcluster-proxy"): bool,
Optional("skip-artifacts"): bool,
},
)
def build_generic_worker_payload(config, task, task_def):
worker = task["worker"]
task_def["payload"] = {
"command": worker["command"],
"maxRunTime": worker["max-run-time"],
}
on_exit_status = {}
if "retry-exit-status" in worker:
on_exit_status["retry"] = worker["retry-exit-status"]
if "purge-caches-exit-status" in worker:
on_exit_status["purgeCaches"] = worker["purge-caches-exit-status"]
if worker["os"] == "windows":
on_exit_status.setdefault("retry", []).extend(
[
1073807364, 3221225786, ]
)
if on_exit_status:
task_def["payload"]["onExitStatus"] = on_exit_status
env = worker.get("env", {})
if task.get("needs-sccache"):
env["USE_SCCACHE"] = "1"
env["SCCACHE_IDLE_TIMEOUT"] = "0"
else:
env["SCCACHE_DISABLE"] = "1"
if env:
task_def["payload"]["env"] = env
artifacts = []
for artifact in worker.get("artifacts", []):
a = {
"path": artifact["path"],
"type": artifact["type"],
}
if "name" in artifact:
a["name"] = artifact["name"]
artifacts.append(a)
if artifacts:
task_def["payload"]["artifacts"] = artifacts
mounts = deepcopy(worker.get("mounts", []))
for mount in mounts:
if "cache-name" in mount:
mount["cacheName"] = "{trust_domain}-level-{level}-{name}".format(
trust_domain=config.graph_config["trust-domain"],
level=config.params["level"],
name=mount.pop("cache-name"),
)
task_def["scopes"].append(
"generic-worker:cache:{}".format(mount["cacheName"])
)
if "content" in mount:
if "task-id" in mount["content"]:
mount["content"]["taskId"] = mount["content"].pop("task-id")
if "artifact" in mount["content"]:
if not mount["content"]["artifact"].startswith("public/"):
task_def["scopes"].append(
"queue:get-artifact:{}".format(mount["content"]["artifact"])
)
if mounts:
task_def["payload"]["mounts"] = mounts
if worker.get("os-groups"):
task_def["payload"]["osGroups"] = worker["os-groups"]
task_def["scopes"].extend(
[
"generic-worker:os-group:{}/{}".format(task["worker-type"], group)
for group in worker["os-groups"]
]
)
features = {}
if worker.get("chain-of-trust"):
features["chainOfTrust"] = True
if worker.get("taskcluster-proxy"):
features["taskclusterProxy"] = True
if worker.get("run-as-administrator", False):
features["runAsAdministrator"] = True
task_def["scopes"].append(
"generic-worker:run-as-administrator:{}".format(task["worker-type"]),
)
if worker.get("run-task-as-current-user", False):
features["runTaskAsCurrentUser"] = True
task_def["scopes"].append(
"generic-worker:run-task-as-current-user:{}".format(task["worker-type"]),
)
if features:
task_def["payload"]["features"] = features
@payload_builder(
"beetmover",
schema={
Required("max-run-time"): int,
Optional("locale"): str,
Optional("partner-public"): bool,
Required("release-properties"): {
"app-name": str,
"app-version": str,
"branch": str,
"build-id": str,
"hash-type": str,
"platform": str,
},
Required("upstream-artifacts"): [
{
Required("taskId"): taskref_or_string,
Required("taskType"): str,
Required("paths"): [str],
Required("locale"): str,
}
],
Optional("artifact-map"): object,
},
)
def build_beetmover_payload(config, task, task_def):
worker = task["worker"]
release_properties = worker["release-properties"]
task_def["payload"] = {
"maxRunTime": worker["max-run-time"],
"releaseProperties": {
"appName": release_properties["app-name"],
"appVersion": release_properties["app-version"],
"branch": release_properties["branch"],
"buildid": release_properties["build-id"],
"hashType": release_properties["hash-type"],
"platform": release_properties["platform"],
},
"upload_date": config.params["build_date"],
"upstreamArtifacts": worker["upstream-artifacts"],
}
if worker.get("locale"):
task_def["payload"]["locale"] = worker["locale"]
if worker.get("artifact-map"):
task_def["payload"]["artifactMap"] = worker["artifact-map"]
if worker.get("partner-public"):
task_def["payload"]["is_partner_repack_public"] = worker["partner-public"]
@payload_builder(
"invalid",
schema={
Extra: object,
},
)
def build_invalid_payload(config, task, task_def):
task_def["payload"] = "invalid task - should never be created"
@payload_builder(
"always-optimized",
schema={
Extra: object,
},
)
@payload_builder("succeed", schema={})
def build_dummy_payload(config, task, task_def):
task_def["payload"] = {}
transforms = TransformSequence()
@transforms.add
def set_implementation(config, tasks):
for task in tasks:
worker = task.setdefault("worker", {})
if "implementation" in task["worker"]:
yield task
continue
impl, os = worker_type_implementation(config.graph_config, task["worker-type"])
tags = task.setdefault("tags", {})
tags["worker-implementation"] = impl
if os:
task["tags"]["os"] = os
worker["implementation"] = impl
if os:
worker["os"] = os
yield task
@transforms.add
def set_defaults(config, tasks):
for task in tasks:
task.setdefault("always-target", False)
task.setdefault("optimization", None)
task.setdefault("needs-sccache", False)
worker = task["worker"]
if worker["implementation"] in ("docker-worker",):
worker.setdefault("relengapi-proxy", False)
worker.setdefault("chain-of-trust", False)
worker.setdefault("taskcluster-proxy", False)
worker.setdefault("allow-ptrace", False)
worker.setdefault("loopback-video", False)
worker.setdefault("loopback-audio", False)
worker.setdefault("docker-in-docker", False)
worker.setdefault("privileged", False)
worker.setdefault("volumes", [])
worker.setdefault("env", {})
if "caches" in worker:
for c in worker["caches"]:
c.setdefault("skip-untrusted", False)
elif worker["implementation"] == "generic-worker":
worker.setdefault("env", {})
worker.setdefault("os-groups", [])
if worker["os-groups"] and worker["os"] != "windows":
raise Exception(
"os-groups feature of generic-worker is only supported on "
"Windows, not on {}".format(worker["os"])
)
worker.setdefault("chain-of-trust", False)
elif worker["implementation"] in (
"scriptworker-signing",
"beetmover",
"beetmover-push-to-release",
"beetmover-maven",
):
worker.setdefault("max-run-time", 600)
elif worker["implementation"] == "push-apk":
worker.setdefault("commit", False)
yield task
@transforms.add
def task_name_from_label(config, tasks):
for task in tasks:
if "label" not in task:
if "name" not in task:
raise Exception("task has neither a name nor a label")
task["label"] = "{}-{}".format(config.kind, task["name"])
if task.get("name"):
del task["name"]
yield task
@transforms.add
def validate(config, tasks):
for task in tasks:
validate_schema(
task_description_schema,
task,
"In task {!r}:".format(task.get("label", "?no-label?")),
)
validate_schema(
payload_builders[task["worker"]["implementation"]].schema,
task["worker"],
"In task.run {!r}:".format(task.get("label", "?no-label?")),
)
yield task
@index_builder("generic")
def add_generic_index_routes(config, task):
index = task.get("index")
routes = task.setdefault("routes", [])
verify_index(config, index)
subs = config.params.copy()
subs["job-name"] = index["job-name"]
subs["build_date_long"] = time.strftime(
"%Y.%m.%d.%Y%m%d%H%M%S", time.gmtime(config.params["build_date"])
)
subs["product"] = index["product"]
subs["trust-domain"] = config.graph_config["trust-domain"]
subs["branch_rev"] = get_branch_rev(config)
for tpl in V2_ROUTE_TEMPLATES:
routes.append(tpl.format(**subs))
return task
@transforms.add
def process_treeherder_metadata(config, tasks):
for task in tasks:
routes = task.get("routes", [])
extra = task.get("extra", {})
task_th = task.get("treeherder")
if task_th:
merged_th = treeherder_defaults(config.kind, task["label"])
if isinstance(task_th, dict):
merged_th.update(task_th)
treeherder = extra.setdefault("treeherder", {})
extra.setdefault("treeherder-platform", merged_th["platform"])
machine_platform, collection = merged_th["platform"].split("/", 1)
treeherder["machine"] = {"platform": machine_platform}
treeherder["collection"] = {collection: True}
group_names = config.graph_config["treeherder"]["group-names"]
groupSymbol, symbol = split_symbol(merged_th["symbol"])
if groupSymbol != "?":
treeherder["groupSymbol"] = groupSymbol
if groupSymbol not in group_names:
path = os.path.join(config.path, task.get("task-from", ""))
raise Exception(UNKNOWN_GROUP_NAME.format(groupSymbol, path))
treeherder["groupName"] = group_names[groupSymbol]
treeherder["symbol"] = symbol
if len(symbol) > 25 or len(groupSymbol) > 25:
raise RuntimeError(
"Treeherder group and symbol names must not be longer than "
"25 characters: {} (see {})".format(
treeherder["symbol"],
TC_TREEHERDER_SCHEMA_URL,
)
)
treeherder["jobKind"] = merged_th["kind"]
treeherder["tier"] = merged_th["tier"]
branch_rev = get_branch_rev(config)
if config.params["tasks_for"].startswith("github-pull-request"):
base_project = config.params["base_repository"].split("/")[-1]
if base_project.endswith(".git"):
base_project = base_project[:-4]
th_project_suffix = "-pr"
else:
base_project = config.params["project"]
th_project_suffix = ""
routes.append(
"{}.v2.{}.{}.{}".format(
TREEHERDER_ROUTE_ROOT,
base_project + th_project_suffix,
branch_rev,
config.params["pushlog_id"],
)
)
task["routes"] = routes
task["extra"] = extra
yield task
@transforms.add
def add_index_routes(config, tasks):
for task in tasks:
index = task.get("index", {})
extra_index = task.setdefault("extra", {}).setdefault("index", {})
rank = index.get("rank", "by-tier")
if rank == "by-tier":
tier = task.get("extra", {}).get("treeherder", {}).get("tier", 3)
extra_index["rank"] = 0 if tier > 1 else int(config.params["build_date"])
elif rank == "build_date":
extra_index["rank"] = int(config.params["build_date"])
else:
extra_index["rank"] = rank
if not index:
yield task
continue
index_type = index.get("type", "generic")
if index_type not in index_builders:
raise ValueError(f"Unknown index-type {index_type}")
task = index_builders[index_type](config, task)
del task["index"]
yield task
@transforms.add
def build_task(config, tasks):
for task in tasks:
level = str(config.params["level"])
provisioner_id, worker_type = get_worker_type(
config.graph_config,
task["worker-type"],
level,
)
task["worker-type"] = "/".join([provisioner_id, worker_type])
project = config.params["project"]
routes = task.get("routes", [])
scopes = [
s.format(level=level, project=project) for s in task.get("scopes", [])
]
extra = task.get("extra", {})
extra["parent"] = os.environ.get("TASK_ID", "")
if "expires-after" not in task:
task["expires-after"] = (
config.graph_config._config.get("task-expires-after", "28 days")
if config.params.is_try()
else "1 year"
)
if "deadline-after" not in task:
if "task-deadline-after" in config.graph_config:
task["deadline-after"] = get_default_deadline(
config.graph_config, config.params["project"]
)
else:
task["deadline-after"] = "1 day"
if "priority" not in task:
task["priority"] = get_default_priority(
config.graph_config, config.params["project"]
)
tags = task.get("tags", {})
tags.update(
{
"createdForUser": config.params["owner"],
"kind": config.kind,
"label": task["label"],
"project": config.params["project"],
"trust-domain": config.graph_config["trust-domain"],
}
)
task_def = {
"provisionerId": provisioner_id,
"workerType": worker_type,
"routes": routes,
"created": {"relative-datestamp": "0 seconds"},
"deadline": {"relative-datestamp": task["deadline-after"]},
"expires": {"relative-datestamp": task["expires-after"]},
"scopes": scopes,
"metadata": {
"description": task["description"],
"name": task["label"],
"owner": config.params["owner"],
"source": config.params.file_url(config.path, pretty=True),
},
"extra": extra,
"tags": tags,
"priority": task["priority"],
}
if task.get("requires", None):
task_def["requires"] = task["requires"]
if task.get("extra", {}).get("treeherder"):
branch_rev = get_branch_rev(config)
if config.params["tasks_for"].startswith("github-pull-request"):
base_project = config.params["base_repository"].split("/")[-1]
if base_project.endswith(".git"):
base_project = base_project[:-4]
th_project_suffix = "-pr"
else:
base_project = config.params["project"]
th_project_suffix = ""
th_push_link = (
"https://treeherder.mozilla.org/#/jobs?repo={}&revision={}".format(
config.params["project"] + th_project_suffix, branch_rev
)
)
task_def["metadata"]["description"] += (
f" ([Treeherder push]({th_push_link}))"
)
payload_builders[task["worker"]["implementation"]].builder(
config, task, task_def
)
attributes = task.get("attributes", {})
build_platform = attributes.get("build_platform")
resolve_keyed_by(
task,
"run-on-projects",
item_name=task["label"],
**{"build-platform": build_platform},
)
attributes["run_on_projects"] = task.get("run-on-projects", ["all"])
attributes["run_on_tasks_for"] = task.get("run-on-tasks-for", ["all"])
if task.get("run-on-git-branches"):
attributes["run_on_git_branches"] = task["run-on-git-branches"]
attributes["always_target"] = task["always-target"]
if task.get("shipping-phase") is not None:
attributes["shipping_phase"] = task["shipping-phase"]
else:
attributes.setdefault("shipping_phase", None)
if task["worker"]["implementation"] in (
"generic-worker",
"docker-worker",
):
payload = task_def.get("payload")
if payload:
env = payload.setdefault("env", {})
env["MOZ_AUTOMATION"] = "1"
dependencies = task.get("dependencies", {})
if_dependencies = task.get("if-dependencies", [])
if if_dependencies:
for i, dep in enumerate(if_dependencies):
if dep in dependencies:
if_dependencies[i] = dependencies[dep]
continue
raise Exception(
"{label} specifies '{dep}' in if-dependencies, "
"but {dep} is not a dependency!".format(
label=task["label"], dep=dep
)
)
yield {
"label": task["label"],
"description": task["description"],
"task": task_def,
"dependencies": dependencies,
"if-dependencies": if_dependencies,
"soft-dependencies": task.get("soft-dependencies", []),
"attributes": attributes,
"optimization": task.get("optimization", None),
}
@transforms.add
def add_github_checks(config, tasks):
if config.params["repository_type"] != "git":
for task in tasks:
yield task
for task in tasks:
task["task"]["routes"].append("checks")
yield task
@transforms.add
def chain_of_trust(config, tasks):
for task in tasks:
if task["task"].get("payload", {}).get("features", {}).get("chainOfTrust"):
image = task.get("dependencies", {}).get("docker-image")
if image:
cot = (
task["task"].setdefault("extra", {}).setdefault("chainOfTrust", {})
)
cot.setdefault("inputs", {})["docker-image"] = {
"task-reference": "<docker-image>"
}
yield task
@transforms.add
def check_task_identifiers(config, tasks):
e = re.compile("^[a-zA-Z0-9_-]{1,38}$")
for task in tasks:
for attrib in ("workerType", "provisionerId"):
if not e.match(task["task"][attrib]):
raise Exception(
"task {}.{} is not a valid identifier: {}".format(
task["label"], attrib, task["task"][attrib]
)
)
yield task
@transforms.add
def check_task_dependencies(config, tasks):
for task in tasks:
number_of_dependencies = (
len(task["dependencies"])
+ len(task["if-dependencies"])
+ len(task["soft-dependencies"])
)
if number_of_dependencies > MAX_DEPENDENCIES:
raise Exception(
"task {}/{} has too many dependencies ({} > {})".format(
config.kind,
task["label"],
number_of_dependencies,
MAX_DEPENDENCIES,
)
)
yield task
def check_caches_are_volumes(task):
volumes = set(task["worker"]["volumes"])
paths = {c["mount-point"] for c in task["worker"].get("caches", [])}
missing = paths - volumes
if not missing:
return
raise Exception(
"task {} (image {}) has caches that are not declared as "
"Docker volumes: {} "
"(have you added them as VOLUMEs in the Dockerfile?)".format(
task["label"], task["worker"]["docker-image"], ", ".join(sorted(missing))
)
)
@transforms.add
def check_run_task_caches(config, tasks):
re_reserved_caches = re.compile(
"""^
(checkouts|tooltool-cache)
""",
re.VERBOSE,
)
cache_prefix = "{trust_domain}-level-{level}-".format(
trust_domain=config.graph_config["trust-domain"],
level=config.params["level"],
)
suffix = _run_task_suffix()
for task in tasks:
payload = task["task"].get("payload", {})
command = payload.get("command") or [""]
main_command = command[0] if isinstance(command[0], str) else ""
run_task = main_command.endswith("run-task")
for cache in payload.get("cache", {}).get(
"task-reference", payload.get("cache", {})
):
if not cache.startswith(cache_prefix):
raise Exception(
"{} is using a cache ({}) which is not appropriate "
"for its trust-domain and level. It should start with {}.".format(
task["label"], cache, cache_prefix
)
)
cache = cache[len(cache_prefix) :]
if not re_reserved_caches.match(cache):
continue
if not run_task:
raise Exception(
f"{task['label']} is using a cache ({cache}) reserved for run-task "
"change the task to use run-task or use a different "
"cache name"
)
if suffix not in cache:
raise Exception(
f"{task['label']} is using a cache ({cache}) reserved for run-task "
"but the cache name is not dependent on the contents "
"of run-task; change the cache name to conform to the "
"naming requirements"
)
yield task