# 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/. import logging import os import sys from dataclasses import dataclass from typing import Dict from voluptuous import All, Any, Extra, Length, Optional, Required from .util import path from .util.python_path import find_object from .util.schema import Schema, optionally_keyed_by, validate_schema from .util.yaml import load_yaml logger = logging.getLogger(__name__) graph_config_schema = Schema( { # The trust-domain for this graph. # (See https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/taskgraph.html#taskgraph-trust-domain) # noqa Required("trust-domain"): str, Required("task-priority"): optionally_keyed_by( "project", Any( "highest", "very-high", "high", "medium", "low", "very-low", "lowest", ), ), Optional( "task-deadline-after", description="Default 'deadline' for tasks, in relative date format. " "Eg: '1 week'", ): optionally_keyed_by("project", str), Optional( "task-expires-after", description="Default 'expires-after' for level 1 tasks, in relative date format. " "Eg: '90 days'", ): str, Required("workers"): { Required("aliases"): { str: { Required("provisioner"): optionally_keyed_by("level", str), Required("implementation"): str, Required("os"): str, Required("worker-type"): optionally_keyed_by("level", str), } }, }, Required("taskgraph"): { Optional( "register", description="Python function to call to register extensions.", ): str, Optional("decision-parameters"): str, Optional( "cached-task-prefix", description="The taskcluster index prefix to use for caching tasks. " "Defaults to `trust-domain`.", ): str, Optional( "cache-pull-requests", description="Should tasks from pull requests populate the cache", ): bool, Optional( "index-path-regexes", description="Regular expressions matching index paths to be summarized.", ): [str], Required("repositories"): All( { str: { Required("name"): str, Optional("project-regex"): str, Optional("ssh-secret-name"): str, # FIXME Extra: str, } }, Length(min=1), ), }, Extra: object, } ) """Schema for GraphConfig""" @dataclass(frozen=True, eq=False) class GraphConfig: _config: Dict root_dir: str _PATH_MODIFIED = False def __getitem__(self, name): return self._config[name] def __contains__(self, name): return name in self._config def register(self): """ Add the project's taskgraph directory to the python path, and register any extensions present. """ if GraphConfig._PATH_MODIFIED: if GraphConfig._PATH_MODIFIED == self.root_dir: # Already modified path with the same root_dir. # We currently need to do this to enable actions to call # taskgraph_decision, e.g. relpro. return raise Exception("Can't register multiple directories on python path.") GraphConfig._PATH_MODIFIED = self.root_dir sys.path.insert(0, self.root_dir) register_path = self["taskgraph"].get("register") if register_path: find_object(register_path)(self) @property def vcs_root(self): if path.split(self.root_dir)[-1:] != ["taskcluster"]: raise Exception( "Not guessing path to vcs root. " "Graph config in non-standard location." ) return os.path.dirname(self.root_dir) @property def taskcluster_yml(self): return os.path.join(self.vcs_root, ".taskcluster.yml") def validate_graph_config(config): validate_schema(graph_config_schema, config, "Invalid graph configuration:") def load_graph_config(root_dir): config_yml = os.path.join(root_dir, "config.yml") if not os.path.exists(config_yml): raise Exception(f"Couldn't find taskgraph configuration: {config_yml}") logger.debug(f"loading config from `{config_yml}`") config = load_yaml(config_yml) validate_graph_config(config) return GraphConfig(config, root_dir=root_dir)