1# Copyright 2022 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.
14import datetime
15import logging
16from typing import List
17
18from absl import flags
19from absl.testing import absltest
20from absl.testing import parameterized
21
22from framework import bootstrap_generator_testcase
23from framework import xds_k8s_testcase
24from framework.helpers import retryers
25from framework.test_app.runners.k8s import k8s_xds_client_runner
26from framework.test_app.runners.k8s import k8s_xds_server_runner
27
28logger = logging.getLogger(__name__)
29flags.adopt_module_key_flags(xds_k8s_testcase)
30
31# Type aliases
32XdsTestServer = xds_k8s_testcase.XdsTestServer
33XdsTestClient = xds_k8s_testcase.XdsTestClient
34KubernetesServerRunner = k8s_xds_server_runner.KubernetesServerRunner
35KubernetesClientRunner = k8s_xds_client_runner.KubernetesClientRunner
36_timedelta = datetime.timedelta
37
38
39# Returns a list of bootstrap generator versions to be tested along with their
40# image names.
41#
42# Whenever we release a new version of the bootstrap generator, we need to add a
43# corresponding entry here.
44#
45# TODO: Update bootstrap generator release instructions to add an entry here,
46# after the release is published.
47def bootstrap_version_testcases() -> List:
48    return (
49        dict(
50            version='v0.14.0',
51            image=
52            'gcr.io/grpc-testing/td-grpc-bootstrap:d6baaf7b0e0c63054ac4d9bedc09021ff261d599'
53        ),
54        dict(
55            version='v0.13.0',
56            image=
57            'gcr.io/grpc-testing/td-grpc-bootstrap:203db6ce70452996f4183c30dd4c5ecaada168b0'
58        ),
59        dict(
60            version='v0.12.0',
61            image=
62            'gcr.io/grpc-testing/td-grpc-bootstrap:8765051ef3b742bc5cd20f16de078ae7547f2ba2'
63        ),
64        dict(
65            version='v0.11.0',
66            image=
67            'gcr.io/grpc-testing/td-grpc-bootstrap:b96f7a73314668aee83cbf86ab1e40135a0542fc'
68        ),
69        # v0.10.0 uses v2 xDS transport protocol by default. TD only supports v3
70        # and we can force the bootstrap generator to emit config with v3
71        # support by setting the --include-v3-features-experimental flag to
72        # true.
73        #
74        # TODO: Figure out how to pass flags to the bootstrap generator via the
75        # client and server runners, and uncomment this version.
76        # ('v0.10.0', 'gcr.io/grpc-testing/td-grpc-bootstrap:66de7ea0e170351c9fae17232b81adbfb3e80ec3'),
77    )
78
79
80# TODO: Reuse service account and namespaces for significant improvements in
81# running time.
82class BootstrapGeneratorClientTest(
83        bootstrap_generator_testcase.BootstrapGeneratorBaseTest,
84        parameterized.TestCase):
85    client_runner: KubernetesClientRunner
86    server_runner: KubernetesServerRunner
87    test_server: XdsTestServer
88
89    @classmethod
90    def setUpClass(cls):
91        """Hook method for setting up class fixture before running tests in
92        the class.
93        """
94        super().setUpClass()
95
96        # For client tests, we use a single server instance that can be shared
97        # across all the parameterized clients. And this server runner will use
98        # the version of the bootstrap generator as configured via the
99        # --td_bootstrap_image flag.
100        cls.server_runner = cls.initKubernetesServerRunner()
101        cls.test_server = cls.startTestServer(
102            server_runner=cls.server_runner,
103            port=cls.server_port,
104            maintenance_port=cls.server_maintenance_port,
105            xds_host=cls.server_xds_host,
106            xds_port=cls.server_xds_port)
107
108        # Load backends.
109        neg_name, neg_zones = cls.server_runner.k8s_namespace.get_service_neg(
110            cls.server_runner.service_name, cls.server_port)
111
112        # Add backends to the Backend Service.
113        cls.td.backend_service_add_neg_backends(neg_name, neg_zones)
114        cls.td.wait_for_backends_healthy_status()
115
116    @classmethod
117    def tearDownClass(cls):
118        # Remove backends from the Backend Service before closing the server
119        # runner.
120        neg_name, neg_zones = cls.server_runner.k8s_namespace.get_service_neg(
121            cls.server_runner.service_name, cls.server_port)
122        cls.td.backend_service_remove_neg_backends(neg_name, neg_zones)
123        cls.server_runner.cleanup(force=cls.force_cleanup)
124        super().tearDownClass()
125
126    def tearDown(self):
127        logger.info('----- TestMethod %s teardown -----', self.id())
128        retryer = retryers.constant_retryer(wait_fixed=_timedelta(seconds=10),
129                                            attempts=3,
130                                            log_level=logging.INFO)
131        try:
132            retryer(self._cleanup)
133        except retryers.RetryError:
134            logger.exception('Got error during teardown')
135        super().tearDown()
136
137    def _cleanup(self):
138        self.client_runner.cleanup(force=self.force_cleanup)
139
140    @parameterized.parameters(
141        (t["version"], t["image"]) for t in bootstrap_version_testcases())
142    def test_baseline_in_client_with_bootstrap_version(self, version, image):
143        """Runs the baseline test for multiple versions of the bootstrap
144        generator on the client.
145        """
146        logger.info('----- testing bootstrap generator version %s -----',
147                    version)
148        self.client_runner = self.initKubernetesClientRunner(
149            td_bootstrap_image=image)
150        test_client: XdsTestClient = self.startTestClient(self.test_server)
151        self.assertXdsConfigExists(test_client)
152        self.assertSuccessfulRpcs(test_client)
153
154
155# TODO: Use unique client and server deployment names while creating the
156# corresponding runners, by suffixing the version of the bootstrap generator
157# being tested. Then, run these in parallel.
158class BootstrapGeneratorServerTest(
159        bootstrap_generator_testcase.BootstrapGeneratorBaseTest,
160        parameterized.TestCase):
161    client_runner: KubernetesClientRunner
162    server_runner: KubernetesServerRunner
163    test_server: XdsTestServer
164
165    def tearDown(self):
166        logger.info('----- TestMethod %s teardown -----', self.id())
167        retryer = retryers.constant_retryer(wait_fixed=_timedelta(seconds=10),
168                                            attempts=3,
169                                            log_level=logging.INFO)
170        try:
171            retryer(self._cleanup)
172        except retryers.RetryError:
173            logger.exception('Got error during teardown')
174        super().tearDown()
175
176    def _cleanup(self):
177        self.client_runner.cleanup(force=self.force_cleanup)
178        self.removeServerBackends()
179        self.server_runner.cleanup(force=self.force_cleanup)
180
181    @parameterized.parameters(
182        (t["version"], t["image"]) for t in bootstrap_version_testcases())
183    def test_baseline_in_server_with_bootstrap_version(self, version, image):
184        """Runs the baseline test for multiple versions of the bootstrap
185        generator on the server.
186        """
187        logger.info('----- Testing bootstrap generator version %s -----',
188                    version)
189        self.server_runner = self.initKubernetesServerRunner(
190            td_bootstrap_image=image)
191        self.test_server = self.startTestServer(
192            server_runner=self.server_runner,
193            port=self.server_port,
194            maintenance_port=self.server_maintenance_port,
195            xds_host=self.server_xds_host,
196            xds_port=self.server_xds_port)
197
198        # Load backends.
199        neg_name, neg_zones = self.server_runner.k8s_namespace.get_service_neg(
200            self.server_runner.service_name, self.server_port)
201
202        # Add backends to the Backend Service.
203        self.td.backend_service_add_neg_backends(neg_name, neg_zones)
204        self.td.wait_for_backends_healthy_status()
205
206        self.client_runner = self.initKubernetesClientRunner()
207        test_client: XdsTestClient = self.startTestClient(self.test_server)
208        self.assertXdsConfigExists(test_client)
209        self.assertSuccessfulRpcs(test_client)
210
211
212if __name__ == '__main__':
213    absltest.main()
214