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