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-06-11 15:37 +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. 

14 

15 

16import operator 

17from dataclasses import dataclass 

18from typing import Any, List 

19 

20from lark import Tree 

21from lark.visitors import Interpreter 

22 

23from buildgrid._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) 

32 

33 

34@dataclass 

35class OperationFilterSpec: 

36 name: str 

37 description: str 

38 sanitizer: ValueSanitizer 

39 

40 

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 # Validate timestamps with a special sanitizer 

95 "queued_time": OperationFilterSpec( 

96 name="Queued Time", 

97 description="The time at which the Action was queued.", 

98 sanitizer=DatetimeValueSanitizer("queued_time"), 

99 ), 

100 "start_time": OperationFilterSpec( 

101 name="Start Time", 

102 description="The time at which a worker started executing the Action.", 

103 sanitizer=DatetimeValueSanitizer("start_time"), 

104 ), 

105 "completed_time": OperationFilterSpec( 

106 name="Completed Time", 

107 description="The time at which a worker finished executing the Action.", 

108 sanitizer=DatetimeValueSanitizer("completed_time"), 

109 ), 

110 # Backends determine what sort orders are acceptable 

111 "sort_order": OperationFilterSpec( 

112 name="Sort Order", 

113 description="Define an ordering for the results.", 

114 sanitizer=SortKeyValueSanitizer("sort_order"), 

115 ), 

116} 

117 

118 

119OPERATOR_MAP = { 

120 "=": operator.eq, 

121 ">": operator.gt, 

122 ">=": operator.ge, 

123 "<": operator.lt, 

124 "<=": operator.le, 

125 "!=": operator.ne, 

126} 

127 

128 

129class FilterTreeInterpreter(Interpreter[Any]): 

130 """Interpreter for the parse tree. 

131 

132 Calling FilterTreeInterpreter().visit(tree) walks the parse tree and 

133 outputs a list of OperationFilters.""" 

134 

135 def filter_phrase(self, tree: Tree) -> List[OperationFilter]: 

136 return self.visit_children(tree) 

137 

138 def filter_elem(self, tree: Tree) -> OperationFilter: 

139 try: 

140 token_map = {token.type: str(token) for token in tree.children} # type: ignore 

141 

142 # Check that the parameter is valid 

143 parameter = token_map["PARAMETER"] 

144 if parameter not in VALID_OPERATION_FILTERS: 

145 raise InvalidArgumentError(f"Invalid filter parameter [{parameter}].") 

146 

147 # Sanitize the value 

148 sanitizer = VALID_OPERATION_FILTERS[parameter].sanitizer 

149 sanitized_value = sanitizer.sanitize(token_map["VALUE"]) 

150 

151 return OperationFilter( 

152 parameter=token_map["PARAMETER"], operator=OPERATOR_MAP[token_map["OPERATOR"]], value=sanitized_value 

153 ) 

154 except KeyError as e: 

155 raise InvalidArgumentError(f"Invalid filter element. Token map: {token_map}") from e