|  | 
| 2 | 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this | 
| 3 | 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. | 
| 4 | 4 | 
 | 
| 5 |  | - | 
| 6 | 5 | import logging | 
| 7 | 6 | import os | 
| 8 | 7 | import sys | 
| 9 | 8 | from dataclasses import dataclass | 
| 10 | 9 | from pathlib import Path | 
| 11 |  | - | 
| 12 |  | -from voluptuous import ALLOW_EXTRA, All, Any, Extra, Length, Optional, Required | 
|  | 10 | +from typing import Dict, List, Literal, Optional, Union | 
| 13 | 11 | 
 | 
| 14 | 12 | from .util.caches import CACHES | 
| 15 | 13 | from .util.python_path import find_object | 
| 16 |  | -from .util.schema import Schema, optionally_keyed_by, validate_schema | 
|  | 14 | +from .util.schema import ( | 
|  | 15 | +    Struct, | 
|  | 16 | +    TaskPriority, | 
|  | 17 | +    optionally_keyed_by, | 
|  | 18 | +    validate_schema, | 
|  | 19 | +) | 
| 17 | 20 | from .util.vcs import get_repository | 
| 18 | 21 | from .util.yaml import load_yaml | 
| 19 | 22 | 
 | 
| 20 | 23 | logger = logging.getLogger(__name__) | 
| 21 | 24 | 
 | 
|  | 25 | +# CacheName type for valid cache names | 
|  | 26 | +CacheName = Literal[tuple(CACHES.keys())] | 
| 22 | 27 | 
 | 
| 23 |  | -#: Schema for the graph config | 
| 24 |  | -graph_config_schema = Schema( | 
| 25 |  | -    { | 
| 26 |  | -        # The trust-domain for this graph. | 
| 27 |  | -        # (See https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/taskgraph.html#taskgraph-trust-domain)  # noqa | 
| 28 |  | -        Required("trust-domain"): str, | 
| 29 |  | -        Optional( | 
| 30 |  | -            "docker-image-kind", | 
| 31 |  | -            description="Name of the docker image kind (default: docker-image)", | 
| 32 |  | -        ): str, | 
| 33 |  | -        Required("task-priority"): optionally_keyed_by( | 
| 34 |  | -            "project", | 
| 35 |  | -            "level", | 
| 36 |  | -            Any( | 
| 37 |  | -                "highest", | 
| 38 |  | -                "very-high", | 
| 39 |  | -                "high", | 
| 40 |  | -                "medium", | 
| 41 |  | -                "low", | 
| 42 |  | -                "very-low", | 
| 43 |  | -                "lowest", | 
| 44 |  | -            ), | 
| 45 |  | -        ), | 
| 46 |  | -        Optional( | 
| 47 |  | -            "task-deadline-after", | 
| 48 |  | -            description="Default 'deadline' for tasks, in relative date format. " | 
| 49 |  | -            "Eg: '1 week'", | 
| 50 |  | -        ): optionally_keyed_by("project", str), | 
| 51 |  | -        Optional( | 
| 52 |  | -            "task-expires-after", | 
| 53 |  | -            description="Default 'expires-after' for level 1 tasks, in relative date format. " | 
| 54 |  | -            "Eg: '90 days'", | 
| 55 |  | -        ): str, | 
| 56 |  | -        Required("workers"): { | 
| 57 |  | -            Required("aliases"): { | 
| 58 |  | -                str: { | 
| 59 |  | -                    Required("provisioner"): optionally_keyed_by("level", str), | 
| 60 |  | -                    Required("implementation"): str, | 
| 61 |  | -                    Required("os"): str, | 
| 62 |  | -                    Required("worker-type"): optionally_keyed_by("level", str), | 
| 63 |  | -                } | 
| 64 |  | -            }, | 
| 65 |  | -        }, | 
| 66 |  | -        Required("taskgraph"): { | 
| 67 |  | -            Optional( | 
| 68 |  | -                "register", | 
| 69 |  | -                description="Python function to call to register extensions.", | 
| 70 |  | -            ): str, | 
| 71 |  | -            Optional("decision-parameters"): str, | 
| 72 |  | -            Optional( | 
| 73 |  | -                "cached-task-prefix", | 
| 74 |  | -                description="The taskcluster index prefix to use for caching tasks. " | 
| 75 |  | -                "Defaults to `trust-domain`.", | 
| 76 |  | -            ): str, | 
| 77 |  | -            Optional( | 
| 78 |  | -                "cache-pull-requests", | 
| 79 |  | -                description="Should tasks from pull requests populate the cache", | 
| 80 |  | -            ): bool, | 
| 81 |  | -            Optional( | 
| 82 |  | -                "index-path-regexes", | 
| 83 |  | -                description="Regular expressions matching index paths to be summarized.", | 
| 84 |  | -            ): [str], | 
| 85 |  | -            Optional( | 
| 86 |  | -                "run", | 
| 87 |  | -                description="Configuration related to the 'run' transforms.", | 
| 88 |  | -            ): { | 
| 89 |  | -                Optional( | 
| 90 |  | -                    "use-caches", | 
| 91 |  | -                    description="List of caches to enable, or a boolean to " | 
| 92 |  | -                    "enable/disable all of them.", | 
| 93 |  | -                ): Any(bool, list(CACHES.keys())), | 
| 94 |  | -            }, | 
| 95 |  | -            Required("repositories"): All( | 
| 96 |  | -                { | 
| 97 |  | -                    str: { | 
| 98 |  | -                        Required("name"): str, | 
| 99 |  | -                        Optional("project-regex"): str, | 
| 100 |  | -                        Optional("ssh-secret-name"): str, | 
| 101 |  | -                        # FIXME | 
| 102 |  | -                        Extra: str, | 
| 103 |  | -                    } | 
| 104 |  | -                }, | 
| 105 |  | -                Length(min=1), | 
| 106 |  | -            ), | 
| 107 |  | -        }, | 
| 108 |  | -    }, | 
| 109 |  | -    extra=ALLOW_EXTRA, | 
| 110 |  | -) | 
|  | 28 | + | 
|  | 29 | +class WorkerAliasStruct(Struct): | 
|  | 30 | +    """Worker alias configuration.""" | 
|  | 31 | + | 
|  | 32 | +    provisioner: optionally_keyed_by("level", str, use_msgspec=True)  # type: ignore | 
|  | 33 | +    implementation: str | 
|  | 34 | +    os: str | 
|  | 35 | +    worker_type: optionally_keyed_by("level", str, use_msgspec=True)  # type: ignore | 
|  | 36 | + | 
|  | 37 | + | 
|  | 38 | +class WorkersStruct(Struct, rename=None): | 
|  | 39 | +    """Workers configuration.""" | 
|  | 40 | + | 
|  | 41 | +    aliases: Dict[str, WorkerAliasStruct] | 
|  | 42 | + | 
|  | 43 | + | 
|  | 44 | +class Repository(Struct, forbid_unknown_fields=False): | 
|  | 45 | +    """Repository configuration. | 
|  | 46 | +
 | 
|  | 47 | +    This schema allows extra fields for repository-specific configuration. | 
|  | 48 | +    """ | 
|  | 49 | + | 
|  | 50 | +    # Required fields first | 
|  | 51 | +    name: str | 
|  | 52 | + | 
|  | 53 | +    # Optional fields | 
|  | 54 | +    project_regex: Optional[str] = None  # Maps from "project-regex" | 
|  | 55 | +    ssh_secret_name: Optional[str] = None  # Maps from "ssh-secret-name" | 
|  | 56 | + | 
|  | 57 | + | 
|  | 58 | +class RunConfig(Struct): | 
|  | 59 | +    """Run transforms configuration.""" | 
|  | 60 | + | 
|  | 61 | +    # List of caches to enable, or a boolean to enable/disable all of them. | 
|  | 62 | +    use_caches: Optional[Union[bool, List[str]]] = None  # Maps from "use-caches" | 
|  | 63 | + | 
|  | 64 | +    def __post_init__(self): | 
|  | 65 | +        """Validate that cache names are valid.""" | 
|  | 66 | +        if isinstance(self.use_caches, list): | 
|  | 67 | +            invalid = set(self.use_caches) - set(CACHES.keys()) | 
|  | 68 | +            if invalid: | 
|  | 69 | +                raise ValueError( | 
|  | 70 | +                    f"Invalid cache names: {invalid}. " | 
|  | 71 | +                    f"Valid names are: {list(CACHES.keys())}" | 
|  | 72 | +                ) | 
|  | 73 | + | 
|  | 74 | + | 
|  | 75 | +class TaskGraphStruct(Struct): | 
|  | 76 | +    """Taskgraph specific configuration.""" | 
|  | 77 | + | 
|  | 78 | +    # Required fields first | 
|  | 79 | +    repositories: Dict[str, Repository] | 
|  | 80 | + | 
|  | 81 | +    # Optional fields | 
|  | 82 | +    # Python function to call to register extensions. | 
|  | 83 | +    register: Optional[str] = None | 
|  | 84 | +    decision_parameters: Optional[str] = None  # Maps from "decision-parameters" | 
|  | 85 | +    # The taskcluster index prefix to use for caching tasks. Defaults to `trust-domain`. | 
|  | 86 | +    cached_task_prefix: Optional[str] = None  # Maps from "cached-task-prefix" | 
|  | 87 | +    # Should tasks from pull requests populate the cache | 
|  | 88 | +    cache_pull_requests: Optional[bool] = None  # Maps from "cache-pull-requests" | 
|  | 89 | +    # Regular expressions matching index paths to be summarized. | 
|  | 90 | +    index_path_regexes: Optional[List[str]] = None  # Maps from "index-path-regexes" | 
|  | 91 | +    # Configuration related to the 'run' transforms. | 
|  | 92 | +    run: Optional[RunConfig] = None | 
|  | 93 | + | 
|  | 94 | + | 
|  | 95 | +class GraphConfigStruct(Struct, forbid_unknown_fields=False): | 
|  | 96 | +    """Main graph configuration schema. | 
|  | 97 | +
 | 
|  | 98 | +    This schema allows extra fields for flexibility in graph configuration. | 
|  | 99 | +    """ | 
|  | 100 | + | 
|  | 101 | +    # Required fields first | 
|  | 102 | +    # The trust-domain for this graph. | 
|  | 103 | +    # (See https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/taskgraph.html#taskgraph-trust-domain) | 
|  | 104 | +    trust_domain: str  # Maps from "trust-domain" | 
|  | 105 | +    task_priority: optionally_keyed_by( | 
|  | 106 | +        "project", "level", TaskPriority, use_msgspec=True | 
|  | 107 | +    )  # type: ignore | 
|  | 108 | +    workers: WorkersStruct | 
|  | 109 | +    taskgraph: TaskGraphStruct | 
|  | 110 | + | 
|  | 111 | +    # Optional fields | 
|  | 112 | +    # Name of the docker image kind (default: docker-image) | 
|  | 113 | +    docker_image_kind: Optional[str] = None  # Maps from "docker-image-kind" | 
|  | 114 | +    # Default 'deadline' for tasks, in relative date format. Eg: '1 week' | 
|  | 115 | +    task_deadline_after: Optional[ | 
|  | 116 | +        optionally_keyed_by("project", str, use_msgspec=True) | 
|  | 117 | +    ] = None  # type: ignore | 
|  | 118 | +    # Default 'expires-after' for level 1 tasks, in relative date format. Eg: '90 days' | 
|  | 119 | +    task_expires_after: Optional[str] = None  # Maps from "task-expires-after" | 
| 111 | 120 | 
 | 
| 112 | 121 | 
 | 
| 113 | 122 | @dataclass(frozen=True, eq=False) | 
| @@ -178,7 +187,8 @@ def kinds_dir(self): | 
| 178 | 187 | 
 | 
| 179 | 188 | 
 | 
| 180 | 189 | def validate_graph_config(config): | 
| 181 |  | -    validate_schema(graph_config_schema, config, "Invalid graph configuration:") | 
|  | 190 | +    """Validate graph configuration using msgspec.""" | 
|  | 191 | +    validate_schema(GraphConfigStruct, config, "Invalid graph configuration:") | 
| 182 | 192 | 
 | 
| 183 | 193 | 
 | 
| 184 | 194 | def load_graph_config(root_dir): | 
|  | 
0 commit comments