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