18 from typing
import Dict, Optional
29 logger = logging.getLogger(__name__)
34 timedelta = datetime.timedelta
38 return '\n'.join(f
'{k}="{v}"' for k, v
in query.items())
42 return ';'.join(f
'{k}={_logs_explorer_quote(v)}' for k, v
in req.items())
46 return urllib.parse.quote_plus(value, safe=
':')
50 """Error running app"""
54 TEMPLATE_DIR_NAME =
'kubernetes-manifests'
55 TEMPLATE_DIR_RELATIVE_PATH = f
'../../{TEMPLATE_DIR_NAME}'
56 ROLE_WORKLOAD_IDENTITY_USER =
'roles/iam.workloadIdentityUser'
60 namespace_template=None,
61 reuse_namespace=False):
65 self.k8s_namespace: k8s.KubernetesNamespace = k8s_namespace
70 self.
namespace: Optional[k8s.V1Namespace] =
None
72 def run(self, **kwargs):
87 template = mako.template.Template(filename=
str(template_file))
88 return template.render(**kwargs)
92 with open(yaml_file)
as f:
93 with contextlib.closing(yaml.safe_load_all(f))
as yml:
99 with contextlib.closing(yaml.safe_load_all(document))
as yml:
105 templates_path = (pathlib.Path(__file__).parent /
107 return templates_path.joinpath(template_name).resolve()
111 logger.debug(
"Loading k8s manifest template: %s", template_file)
118 manifest =
next(manifests)
120 if next(manifests,
False):
121 raise RunnerError(
'Exactly one document expected in manifest '
123 k8s_objects = self.k8s_namespace.apply_manifest(manifest)
124 if len(k8s_objects) != 1:
125 raise RunnerError(
'Expected exactly one object must created from '
126 f
'manifest {template_file}')
128 logger.info(
'%s %s created', k8s_objects[0].kind,
129 k8s_objects[0].metadata.name)
130 return k8s_objects[0]
133 deployment = self.k8s_namespace.get_deployment(deployment_name)
138 service = self.k8s_namespace.get_service(service_name)
143 return self.k8s_namespace.
get()
147 if not isinstance(namespace, k8s.V1Namespace):
148 raise RunnerError(
'Expected V1Namespace to be created '
149 f
'from manifest {template}')
150 if namespace.metadata.name != kwargs[
'namespace_name']:
151 raise RunnerError(
'V1Namespace created with unexpected name: '
152 f
'{namespace.metadata.name}')
153 logger.debug(
'V1Namespace %s created at %s',
154 namespace.metadata.self_link,
155 namespace.metadata.creation_timestamp)
160 service_account_name):
162 Returns workload identity member name used to authenticate Kubernetes
165 https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity
167 return (f
'serviceAccount:{project}.svc.id.goog'
168 f
'[{namespace_name}/{service_account_name}]')
171 service_account_name):
173 gcp_iam.project, self.k8s_namespace.name, service_account_name)
174 logger.info(
'Granting %s to %s for GCP Service Account %s',
178 gcp_iam.add_service_account_iam_policy_binding(
180 workload_identity_member)
183 service_account_name):
185 gcp_iam.project, self.k8s_namespace.name, service_account_name)
186 logger.info(
'Revoking %s from %s for GCP Service Account %s',
190 gcp_iam.remove_service_account_iam_policy_binding(
192 workload_identity_member)
193 except gcp.api.Error
as error:
194 logger.warning(
'Failed %s from %s for Service Account %s: %r',
196 workload_identity_member, gcp_service_account, error)
199 **kwargs) -> k8s.V1ServiceAccount:
201 if not isinstance(resource, k8s.V1ServiceAccount):
202 raise RunnerError(
'Expected V1ServiceAccount to be created '
203 f
'from manifest {template}')
204 if resource.metadata.name != kwargs[
'service_account_name']:
205 raise RunnerError(
'V1ServiceAccount created with unexpected name: '
206 f
'{resource.metadata.name}')
207 logger.debug(
'V1ServiceAccount %s created at %s',
208 resource.metadata.self_link,
209 resource.metadata.creation_timestamp)
214 if not isinstance(deployment, k8s.V1Deployment):
215 raise RunnerError(
'Expected V1Deployment to be created '
216 f
'from manifest {template}')
217 if deployment.metadata.name != kwargs[
'deployment_name']:
218 raise RunnerError(
'V1Deployment created with unexpected name: '
219 f
'{deployment.metadata.name}')
220 logger.debug(
'V1Deployment %s created at %s',
221 deployment.metadata.self_link,
222 deployment.metadata.creation_timestamp)
227 if not isinstance(service, k8s.V1Service):
228 raise RunnerError(
'Expected V1Service to be created '
229 f
'from manifest {template}')
230 if service.metadata.name != kwargs[
'service_name']:
231 raise RunnerError(
'V1Service created with unexpected name: '
232 f
'{service.metadata.name}')
233 logger.debug(
'V1Service %s created at %s', service.metadata.self_link,
234 service.metadata.creation_timestamp)
238 logger.info(
'Deleting deployment %s', name)
240 self.k8s_namespace.delete_deployment(name)
241 except k8s.ApiException
as e:
242 logger.info(
'Deployment %s deletion failed, error: %s %s', name,
246 if wait_for_deletion:
247 self.k8s_namespace.wait_for_deployment_deleted(name)
248 logger.debug(
'Deployment %s deleted', name)
251 logger.info(
'Deleting service %s', name)
253 self.k8s_namespace.delete_service(name)
254 except k8s.ApiException
as e:
255 logger.info(
'Service %s deletion failed, error: %s %s', name,
259 if wait_for_deletion:
260 self.k8s_namespace.wait_for_service_deleted(name)
261 logger.debug(
'Service %s deleted', name)
264 logger.info(
'Deleting service account %s', name)
266 self.k8s_namespace.delete_service_account(name)
267 except k8s.ApiException
as e:
268 logger.info(
'Service account %s deletion failed, error: %s %s',
269 name, e.status, e.reason)
272 if wait_for_deletion:
273 self.k8s_namespace.wait_for_service_account_deleted(name)
274 logger.debug(
'Service account %s deleted', name)
277 logger.info(
'Deleting namespace %s', self.k8s_namespace.name)
279 self.k8s_namespace.
delete()
280 except k8s.ApiException
as e:
281 logger.info(
'Namespace %s deletion failed, error: %s %s',
282 self.k8s_namespace.name, e.status, e.reason)
285 if wait_for_deletion:
286 self.k8s_namespace.wait_for_namespace_deleted()
287 logger.debug(
'Namespace %s deleted', self.k8s_namespace.name)
290 logger.info(
'Waiting for deployment %s to have %s available replica(s)',
292 self.k8s_namespace.wait_for_deployment_available_replicas(
293 name, count, **kwargs)
294 deployment = self.k8s_namespace.get_deployment(name)
295 logger.info(
'Deployment %s has %i replicas available',
296 deployment.metadata.name,
297 deployment.status.available_replicas)
300 logger.info(
'Waiting for pod %s to start', name)
301 self.k8s_namespace.wait_for_pod_started(name, **kwargs)
302 pod = self.k8s_namespace.get_pod(name)
303 logger.info(
'Pod %s ready, IP: %s', pod.metadata.name,
307 logger.info(
'Waiting for NEG for service %s', name)
308 self.k8s_namespace.wait_for_service_neg(name, **kwargs)
309 neg_name, neg_zones = self.k8s_namespace.get_service_neg(
311 logger.info(
"Service %s: detected NEG=%s in zones=%s", name, neg_name,
316 deployment_name: str,
320 end_delta: Optional[timedelta] =
None) ->
None:
321 """Output the link to test server/client logs in GCP Logs Explorer."""
322 if end_delta
is None:
325 time_now = _helper_datetime.iso8601_utc_time()
326 time_end = _helper_datetime.iso8601_utc_time(end_delta)
328 'resource.type':
'k8s_container',
329 'resource.labels.project_id': gcp_project,
330 'resource.labels.container_name': deployment_name,
331 'resource.labels.namespace_name': namespace_name,
335 'timeRange': f
'{time_now}/{time_end}',
338 link = f
'https://{gcp_ui_url}/logs/query;{req}?project={gcp_project}'
340 logger.info(
"GCP Logs Explorer link to %s:\n%s ", deployment_name, link)
345 """A helper to make consistent test app kubernetes namespace name
346 for given resource prefix and suffix."""
347 parts = [resource_prefix, name]
350 parts.append(resource_suffix)
351 return '-'.join(parts)