1# Copyright 2020 gRPC authors. 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 15import abc 16import dataclasses 17import logging 18from typing import Any, Dict, List, Optional, Tuple 19 20from google.rpc import code_pb2 21import tenacity 22 23from framework.infrastructure import gcp 24 25logger = logging.getLogger(__name__) 26 27# Type aliases 28GcpResource = gcp.compute.ComputeV1.GcpResource 29 30 31@dataclasses.dataclass(frozen=True) 32class EndpointPolicy: 33 url: str 34 name: str 35 type: str 36 traffic_port_selector: dict 37 endpoint_matcher: dict 38 update_time: str 39 create_time: str 40 http_filters: Optional[dict] = None 41 server_tls_policy: Optional[str] = None 42 43 @classmethod 44 def from_response(cls, name: str, response: Dict[str, 45 Any]) -> 'EndpointPolicy': 46 return cls(name=name, 47 url=response['name'], 48 type=response['type'], 49 server_tls_policy=response.get('serverTlsPolicy', None), 50 traffic_port_selector=response['trafficPortSelector'], 51 endpoint_matcher=response['endpointMatcher'], 52 http_filters=response.get('httpFilters', None), 53 update_time=response['updateTime'], 54 create_time=response['createTime']) 55 56 57@dataclasses.dataclass(frozen=True) 58class Mesh: 59 60 name: str 61 url: str 62 routes: Optional[List[str]] 63 64 @classmethod 65 def from_response(cls, name: str, d: Dict[str, Any]) -> 'Mesh': 66 return cls( 67 name=name, 68 url=d["name"], 69 routes=list(d["routes"]) if "routes" in d else None, 70 ) 71 72 73@dataclasses.dataclass(frozen=True) 74class GrpcRoute: 75 76 @dataclasses.dataclass(frozen=True) 77 class MethodMatch: 78 type: Optional[str] 79 grpc_service: Optional[str] 80 grpc_method: Optional[str] 81 case_sensitive: Optional[bool] 82 83 @classmethod 84 def from_response(cls, d: Dict[str, Any]) -> 'GrpcRoute.MethodMatch': 85 return cls( 86 type=d.get("type"), 87 grpc_service=d.get("grpcService"), 88 grpc_method=d.get("grpcMethod"), 89 case_sensitive=d.get("caseSensitive"), 90 ) 91 92 @dataclasses.dataclass(frozen=True) 93 class HeaderMatch: 94 type: Optional[str] 95 key: str 96 value: str 97 98 @classmethod 99 def from_response(cls, d: Dict[str, Any]) -> 'GrpcRoute.HeaderMatch': 100 return cls( 101 type=d.get("type"), 102 key=d["key"], 103 value=d["value"], 104 ) 105 106 @dataclasses.dataclass(frozen=True) 107 class RouteMatch: 108 method: Optional['GrpcRoute.MethodMatch'] 109 headers: Tuple['GrpcRoute.HeaderMatch'] 110 111 @classmethod 112 def from_response(cls, d: Dict[str, Any]) -> 'GrpcRoute.RouteMatch': 113 return cls( 114 method=GrpcRoute.MethodMatch.from_response(d["method"]) 115 if "method" in d else None, 116 headers=tuple( 117 GrpcRoute.HeaderMatch.from_response(h) 118 for h in d["headers"]) if "headers" in d else (), 119 ) 120 121 @dataclasses.dataclass(frozen=True) 122 class Destination: 123 service_name: str 124 weight: Optional[int] 125 126 @classmethod 127 def from_response(cls, d: Dict[str, Any]) -> 'GrpcRoute.Destination': 128 return cls( 129 service_name=d["serviceName"], 130 weight=d.get("weight"), 131 ) 132 133 @dataclasses.dataclass(frozen=True) 134 class RouteAction: 135 136 @classmethod 137 def from_response(cls, d: Dict[str, Any]) -> 'GrpcRoute.RouteAction': 138 destinations = [ 139 GrpcRoute.Destination.from_response(dest) 140 for dest in d["destinations"] 141 ] if "destinations" in d else [] 142 return cls(destinations=destinations) 143 144 @dataclasses.dataclass(frozen=True) 145 class RouteRule: 146 matches: List['GrpcRoute.RouteMatch'] 147 action: 'GrpcRoute.RouteAction' 148 149 @classmethod 150 def from_response(cls, d: Dict[str, Any]) -> 'GrpcRoute.RouteRule': 151 matches = [ 152 GrpcRoute.RouteMatch.from_response(m) for m in d["matches"] 153 ] if "matches" in d else [] 154 return cls( 155 matches=matches, 156 action=GrpcRoute.RouteAction.from_response(d["action"]), 157 ) 158 159 name: str 160 url: str 161 hostnames: Tuple[str] 162 rules: Tuple['GrpcRoute.RouteRule'] 163 meshes: Optional[Tuple[str]] 164 165 @classmethod 166 def from_response(cls, name: str, d: Dict[str, 167 Any]) -> 'GrpcRoute.RouteRule': 168 return cls( 169 name=name, 170 url=d["name"], 171 hostnames=tuple(d["hostnames"]), 172 rules=tuple(d["rules"]), 173 meshes=None if d.get("meshes") is None else tuple(d["meshes"]), 174 ) 175 176 177class _NetworkServicesBase(gcp.api.GcpStandardCloudApiResource, 178 metaclass=abc.ABCMeta): 179 """Base class for NetworkServices APIs.""" 180 181 # TODO(https://github.com/grpc/grpc/issues/29532) remove pylint disable 182 # pylint: disable=abstract-method 183 184 def __init__(self, api_manager: gcp.api.GcpApiManager, project: str): 185 super().__init__(api_manager.networkservices(self.api_version), project) 186 # Shortcut to projects/*/locations/ endpoints 187 self._api_locations = self.api.projects().locations() 188 189 @property 190 def api_name(self) -> str: 191 return 'networkservices' 192 193 def _execute(self, *args, **kwargs): # pylint: disable=signature-differs,arguments-differ 194 # Workaround TD bug: throttled operations are reported as internal. 195 # Ref b/175345578 196 retryer = tenacity.Retrying( 197 retry=tenacity.retry_if_exception(self._operation_internal_error), 198 wait=tenacity.wait_fixed(10), 199 stop=tenacity.stop_after_delay(5 * 60), 200 before_sleep=tenacity.before_sleep_log(logger, logging.DEBUG), 201 reraise=True) 202 retryer(super()._execute, *args, **kwargs) 203 204 @staticmethod 205 def _operation_internal_error(exception): 206 return (isinstance(exception, gcp.api.OperationError) and 207 exception.error.code == code_pb2.INTERNAL) 208 209 210class NetworkServicesV1Beta1(_NetworkServicesBase): 211 """NetworkServices API v1beta1.""" 212 ENDPOINT_POLICIES = 'endpointPolicies' 213 214 @property 215 def api_version(self) -> str: 216 return 'v1beta1' 217 218 def create_endpoint_policy(self, name, body: dict) -> GcpResource: 219 return self._create_resource( 220 collection=self._api_locations.endpointPolicies(), 221 body=body, 222 endpointPolicyId=name) 223 224 def get_endpoint_policy(self, name: str) -> EndpointPolicy: 225 response = self._get_resource( 226 collection=self._api_locations.endpointPolicies(), 227 full_name=self.resource_full_name(name, self.ENDPOINT_POLICIES)) 228 return EndpointPolicy.from_response(name, response) 229 230 def delete_endpoint_policy(self, name: str) -> bool: 231 return self._delete_resource( 232 collection=self._api_locations.endpointPolicies(), 233 full_name=self.resource_full_name(name, self.ENDPOINT_POLICIES)) 234 235 236class NetworkServicesV1Alpha1(NetworkServicesV1Beta1): 237 """NetworkServices API v1alpha1. 238 239 Note: extending v1beta1 class presumes that v1beta1 is just a v1alpha1 API 240 graduated into a more stable version. This is true in most cases. However, 241 v1alpha1 class can always override and reimplement incompatible methods. 242 """ 243 244 GRPC_ROUTES = 'grpcRoutes' 245 MESHES = 'meshes' 246 247 @property 248 def api_version(self) -> str: 249 return 'v1alpha1' 250 251 def create_mesh(self, name: str, body: dict) -> GcpResource: 252 return self._create_resource(collection=self._api_locations.meshes(), 253 body=body, 254 meshId=name) 255 256 def get_mesh(self, name: str) -> Mesh: 257 full_name = self.resource_full_name(name, self.MESHES) 258 result = self._get_resource(collection=self._api_locations.meshes(), 259 full_name=full_name) 260 return Mesh.from_response(name, result) 261 262 def delete_mesh(self, name: str) -> bool: 263 return self._delete_resource(collection=self._api_locations.meshes(), 264 full_name=self.resource_full_name( 265 name, self.MESHES)) 266 267 def create_grpc_route(self, name: str, body: dict) -> GcpResource: 268 return self._create_resource( 269 collection=self._api_locations.grpcRoutes(), 270 body=body, 271 grpcRouteId=name) 272 273 def get_grpc_route(self, name: str) -> GrpcRoute: 274 full_name = self.resource_full_name(name, self.GRPC_ROUTES) 275 result = self._get_resource(collection=self._api_locations.grpcRoutes(), 276 full_name=full_name) 277 return GrpcRoute.from_response(name, result) 278 279 def delete_grpc_route(self, name: str) -> bool: 280 return self._delete_resource( 281 collection=self._api_locations.grpcRoutes(), 282 full_name=self.resource_full_name(name, self.GRPC_ROUTES)) 283