| 
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