summaryrefslogtreecommitdiffstats
path: root/taskcluster/taskgraph/parameters.py
diff options
context:
space:
mode:
Diffstat (limited to 'taskcluster/taskgraph/parameters.py')
-rw-r--r--taskcluster/taskgraph/parameters.py327
1 files changed, 327 insertions, 0 deletions
diff --git a/taskcluster/taskgraph/parameters.py b/taskcluster/taskgraph/parameters.py
new file mode 100644
index 0000000000..76ed47c950
--- /dev/null
+++ b/taskcluster/taskgraph/parameters.py
@@ -0,0 +1,327 @@
+# -*- 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 io
+import logging
+import os.path
+import json
+from datetime import datetime
+
+from mozbuild.util import ReadOnlyDict, memoize
+from mozversioncontrol import get_repository_object
+from taskgraph.util.schema import validate_schema
+from voluptuous import (
+ ALLOW_EXTRA,
+ Any,
+ Inclusive,
+ Required,
+ Schema,
+)
+
+import six
+from six import text_type
+
+from . import GECKO
+from .util.attributes import release_level
+
+logger = logging.getLogger(__name__)
+
+
+class ParameterMismatch(Exception):
+ """Raised when a parameters.yml has extra or missing parameters."""
+
+
+@memoize
+def get_head_ref():
+ return six.ensure_text(get_repository_object(GECKO).head_ref)
+
+
+def get_contents(path):
+ with io.open(path, "r") as fh:
+ contents = fh.readline().rstrip()
+ return contents
+
+
+def get_version(product_dir="browser"):
+ version_path = os.path.join(GECKO, product_dir, "config", "version_display.txt")
+ return get_contents(version_path)
+
+
+def get_app_version(product_dir="browser"):
+ app_version_path = os.path.join(GECKO, product_dir, "config", "version.txt")
+ return get_contents(app_version_path)
+
+
+base_schema = Schema(
+ {
+ Required("app_version"): text_type,
+ Required("backstop"): bool,
+ Required("base_repository"): text_type,
+ Required("build_date"): int,
+ Required("build_number"): int,
+ Inclusive("comm_base_repository", "comm"): text_type,
+ Inclusive("comm_head_ref", "comm"): text_type,
+ Inclusive("comm_head_repository", "comm"): text_type,
+ Inclusive("comm_head_rev", "comm"): text_type,
+ Required("do_not_optimize"): [text_type],
+ Required("existing_tasks"): {text_type: text_type},
+ Required("filters"): [text_type],
+ Required("head_ref"): text_type,
+ Required("head_repository"): text_type,
+ Required("head_rev"): text_type,
+ Required("hg_branch"): text_type,
+ Required("level"): text_type,
+ Required("message"): text_type,
+ Required("moz_build_date"): text_type,
+ Required("next_version"): Any(None, text_type),
+ Required("optimize_strategies"): Any(None, text_type),
+ Required("optimize_target_tasks"): bool,
+ Required("owner"): text_type,
+ Required("phabricator_diff"): Any(None, text_type),
+ Required("project"): text_type,
+ Required("pushdate"): int,
+ Required("pushlog_id"): text_type,
+ Required("release_enable_emefree"): bool,
+ Required("release_enable_partner_repack"): bool,
+ Required("release_enable_partner_attribution"): bool,
+ Required("release_eta"): Any(None, text_type),
+ Required("release_history"): {text_type: dict},
+ Required("release_partners"): Any(None, [text_type]),
+ Required("release_partner_config"): Any(None, dict),
+ Required("release_partner_build_number"): int,
+ Required("release_type"): text_type,
+ Required("release_product"): Any(None, text_type),
+ Required("required_signoffs"): [text_type],
+ Required("signoff_urls"): dict,
+ # target-kind is not included, since it should never be
+ # used at run-time
+ Required("target_tasks_method"): text_type,
+ Required("tasks_for"): text_type,
+ Required("test_manifest_loader"): text_type,
+ Required("try_mode"): Any(None, text_type),
+ Required("try_options"): Any(None, dict),
+ Required("try_task_config"): dict,
+ Required("version"): text_type,
+ }
+)
+
+
+COMM_PARAMETERS = [
+ "comm_base_repository",
+ "comm_head_ref",
+ "comm_head_repository",
+ "comm_head_rev",
+]
+
+
+def extend_parameters_schema(schema):
+ """
+ Extend the schema for parameters to include per-project configuration.
+
+ This should be called by the `taskgraph.register` function in the
+ graph-configuration.
+ """
+ global base_schema
+ base_schema = base_schema.extend(schema)
+
+
+class Parameters(ReadOnlyDict):
+ """An immutable dictionary with nicer KeyError messages on failure"""
+
+ def __init__(self, strict=True, **kwargs):
+ self.strict = strict
+
+ if not self.strict:
+ # apply defaults to missing parameters
+ kwargs = Parameters._fill_defaults(**kwargs)
+
+ ReadOnlyDict.__init__(self, **kwargs)
+
+ @staticmethod
+ def _fill_defaults(**kwargs):
+ now = datetime.utcnow()
+ epoch = datetime.utcfromtimestamp(0)
+ seconds_from_epoch = int((now - epoch).total_seconds())
+
+ defaults = {
+ "app_version": get_app_version(),
+ "backstop": False,
+ "base_repository": "https://hg.mozilla.org/mozilla-unified",
+ "build_date": seconds_from_epoch,
+ "build_number": 1,
+ "do_not_optimize": [],
+ "existing_tasks": {},
+ "filters": ["target_tasks_method"],
+ "head_ref": get_head_ref(),
+ "head_repository": "https://hg.mozilla.org/mozilla-central",
+ "head_rev": get_head_ref(),
+ "hg_branch": "default",
+ "level": "3",
+ "message": "",
+ "moz_build_date": six.ensure_text(now.strftime("%Y%m%d%H%M%S")),
+ "next_version": None,
+ "optimize_strategies": None,
+ "optimize_target_tasks": True,
+ "owner": "nobody@mozilla.com",
+ "phabricator_diff": None,
+ "project": "mozilla-central",
+ "pushdate": seconds_from_epoch,
+ "pushlog_id": "0",
+ "release_enable_emefree": False,
+ "release_enable_partner_repack": False,
+ "release_enable_partner_attribution": False,
+ "release_eta": "",
+ "release_history": {},
+ "release_partners": [],
+ "release_partner_config": None,
+ "release_partner_build_number": 1,
+ "release_product": None,
+ "release_type": "nightly",
+ "required_signoffs": [],
+ "signoff_urls": {},
+ "target_tasks_method": "default",
+ "tasks_for": "hg-push",
+ "test_manifest_loader": "default",
+ "try_mode": None,
+ "try_options": None,
+ "try_task_config": {},
+ "version": get_version(),
+ }
+
+ if set(COMM_PARAMETERS) & set(kwargs):
+ defaults.update(
+ {
+ "comm_base_repository": "https://hg.mozilla.org/comm-central",
+ "comm_head_repository": "https://hg.mozilla.org/comm-central",
+ }
+ )
+
+ for name, default in defaults.items():
+ if name not in kwargs:
+ kwargs[name] = default
+
+ return kwargs
+
+ def check(self):
+ schema = (
+ base_schema if self.strict else base_schema.extend({}, extra=ALLOW_EXTRA)
+ )
+ validate_schema(schema, self.copy(), "Invalid parameters:")
+
+ def __getitem__(self, k):
+ try:
+ return super(Parameters, self).__getitem__(k)
+ except KeyError:
+ raise KeyError("taskgraph parameter {!r} not found".format(k))
+
+ def is_try(self):
+ """
+ Determine whether this graph is being built on a try project or for
+ `mach try fuzzy`.
+ """
+ return "try" in self["project"] or self["try_mode"] == "try_select"
+
+ def file_url(self, path, pretty=False):
+ """
+ Determine the VCS URL for viewing a file in the tree, suitable for
+ viewing by a human.
+
+ :param text_type path: The path, relative to the root of the repository.
+ :param bool pretty: Whether to return a link to a formatted version of the
+ file, or the raw file version.
+ :return text_type: The URL displaying the given path.
+ """
+ if path.startswith("comm/"):
+ path = path[len("comm/") :]
+ repo = self["comm_head_repository"]
+ rev = self["comm_head_rev"]
+ else:
+ repo = self["head_repository"]
+ rev = self["head_rev"]
+
+ endpoint = "file" if pretty else "raw-file"
+ return "{}/{}/{}/{}".format(repo, endpoint, rev, path)
+
+ def release_level(self):
+ """
+ Whether this is a staging release or not.
+
+ :return six.text_type: One of "production" or "staging".
+ """
+ return release_level(self["project"])
+
+
+def load_parameters_file(filename, strict=True, overrides=None, trust_domain=None):
+ """
+ Load parameters from a path, url, decision task-id or project.
+
+ Examples:
+ task-id=fdtgsD5DQUmAQZEaGMvQ4Q
+ project=mozilla-central
+ """
+ import requests
+ from taskgraph.util.taskcluster import get_artifact_url, find_task_id
+ from taskgraph.util import yaml
+
+ if overrides is None:
+ overrides = {}
+
+ if not filename:
+ return Parameters(strict=strict, **overrides)
+
+ try:
+ # reading parameters from a local parameters.yml file
+ f = open(filename)
+ except IOError:
+ # fetching parameters.yml using task task-id, project or supplied url
+ task_id = None
+ if filename.startswith("task-id="):
+ task_id = filename.split("=")[1]
+ elif filename.startswith("project="):
+ if trust_domain is None:
+ raise ValueError(
+ "Can't specify parameters by project "
+ "if trust domain isn't supplied.",
+ )
+ index = "{trust_domain}.v2.{project}.latest.taskgraph.decision".format(
+ trust_domain=trust_domain,
+ project=filename.split("=")[1],
+ )
+ task_id = find_task_id(index)
+
+ if task_id:
+ filename = get_artifact_url(task_id, "public/parameters.yml")
+ logger.info("Loading parameters from {}".format(filename))
+ resp = requests.get(filename, stream=True)
+ resp.raise_for_status()
+ f = resp.raw
+
+ if filename.endswith(".yml"):
+ kwargs = yaml.load_stream(f)
+ elif filename.endswith(".json"):
+ kwargs = json.load(f)
+ else:
+ raise TypeError("Parameters file `{}` is not JSON or YAML".format(filename))
+
+ kwargs.update(overrides)
+
+ return Parameters(strict=strict, **kwargs)
+
+
+def parameters_loader(filename, strict=True, overrides=None):
+ def get_parameters(graph_config):
+ parameters = load_parameters_file(
+ filename,
+ strict=strict,
+ overrides=overrides,
+ trust_domain=graph_config["trust-domain"],
+ )
+ parameters.check()
+ return parameters
+
+ return get_parameters