import os
import re
from dataclasses import dataclass
from typing import Callable
from voluptuous import Extra, Optional, Required
import taskgraph
from ..util import path
from ..util.cached_tasks import add_optimization
from ..util.schema import Schema, validate_schema
from ..util.treeherder import join_symbol
from .base import TransformSequence
CACHE_TYPE = "content.v1"
FETCH_SCHEMA = Schema(
{
Required("name"): str,
Optional("task-from"): str,
Required("description"): str,
Optional("expires-after"): str,
Optional("docker-image"): object,
Optional(
"fetch-alias",
description="An alias that can be used instead of the real fetch task name in "
"fetch stanzas for tasks.",
): str,
Optional(
"artifact-prefix",
description="The prefix of the taskcluster artifact being uploaded. "
"Defaults to `public/`; if it starts with something other than "
"`public/` the artifact will require scopes to access.",
): str,
Optional("attributes"): {str: object},
Required("fetch"): {
Required("type"): str,
Extra: object,
},
}
)
fetch_builders = {}
@dataclass(frozen=True)
class FetchBuilder:
schema: Schema
builder: Callable
def fetch_builder(name, schema):
schema = Schema({Required("type"): name}).extend(schema)
def wrap(func):
fetch_builders[name] = FetchBuilder(schema, func) return func
return wrap
transforms = TransformSequence()
transforms.add_validate(FETCH_SCHEMA)
@transforms.add
def process_fetch_task(config, tasks):
for task in tasks:
typ = task["fetch"]["type"]
name = task["name"]
fetch = task.pop("fetch")
if typ not in fetch_builders:
raise Exception(f"Unknown fetch type {typ} in fetch {name}")
validate_schema(fetch_builders[typ].schema, fetch, f"In task.fetch {name!r}:")
task.update(configure_fetch(config, typ, name, fetch))
yield task
def configure_fetch(config, typ, name, fetch):
if typ not in fetch_builders:
raise Exception(f"No fetch type {typ} in fetch {name}")
validate_schema(fetch_builders[typ].schema, fetch, f"In task.fetch {name!r}:")
return fetch_builders[typ].builder(config, name, fetch)
@transforms.add
def make_task(config, tasks):
if config.params["level"] == "3":
expires = "1000 years"
else:
expires = config.graph_config._config.get("task-expires-after", "28 days")
for task in tasks:
name = task["name"]
artifact_prefix = task.get("artifact-prefix", "public")
env = task.get("env", {})
env.update({"UPLOAD_DIR": "/builds/worker/artifacts"})
attributes = task.get("attributes", {})
attributes["fetch-artifact"] = path.join(artifact_prefix, task["artifact_name"])
alias = task.get("fetch-alias")
if alias:
attributes["fetch-alias"] = alias
task_desc = {
"attributes": attributes,
"name": name,
"description": task["description"],
"expires-after": task.get("expires-after", expires),
"label": f"fetch-{name}",
"run-on-projects": [],
"run": {
"using": "run-task",
"checkout": False,
"command": task["command"],
},
"worker-type": "images",
"worker": {
"chain-of-trust": True,
"docker-image": task.get("docker-image", {"in-tree": "fetch"}),
"env": env,
"max-run-time": 900,
"artifacts": [
{
"type": "directory",
"name": artifact_prefix,
"path": "/builds/worker/artifacts",
}
],
},
}
if "treeherder" in config.graph_config:
task_desc["treeherder"] = {
"symbol": join_symbol("Fetch", name),
"kind": "build",
"platform": "fetch/opt",
"tier": 1,
}
if task.get("secret", None):
task_desc["scopes"] = ["secrets:get:" + task.get("secret")]
task_desc["worker"]["taskcluster-proxy"] = True
if not taskgraph.fast:
cache_name = task_desc["label"].replace(f"{config.kind}-", "", 1)
add_optimization(
config,
task_desc,
cache_type=CACHE_TYPE,
cache_name=cache_name,
digest_data=task["digest_data"],
)
yield task_desc
@fetch_builder(
"static-url",
schema={
Required("url"): str,
Required("sha256"): str,
Required("size"): int,
Optional("gpg-signature"): {
Required("sig-url"): str,
Required("key-path"): str,
},
Optional("artifact-name"): str,
Optional("strip-components"): int,
Optional("add-prefix"): str,
Optional("headers"): {
str: str,
},
},
)
def create_fetch_url_task(config, name, fetch):
artifact_name = fetch.get("artifact-name")
if not artifact_name:
artifact_name = fetch["url"].split("/")[-1]
command = [
"fetch-content",
"static-url",
]
args = [
"--sha256",
fetch["sha256"],
"--size",
f"{fetch['size']}",
]
if fetch.get("strip-components"):
args.extend(["--strip-components", f"{fetch['strip-components']}"])
if fetch.get("add-prefix"):
args.extend(["--add-prefix", fetch["add-prefix"]])
command.extend(args)
env = {}
if "gpg-signature" in fetch:
sig_url = fetch["gpg-signature"]["sig-url"].format(url=fetch["url"])
key_path = os.path.join(taskgraph.GECKO, fetch["gpg-signature"]["key-path"])
with open(key_path) as fh:
gpg_key = fh.read()
env["FETCH_GPG_KEY"] = gpg_key
command.extend(
[
"--gpg-sig-url",
sig_url,
"--gpg-key-env",
"FETCH_GPG_KEY",
]
)
if "headers" in fetch:
for k, v in fetch["headers"].items():
command.extend(["-H", f"{k}:{v}"])
command.extend([fetch["url"], f"/builds/worker/artifacts/{artifact_name}"])
return {
"command": command,
"artifact_name": artifact_name,
"env": env,
"digest_data": args + [artifact_name],
}
@fetch_builder(
"git",
schema={
Required("repo"): str,
Required("revision"): str,
Optional("include-dot-git"): bool,
Optional("artifact-name"): str,
Optional("path-prefix"): str,
Optional("ssh-key"): str,
},
)
def create_git_fetch_task(config, name, fetch):
path_prefix = fetch.get("path-prefix")
if not path_prefix:
path_prefix = fetch["repo"].rstrip("/").rsplit("/", 1)[-1]
artifact_name = fetch.get("artifact-name")
if not artifact_name:
artifact_name = f"{path_prefix}.tar.zst"
if not re.match(r"[0-9a-fA-F]{40}", fetch["revision"]):
raise Exception(f'Revision is not a sha1 in fetch task "{name}"')
args = [
"fetch-content",
"git-checkout-archive",
"--path-prefix",
path_prefix,
fetch["repo"],
fetch["revision"],
f"/builds/worker/artifacts/{artifact_name}",
]
ssh_key = fetch.get("ssh-key")
if ssh_key:
args.append("--ssh-key-secret")
args.append(ssh_key)
digest_data = [fetch["revision"], path_prefix, artifact_name]
if fetch.get("include-dot-git", False):
args.append("--include-dot-git")
digest_data.append(".git")
return {
"command": args,
"artifact_name": artifact_name,
"digest_data": digest_data,
"secret": ssh_key,
}