Coverage for /builds/BuildGrid/buildgrid/buildgrid/server/operations/filtering/interpreter.py: 96.55%
29 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-10-04 17:48 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-10-04 17:48 +0000
1# Copyright (C) 2020 Bloomberg LP
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# <http://www.apache.org/licenses/LICENSE-2.0>
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
16import operator
17from dataclasses import dataclass
18from typing import Any, List
20from lark import Tree
21from lark.visitors import Interpreter
23from buildgrid.server.exceptions import InvalidArgumentError
24from buildgrid.server.operations.filtering import OperationFilter
25from buildgrid.server.operations.filtering.sanitizer import (
26 DatetimeValueSanitizer,
27 OperationStageValueSanitizer,
28 RegexValueSanitizer,
29 SortKeyValueSanitizer,
30 ValueSanitizer,
31)
34@dataclass
35class OperationFilterSpec:
36 name: str
37 description: str
38 sanitizer: ValueSanitizer
41# Valid operation filters mapped to regexes representing valid values.
42VALID_OPERATION_FILTERS = {
43 "stage": OperationFilterSpec(
44 name="Stage",
45 description="Operation stage, for example QUEUED or EXECUTING.",
46 sanitizer=OperationStageValueSanitizer("stage"),
47 ),
48 # The operation name can technically be parsed as a UUID4, but this
49 # parses it as an arbitrary string in case the naming scheme changes
50 # in the future
51 "name": OperationFilterSpec(
52 name="Name",
53 description="Operation name, without the BuildGrid instance name.",
54 sanitizer=RegexValueSanitizer("name", r"\S+"),
55 ),
56 # The command is an arbitrary string
57 "command": OperationFilterSpec(
58 name="Command",
59 description="Command to be executed by the remote worker.",
60 sanitizer=RegexValueSanitizer("command", r".+"),
61 ),
62 # The platform properties are `key:value`
63 "platform": OperationFilterSpec(
64 name="Platform",
65 description="Platform property specified by the Action, in the form `key:value`.",
66 sanitizer=RegexValueSanitizer("platform", r"\S+:.+"),
67 ),
68 "action_digest": OperationFilterSpec(
69 name="Action Digest",
70 description="Digest of the Action message, in the form `hash/size_bytes`.",
71 sanitizer=RegexValueSanitizer("action_digest", r"[a-f0-9]+/[0-9]+"),
72 ),
73 # The request metadata is all arbitrary strings
74 "invocation_id": OperationFilterSpec(
75 name="Invocation ID",
76 description="The unique ID for a given invocation of the tool which started the Operation.",
77 sanitizer=RegexValueSanitizer("invocation_id", r".+"),
78 ),
79 "correlated_invocations_id": OperationFilterSpec(
80 name="Correlated Invocations ID",
81 description="The unique ID for a set of related tool invocations.",
82 sanitizer=RegexValueSanitizer("correlated_invocations_id", r".+"),
83 ),
84 "tool_name": OperationFilterSpec(
85 name="Tool Name",
86 description="The name of the tool which started the Operation.",
87 sanitizer=RegexValueSanitizer("tool_name", r".+"),
88 ),
89 "tool_version": OperationFilterSpec(
90 name="Tool Version",
91 description="The version of the tool that started the Operation.",
92 sanitizer=RegexValueSanitizer("tool_version", r".+"),
93 ),
94 "action_mnemonic": OperationFilterSpec(
95 name="Action Mnemonic",
96 description="A brief description of the kind of action. This is not "
97 "standardized and contents are up to the client tooling used.",
98 sanitizer=RegexValueSanitizer("action_mnemonic", r".+"),
99 ),
100 "target_id": OperationFilterSpec(
101 name="Target ID",
102 description="An identifier for the target which produced this action. "
103 "A single target may relate to one or many actions.",
104 sanitizer=RegexValueSanitizer("target_id", r".+"),
105 ),
106 "configuration_id": OperationFilterSpec(
107 name="Configuration ID",
108 description="An identifier for the configuration in which the target was built, "
109 "e.g. for differentiating host tooling or different target platforms.",
110 sanitizer=RegexValueSanitizer("configuration_id", r".+"),
111 ),
112 # Validate timestamps with a special sanitizer
113 "queued_time": OperationFilterSpec(
114 name="Queued Time",
115 description="The time at which the Action was queued.",
116 sanitizer=DatetimeValueSanitizer("queued_time"),
117 ),
118 "start_time": OperationFilterSpec(
119 name="Start Time",
120 description="The time at which a worker started executing the Action.",
121 sanitizer=DatetimeValueSanitizer("start_time"),
122 ),
123 "completed_time": OperationFilterSpec(
124 name="Completed Time",
125 description="The time at which a worker finished executing the Action.",
126 sanitizer=DatetimeValueSanitizer("completed_time"),
127 ),
128 # Client identity metadata is all arbitrary strings
129 "client_workflow": OperationFilterSpec(
130 name="Client Workflow",
131 description="The client workflow information that was used to authorize the Action.",
132 sanitizer=RegexValueSanitizer("action_mnemonic", r".+"),
133 ),
134 "client_actor": OperationFilterSpec(
135 name="Client Actor",
136 description="The client actor that was used to authorize the Action.",
137 sanitizer=RegexValueSanitizer("target_id", r".+"),
138 ),
139 "client_subject": OperationFilterSpec(
140 name="Client Subject",
141 description="The client subject that was used to authorize the Action.",
142 sanitizer=RegexValueSanitizer("configuration_id", r".+"),
143 ),
144 # Backends determine what sort orders are acceptable
145 "sort_order": OperationFilterSpec(
146 name="Sort Order",
147 description="Define an ordering for the results.",
148 sanitizer=SortKeyValueSanitizer("sort_order"),
149 ),
150}
153OPERATOR_MAP = {
154 "=": operator.eq,
155 ">": operator.gt,
156 ">=": operator.ge,
157 "<": operator.lt,
158 "<=": operator.le,
159 "!=": operator.ne,
160}
163class FilterTreeInterpreter(Interpreter[Any]):
164 """Interpreter for the parse tree.
166 Calling FilterTreeInterpreter().visit(tree) walks the parse tree and
167 outputs a list of OperationFilters."""
169 def filter_phrase(self, tree: Tree) -> List[OperationFilter]:
170 return self.visit_children(tree)
172 def filter_elem(self, tree: Tree) -> OperationFilter:
173 try:
174 token_map = {token.type: str(token) for token in tree.children} # type: ignore
176 # Check that the parameter is valid
177 parameter = token_map["PARAMETER"]
178 if parameter not in VALID_OPERATION_FILTERS:
179 raise InvalidArgumentError(f"Invalid filter parameter [{parameter}].")
181 # Sanitize the value
182 sanitizer = VALID_OPERATION_FILTERS[parameter].sanitizer
183 sanitized_value = sanitizer.sanitize(token_map["VALUE"])
185 return OperationFilter(
186 parameter=token_map["PARAMETER"], operator=OPERATOR_MAP[token_map["OPERATOR"]], value=sanitized_value
187 )
188 except KeyError as e:
189 raise InvalidArgumentError(f"Invalid filter element. Token map: {token_map}") from e