[PSM Interop] Output GCP log links at the end of the test (#33104)

Output example:

```
I0512 21:33:09.879951 140704627381056 xds_k8s_testcase.py:482] ----- TestMethod __main__.BaselineTest.test_traffic_director_grpc_setup teardown -----
I0512 21:33:09.880299 140704627381056 traffic_director.py:582] Deleting Forwarding rule "sergiitk-forwarding-rule-20230513-0428-aolg9"
...
I0512 21:35:53.437206 140704627381056 xds_k8s_testcase.py:491] ----- Test client/server logs -----
I0512 21:35:53.437497 140704627381056 k8s_base_runner.py:491] GCP Logs Explorer link to psm-grpc-client:
https://pantheon.corp.google.com/logs/query;query=resource.type%3D%22k8s_container%22%0Aresource.labels.project_id%3D%22sergiitk-grpc-gke%22%0Aresource.labels.container_name%3D%22psm-grpc-client%22%0Aresource.labels.namespace_name%3D%22sergiitk-client-20230513-0428-aolg9%22;timeRange=2023-05-13T04:30:45.361596Z%2F2023-05-13T04:34:41.227014Z?project=sergiitk-grpc-gke
I0512 21:35:53.437677 140704627381056 k8s_base_runner.py:491] GCP Logs Explorer link to psm-grpc-server:
https://pantheon.corp.google.com/logs/query;query=resource.type%3D%22k8s_container%22%0Aresource.labels.project_id%3D%22sergiitk-grpc-gke%22%0Aresource.labels.container_name%3D%22psm-grpc-server%22%0Aresource.labels.namespace_name%3D%22sergiitk-server-20230513-0428-aolg9%22;timeRange=2023-05-13T04:29:41.171388Z%2F2023-05-13T04:35:53.437068Z?project=sergiitk-grpc-gke
[       OK ] BaselineTest.test_traffic_director_grpc_setup
```
pull/33266/head
Sergii Tkachenko 2 years ago committed by GitHub
parent d1c0dc58cc
commit acbac3deee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      tools/run_tests/xds_k8s_test_driver/bin/lib/common.py
  2. 3
      tools/run_tests/xds_k8s_test_driver/config/common.cfg
  3. 12
      tools/run_tests/xds_k8s_test_driver/framework/helpers/datetime.py
  4. 101
      tools/run_tests/xds_k8s_test_driver/framework/test_app/runners/k8s/k8s_base_runner.py
  5. 129
      tools/run_tests/xds_k8s_test_driver/framework/test_app/runners/k8s/k8s_xds_client_runner.py
  6. 158
      tools/run_tests/xds_k8s_test_driver/framework/test_app/runners/k8s/k8s_xds_server_runner.py
  7. 4
      tools/run_tests/xds_k8s_test_driver/framework/xds_k8s_testcase.py
  8. 4
      tools/run_tests/xds_k8s_test_driver/framework/xds_url_map_testcase.py

@ -86,6 +86,7 @@ def make_server_runner(namespace: k8s.KubernetesNamespace,
deployment_name=xds_flags.SERVER_NAME.value,
image_name=xds_k8s_flags.SERVER_IMAGE.value,
td_bootstrap_image=xds_k8s_flags.TD_BOOTSTRAP_IMAGE.value,
xds_server_uri=xds_flags.XDS_SERVER_URI.value,
gcp_project=xds_flags.PROJECT.value,
gcp_api_manager=gcp_api_manager,
gcp_service_account=xds_k8s_flags.GCP_SERVICE_ACCOUNT.value,
@ -95,9 +96,7 @@ def make_server_runner(namespace: k8s.KubernetesNamespace,
debug_use_port_forwarding=port_forwarding)
if secure:
runner_kwargs.update(
xds_server_uri=xds_flags.XDS_SERVER_URI.value,
deployment_template='server-secure.deployment.yaml')
runner_kwargs['deployment_template'] = 'server-secure.deployment.yaml'
return KubernetesServerRunner(namespace, **runner_kwargs)

@ -11,6 +11,3 @@
--logger_levels=__main__:DEBUG,framework:INFO
--verbosity=0
# Google projects: remove if console.cloud.google.com redirects to Logs Explorer
# ref: https://github.com/grpc/grpc/pull/26844#discussion_r680224772
--gcp_ui_url=pantheon.corp.google.com

@ -14,7 +14,7 @@
"""This contains common helpers for working with dates and time."""
import datetime
import re
from typing import Optional, Pattern
from typing import Pattern
RE_ZERO_OFFSET: Pattern[str] = re.compile(r'[+\-]00:?00$')
@ -29,12 +29,10 @@ def shorten_utc_zone(utc_datetime_str: str) -> str:
return RE_ZERO_OFFSET.sub('Z', utc_datetime_str)
def iso8601_utc_time(timedelta: Optional[datetime.timedelta] = None) -> str:
"""Return datetime relative to current in ISO-8601 format, UTC tz."""
time: datetime.datetime = utc_now()
if timedelta:
time += timedelta
return shorten_utc_zone(time.isoformat())
def iso8601_utc_time(time: datetime.datetime = None) -> str:
"""Converts datetime UTC and formats as ISO-8601 Zulu time."""
utc_time = time.astimezone(tz=datetime.timezone.utc)
return shorten_utc_zone(utc_time.isoformat())
def datetime_suffix(*, seconds: bool = False) -> str:

@ -14,6 +14,7 @@
"""
Common functionality for running xDS Test Client and Server on Kubernetes.
"""
from abc import ABCMeta
import contextlib
import datetime
import logging
@ -37,42 +38,95 @@ logger = logging.getLogger(__name__)
_RunnerError = base_runner.RunnerError
_HighlighterYaml = framework.helpers.highlighter.HighlighterYaml
_helper_datetime = framework.helpers.datetime
_datetime = datetime.datetime
_timedelta = datetime.timedelta
class KubernetesBaseRunner(base_runner.BaseRunner):
class KubernetesBaseRunner(base_runner.BaseRunner, metaclass=ABCMeta):
# Pylint wants abstract classes to override abstract methods.
# pylint: disable=abstract-method
TEMPLATE_DIR_NAME = 'kubernetes-manifests'
TEMPLATE_DIR_RELATIVE_PATH = f'../../../../{TEMPLATE_DIR_NAME}'
ROLE_WORKLOAD_IDENTITY_USER = 'roles/iam.workloadIdentityUser'
pod_port_forwarders: List[k8s.PortForwarder]
pod_log_collectors: List[k8s.PodLogCollector]
# Required fields.
k8s_namespace: k8s.KubernetesNamespace
deployment_name: str
image_name: str
gcp_project: str
gcp_service_account: str
gcp_ui_url: str
# Fields with default values.
namespace_template: str = 'namespace.yaml'
reuse_namespace: bool = False
# Mutable state.
deployment: Optional[k8s.V1Deployment] = None
service_account: Optional[k8s.V1ServiceAccount] = None
time_start_requested: Optional[_datetime] = None
time_start_completed: Optional[_datetime] = None
time_stopped: Optional[_datetime] = None
def __init__(self,
k8s_namespace,
namespace_template=None,
reuse_namespace=False):
k8s_namespace: k8s.KubernetesNamespace,
*,
deployment_name: str,
image_name: str,
gcp_project: str,
gcp_service_account: str,
gcp_ui_url: str,
namespace_template: Optional[str] = 'namespace.yaml',
reuse_namespace: bool = False):
super().__init__()
self._highlighter = _HighlighterYaml()
# Kubernetes namespaced resources manager
self.k8s_namespace: k8s.KubernetesNamespace = k8s_namespace
# Required fields.
self.deployment_name = deployment_name
self.image_name = image_name
self.gcp_project = gcp_project
# Maps GCP service account to Kubernetes service account
self.gcp_service_account = gcp_service_account
self.gcp_ui_url = gcp_ui_url
# Kubernetes namespace resources manager.
self.k8s_namespace = k8s_namespace
if namespace_template:
self.namespace_template = namespace_template
self.reuse_namespace = reuse_namespace
self.namespace_template = namespace_template or 'namespace.yaml'
# Mutable state
self.namespace: Optional[k8s.V1Namespace] = None
self.pod_port_forwarders = []
self.pod_log_collectors = []
# Highlighter.
self._highlighter = _HighlighterYaml()
def run(self, **kwargs):
del kwargs
if self.time_start_requested:
if self.time_start_completed:
raise RuntimeError(
f"Deployment {self.deployment_name}: has already been"
f" started at {self.time_start_completed.isoformat()}")
else:
raise RuntimeError(
f"Deployment {self.deployment_name}: start has already been"
f" requested at {self.time_start_requested.isoformat()}")
self.time_start_requested = _datetime.now()
self.logs_explorer_link()
if self.reuse_namespace:
self.namespace = self._reuse_namespace()
if not self.namespace:
self.namespace = self._create_namespace(
self.namespace_template, namespace_name=self.k8s_namespace.name)
def cleanup(self, *, force=False):
def _cleanup_namespace(self, *, force=False):
if (self.namespace and not self.reuse_namespace) or force:
self.delete_namespace()
self.namespace = None
@ -405,6 +459,19 @@ class KubernetesBaseRunner(base_runner.BaseRunner):
logger.info("Service %s: detected NEG=%s in zones=%s", name, neg_name,
neg_zones)
def logs_explorer_link(self):
if not self.time_start_requested:
logger.warning(
'Skipped printing GCP log link for a non-started deployment %s',
self.deployment_name)
return
self._logs_explorer_link(deployment_name=self.deployment_name,
namespace_name=self.k8s_namespace.name,
gcp_project=self.gcp_project,
gcp_ui_url=self.gcp_ui_url,
start_time=self.time_start_requested,
end_time=self.time_stopped)
@classmethod
def _logs_explorer_link(cls,
*,
@ -412,13 +479,17 @@ class KubernetesBaseRunner(base_runner.BaseRunner):
namespace_name: str,
gcp_project: str,
gcp_ui_url: str,
end_delta: Optional[_timedelta] = None) -> None:
start_time: Optional[_datetime] = None,
end_time: Optional[_datetime] = None):
"""Output the link to test server/client logs in GCP Logs Explorer."""
if end_delta is None:
end_delta = _timedelta(hours=1)
time_now = _helper_datetime.iso8601_utc_time()
time_end = _helper_datetime.iso8601_utc_time(end_delta)
request = {'timeRange': f'{time_now}/{time_end}'}
if not start_time:
start_time = _datetime.now()
if not end_time:
end_time = start_time + _timedelta(minutes=30)
logs_start = _helper_datetime.iso8601_utc_time(start_time)
logs_end = _helper_datetime.iso8601_utc_time(end_time)
request = {'timeRange': f'{logs_start}/{logs_end}'}
query = {
'resource.type': 'k8s_container',
'resource.labels.project_id': gcp_project,

@ -14,7 +14,7 @@
"""
Run xDS Test Client on Kubernetes.
"""
import datetime
import logging
from typing import Optional
@ -28,61 +28,69 @@ logger = logging.getLogger(__name__)
class KubernetesClientRunner(k8s_base_runner.KubernetesBaseRunner):
# Required fields.
xds_server_uri: str
stats_port: int
deployment_template: str
enable_workload_identity: bool
debug_use_port_forwarding: bool
td_bootstrap_image: str
network: str
# Optional fields.
service_account_name: Optional[str] = None
service_account_template: Optional[str] = None
gcp_iam: Optional[gcp.iam.IamV1] = None
def __init__( # pylint: disable=too-many-locals
self,
k8s_namespace,
k8s_namespace: k8s.KubernetesNamespace,
*,
deployment_name,
image_name,
td_bootstrap_image,
deployment_name: str,
image_name: str,
td_bootstrap_image: str,
network='default',
xds_server_uri: Optional[str] = None,
gcp_api_manager: gcp.api.GcpApiManager,
gcp_project: str,
gcp_service_account: str,
xds_server_uri=None,
network='default',
service_account_name=None,
stats_port=8079,
deployment_template='client.deployment.yaml',
service_account_template='service-account.yaml',
reuse_namespace=False,
namespace_template=None,
debug_use_port_forwarding=False,
enable_workload_identity=True):
super().__init__(k8s_namespace, namespace_template, reuse_namespace)
service_account_name: Optional[str] = None,
stats_port: int = 8079,
deployment_template: str = 'client.deployment.yaml',
service_account_template: str = 'service-account.yaml',
reuse_namespace: bool = False,
namespace_template: Optional[str] = None,
debug_use_port_forwarding: bool = False,
enable_workload_identity: bool = True):
super().__init__(k8s_namespace,
deployment_name=deployment_name,
image_name=image_name,
gcp_project=gcp_project,
gcp_service_account=gcp_service_account,
gcp_ui_url=gcp_api_manager.gcp_ui_url,
namespace_template=namespace_template,
reuse_namespace=reuse_namespace)
# Settings
self.deployment_name = deployment_name
self.image_name = image_name
self.stats_port = stats_port
# xDS bootstrap generator
self.td_bootstrap_image = td_bootstrap_image
self.xds_server_uri = xds_server_uri
self.network = network
self.deployment_template = deployment_template
self.debug_use_port_forwarding = debug_use_port_forwarding
self.enable_workload_identity = enable_workload_identity
# Service account settings:
# Kubernetes service account
self.debug_use_port_forwarding = debug_use_port_forwarding
# Used by the TD bootstrap generator.
self.td_bootstrap_image = td_bootstrap_image
self.network = network
self.xds_server_uri = xds_server_uri
# Workload identity settings:
if self.enable_workload_identity:
# Kubernetes service account.
self.service_account_name = service_account_name or deployment_name
self.service_account_template = service_account_template
else:
self.service_account_name = None
self.service_account_template = None
# GCP.
self.gcp_project = gcp_project
self.gcp_ui_url = gcp_api_manager.gcp_ui_url
# GCP service account to map to Kubernetes service account
self.gcp_service_account = gcp_service_account
# GCP IAM API used to grant allow workload service accounts permission
# to use GCP service account identity.
self.gcp_iam = gcp.iam.IamV1(gcp_api_manager, gcp_project)
# Mutable state
self.deployment: Optional[k8s.V1Deployment] = None
self.service_account: Optional[k8s.V1ServiceAccount] = None
# TODO(sergiitk): make rpc UnaryCall enum or get it from proto
# GCP IAM API used to grant allow workload service accounts
# permission to use GCP service account identity.
self.gcp_iam = gcp.iam.IamV1(gcp_api_manager, gcp_project)
def run( # pylint: disable=arguments-differ
self,
*,
@ -99,11 +107,6 @@ class KubernetesClientRunner(k8s_base_runner.KubernetesBaseRunner):
'server_target=%s rpc=%s qps=%s metadata=%r secure_mode=%s '
'print_response=%s', self.deployment_name, self.k8s_namespace.name,
server_target, rpc, qps, metadata, secure_mode, print_response)
self._logs_explorer_link(deployment_name=self.deployment_name,
namespace_name=self.k8s_namespace.name,
gcp_project=self.gcp_project,
gcp_ui_url=self.gcp_ui_url)
super().run()
if self.enable_workload_identity:
@ -148,6 +151,7 @@ class KubernetesClientRunner(k8s_base_runner.KubernetesBaseRunner):
# Verify the deployment reports all pods started as well.
self._wait_deployment_with_available_replicas(self.deployment_name)
self.time_start_completed = datetime.datetime.now()
return self._xds_test_client_for_pod(pod, server_target=server_target)
@ -165,18 +169,25 @@ class KubernetesClientRunner(k8s_base_runner.KubernetesBaseRunner):
hostname=pod.metadata.name,
rpc_host=rpc_host)
def cleanup(self, *, force=False, force_namespace=False): # pylint: disable=arguments-differ
if self.deployment or force:
self._delete_deployment(self.deployment_name)
self.deployment = None
if self.enable_workload_identity and (self.service_account or force):
self._revoke_workload_identity_user(
gcp_iam=self.gcp_iam,
gcp_service_account=self.gcp_service_account,
service_account_name=self.service_account_name)
self._delete_service_account(self.service_account_name)
self.service_account = None
super().cleanup(force=force_namespace and force)
# pylint: disable=arguments-differ
def cleanup(self, *, force=False, force_namespace=False):
try:
if self.deployment or force:
self._delete_deployment(self.deployment_name)
self.deployment = None
if (self.enable_workload_identity and
(self.service_account or force)):
self._revoke_workload_identity_user(
gcp_iam=self.gcp_iam,
gcp_service_account=self.gcp_service_account,
service_account_name=self.service_account_name)
self._delete_service_account(self.service_account_name)
self.service_account = None
self._cleanup_namespace(force=force_namespace and force)
finally:
self.time_stopped = datetime.datetime.now()
# pylint: enable=arguments-differ
@classmethod
def make_namespace_name(cls,

@ -14,6 +14,7 @@
"""
Run xDS Test Client on Kubernetes.
"""
import datetime
import logging
from typing import List, Optional
@ -30,70 +31,82 @@ class KubernetesServerRunner(k8s_base_runner.KubernetesBaseRunner):
DEFAULT_MAINTENANCE_PORT = 8080
DEFAULT_SECURE_MODE_MAINTENANCE_PORT = 8081
# Required fields.
deployment_template: str
service_name: str
service_template: str
reuse_service: bool
enable_workload_identity: bool
debug_use_port_forwarding: bool
gcp_neg_name: str
td_bootstrap_image: str
xds_server_uri: str
network: str
# Optional fields.
service_account_name: Optional[str] = None
service_account_template: Optional[str] = None
gcp_iam: Optional[gcp.iam.IamV1] = None
# Mutable state.
service: Optional[k8s.V1Service] = None
def __init__( # pylint: disable=too-many-locals
self,
k8s_namespace,
k8s_namespace: k8s.KubernetesNamespace,
*,
deployment_name,
image_name,
td_bootstrap_image,
deployment_name: str,
image_name: str,
td_bootstrap_image: str,
network: str = 'default',
xds_server_uri: Optional[str] = None,
gcp_api_manager: gcp.api.GcpApiManager,
gcp_project: str,
gcp_service_account: str,
service_account_name=None,
service_name=None,
neg_name=None,
xds_server_uri=None,
network='default',
deployment_template='server.deployment.yaml',
service_account_template='service-account.yaml',
service_template='server.service.yaml',
reuse_service=False,
reuse_namespace=False,
namespace_template=None,
debug_use_port_forwarding=False,
enable_workload_identity=True):
super().__init__(k8s_namespace, namespace_template, reuse_namespace)
service_account_name: Optional[str] = None,
service_name: Optional[str] = None,
neg_name: Optional[str] = None,
deployment_template: str = 'server.deployment.yaml',
service_account_template: str = 'service-account.yaml',
service_template: str = 'server.service.yaml',
reuse_service: bool = False,
reuse_namespace: bool = False,
namespace_template: Optional[str] = None,
debug_use_port_forwarding: bool = False,
enable_workload_identity: bool = True):
super().__init__(k8s_namespace,
deployment_name=deployment_name,
image_name=image_name,
gcp_project=gcp_project,
gcp_service_account=gcp_service_account,
gcp_ui_url=gcp_api_manager.gcp_ui_url,
namespace_template=namespace_template,
reuse_namespace=reuse_namespace)
# Settings
self.deployment_name = deployment_name
self.image_name = image_name
self.service_name = service_name or deployment_name
# xDS bootstrap generator
self.td_bootstrap_image = td_bootstrap_image
self.xds_server_uri = xds_server_uri
# This only works in k8s >= 1.18.10-gke.600
# https://cloud.google.com/kubernetes-engine/docs/how-to/standalone-neg#naming_negs
self.neg_name = neg_name or (f'{self.k8s_namespace.name}-'
f'{self.service_name}')
self.network = network
self.deployment_template = deployment_template
self.service_name = service_name or deployment_name
self.service_template = service_template
self.reuse_service = reuse_service
self.debug_use_port_forwarding = debug_use_port_forwarding
self.enable_workload_identity = enable_workload_identity
# Service account settings:
# Kubernetes service account
self.debug_use_port_forwarding = debug_use_port_forwarding
# GCP Network Endpoint Group.
self.gcp_neg_name = neg_name or (f'{self.k8s_namespace.name}-'
f'{self.service_name}')
# Used by the TD bootstrap generator.
self.td_bootstrap_image = td_bootstrap_image
self.network = network
self.xds_server_uri = xds_server_uri
# Workload identity settings:
if self.enable_workload_identity:
# Kubernetes service account.
self.service_account_name = service_account_name or deployment_name
self.service_account_template = service_account_template
else:
self.service_account_name = None
self.service_account_template = None
# GCP.
self.gcp_project = gcp_project
self.gcp_ui_url = gcp_api_manager.gcp_ui_url
# GCP service account to map to Kubernetes service account
self.gcp_service_account = gcp_service_account
# GCP IAM API used to grant allow workload service accounts permission
# to use GCP service account identity.
self.gcp_iam = gcp.iam.IamV1(gcp_api_manager, gcp_project)
# Mutable state
self.deployment: Optional[k8s.V1Deployment] = None
self.service_account: Optional[k8s.V1ServiceAccount] = None
self.service: Optional[k8s.V1Service] = None
# GCP IAM API used to grant allow workload service accounts
# permission to use GCP service account identity.
self.gcp_iam = gcp.iam.IamV1(gcp_api_manager, gcp_project)
def run( # pylint: disable=arguments-differ,too-many-branches
self,
@ -126,12 +139,6 @@ class KubernetesServerRunner(k8s_base_runner.KubernetesBaseRunner):
'maintenance_port=%s secure_mode=%s replica_count=%s',
self.deployment_name, self.k8s_namespace.name, test_port,
maintenance_port, secure_mode, replica_count)
self._logs_explorer_link(deployment_name=self.deployment_name,
namespace_name=self.k8s_namespace.name,
gcp_project=self.gcp_project,
gcp_ui_url=self.gcp_ui_url)
# Create namespace.
super().run()
# Reuse existing if requested, create a new deployment when missing.
@ -144,7 +151,7 @@ class KubernetesServerRunner(k8s_base_runner.KubernetesBaseRunner):
service_name=self.service_name,
namespace_name=self.k8s_namespace.name,
deployment_name=self.deployment_name,
neg_name=self.neg_name,
neg_name=self.gcp_neg_name,
test_port=test_port)
self._wait_service_neg(self.service_name, test_port)
@ -190,6 +197,8 @@ class KubernetesServerRunner(k8s_base_runner.KubernetesBaseRunner):
# Verify the deployment reports all pods started as well.
self._wait_deployment_with_available_replicas(self.deployment_name,
replica_count)
self.time_start_completed = datetime.datetime.now()
servers: List[XdsTestServer] = []
for pod in pods:
servers.append(
@ -228,21 +237,28 @@ class KubernetesServerRunner(k8s_base_runner.KubernetesBaseRunner):
secure_mode=secure_mode,
rpc_host=rpc_host)
def cleanup(self, *, force=False, force_namespace=False): # pylint: disable=arguments-differ
if self.deployment or force:
self._delete_deployment(self.deployment_name)
self.deployment = None
if (self.service and not self.reuse_service) or force:
self._delete_service(self.service_name)
self.service = None
if self.enable_workload_identity and (self.service_account or force):
self._revoke_workload_identity_user(
gcp_iam=self.gcp_iam,
gcp_service_account=self.gcp_service_account,
service_account_name=self.service_account_name)
self._delete_service_account(self.service_account_name)
self.service_account = None
super().cleanup(force=(force_namespace and force))
# pylint: disable=arguments-differ
def cleanup(self, *, force=False, force_namespace=False):
try:
if self.deployment or force:
self._delete_deployment(self.deployment_name)
self.deployment = None
if (self.service and not self.reuse_service) or force:
self._delete_service(self.service_name)
self.service = None
if (self.enable_workload_identity and
(self.service_account or force)):
self._revoke_workload_identity_user(
gcp_iam=self.gcp_iam,
gcp_service_account=self.gcp_service_account,
service_account_name=self.service_account_name)
self._delete_service_account(self.service_account_name)
self.service_account = None
self._cleanup_namespace(force=(force_namespace and force))
finally:
self.time_stopped = datetime.datetime.now()
# pylint: enable=arguments-differ
@classmethod
def make_namespace_name(cls,

@ -511,6 +511,10 @@ class IsolatedXdsKubernetesTestCase(XdsKubernetesBaseTestCase,
except retryers.RetryError:
logger.exception('Got error during teardown')
finally:
logger.info('----- Test client/server logs -----')
self.client_runner.logs_explorer_link()
self.server_runner.logs_explorer_link()
# Fail if any of the pods restarted.
self.assertEqual(
client_restarts,

@ -393,6 +393,10 @@ class XdsUrlMapTestCase(absltest.TestCase, metaclass=_MetaXdsUrlMapTestCase):
except retryers.RetryError:
logging.exception('Got error during teardown')
finally:
if hasattr(cls, 'test_client_runner') and cls.test_client_runner:
logging.info('----- Test client logs -----')
cls.test_client_runner.logs_explorer_link()
# Fail if any of the pods restarted.
error_msg = (
'Client pods unexpectedly restarted'

Loading…
Cancel
Save