xds-k8s: grant roles/iam.workloadIdentityUser automatically (#26487)

pull/25960/head
Sergii Tkachenko 4 years ago committed by GitHub
parent b53f60d353
commit 433c5ea261
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 68
      tools/run_tests/xds_k8s_test_driver/README.md
  2. 9
      tools/run_tests/xds_k8s_test_driver/bin/run_test_client.py
  3. 11
      tools/run_tests/xds_k8s_test_driver/bin/run_test_server.py
  4. 2
      tools/run_tests/xds_k8s_test_driver/config/grpc-testing.cfg
  5. 1
      tools/run_tests/xds_k8s_test_driver/framework/infrastructure/gcp/__init__.py
  6. 14
      tools/run_tests/xds_k8s_test_driver/framework/infrastructure/gcp/api.py
  7. 311
      tools/run_tests/xds_k8s_test_driver/framework/infrastructure/gcp/iam.py
  8. 42
      tools/run_tests/xds_k8s_test_driver/framework/test_app/base_runner.py
  9. 28
      tools/run_tests/xds_k8s_test_driver/framework/test_app/client_app.py
  10. 30
      tools/run_tests/xds_k8s_test_driver/framework/test_app/server_app.py
  11. 16
      tools/run_tests/xds_k8s_test_driver/framework/xds_k8s_testcase.py

@ -14,7 +14,7 @@ changes to this codebase at the moment.
- [ ] Make framework.infrastructure.gcp resources [first-class
citizen](https://en.wikipedia.org/wiki/First-class_citizen), support
simpler CRUD
- [ ] Security: manage `roles/iam.workloadIdentityUser` role grant lifecycle for
- [x] Security: manage `roles/iam.workloadIdentityUser` role grant lifecycle for
dynamically-named namespaces
- [ ] Restructure `framework.test_app` and `framework.xds_k8s*` into a module
containing xDS-interop-specific logic
@ -44,13 +44,18 @@ Pre-populate environment variables for convenience. To find project id, refer to
```shell
export PROJECT_ID="your-project-id"
export PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)")
# Compute Engine default service account
export GCE_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
# The prefix to name GCP resources used by the framework
export RESOURCE_PREFIX="xds-k8s-interop-tests"
# The zone name your cluster, f.e. xds-k8s-test-cluster
export CLUSTER_NAME="xds-k8s-test-cluster"
export CLUSTER_NAME="${RESOURCE_PREFIX}-cluster"
# The zone of your cluster, f.e. us-central1-a
export ZONE="us-central1-a"
# K8S namespace you'll use to run the cluster, f.e.
export K8S_NAMESPACE="interop-psm-security"
export ZONE="us-central1-a"
# Dedicated GCP Service Account to use with workload identity.
export WORKLOAD_SA_NAME="${RESOURCE_PREFIX}"
export WORKLOAD_SA_EMAIL="${WORKLOAD_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
```
##### Create the cluster
@ -70,23 +75,58 @@ Allow [health checking mechanisms](https://cloud.google.com/traffic-director/doc
to query the workloads health.
This step can be skipped, if the driver is executed with `--ensure_firewall`.
```shell
gcloud compute firewall-rules create "${K8S_NAMESPACE}-allow-health-checks" \
gcloud compute firewall-rules create "${RESOURCE_PREFIX}-allow-health-checks" \
--network=default --action=allow --direction=INGRESS \
--source-ranges="35.191.0.0/16,130.211.0.0/22" \
--target-tags=allow-health-checks \
--rules=tcp:8080-8100
```
##### Allow workload identities to talk to Traffic Director APIs
##### Setup GCP Service Account
Create dedicated GCP Service Account to use
with [workload identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity).
```shell
gcloud iam service-accounts add-iam-policy-binding "${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:${PROJECT_ID}.svc.id.goog[${K8S_NAMESPACE}/psm-grpc-client]"
gcloud iam service-accounts create "${WORKLOAD_SA_NAME}" \
--display-name="xDS K8S Interop Tests Workload Identity Service Account"
```
gcloud iam service-accounts add-iam-policy-binding "${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:${PROJECT_ID}.svc.id.goog[${K8S_NAMESPACE}/psm-grpc-server]"
```
Enable the service account to [access the Traffic Director API](https://cloud.google.com/traffic-director/docs/prepare-for-envoy-setup#enable-service-account).
```shell
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${WORKLOAD_SERVICE_ACCOUNT}" \
--role="roles/trafficdirector.client"
```
##### Allow test driver to configure workload identity automatically
Test driver will automatically grant `roles/iam.workloadIdentityUser` to
allow the Kubernetes service account to impersonate the dedicated GCP workload
service account (corresponds to the step 5
of [Authenticating to Google Cloud](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#authenticating_to)).
This action requires the test framework to have `iam.serviceAccounts.create`
permission on the project.
If you're running test framework locally, and you have `roles/owner` to your
project, **you can skip this step**.
If you're configuring the test framework to run on a CI: use `roles/owner`
account once to allow test framework to grant `roles/iam.workloadIdentityUser`.
```shell
# Assuming CI is using Compute Engine default service account.
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${GCE_SA}" \
--role="roles/iam.serviceAccountAdmin" \
--condition-from-file=<(cat <<-END
---
title: allow_workload_identity_only
description: Restrict serviceAccountAdmin to granting role iam.workloadIdentityUser
expression: |-
api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', [])
.hasOnly(['roles/iam.workloadIdentityUser'])
END
)
```
##### Configure GKE cluster access
```shell

@ -18,6 +18,7 @@ from absl import flags
from framework import xds_flags
from framework import xds_k8s_flags
from framework.infrastructure import gcp
from framework.infrastructure import k8s
from framework.test_app import client_app
@ -49,6 +50,10 @@ def main(argv):
if len(argv) > 1:
raise app.UsageError('Too many command-line arguments.')
# Flag shortcuts.
project: str = xds_flags.PROJECT.value
# GCP Service Account email
gcp_service_account: str = xds_k8s_flags.GCP_SERVICE_ACCOUNT.value
# Base namespace
namespace = xds_flags.NAMESPACE.value
client_namespace = namespace
@ -56,8 +61,10 @@ def main(argv):
runner_kwargs = dict(
deployment_name=xds_flags.CLIENT_NAME.value,
image_name=xds_k8s_flags.CLIENT_IMAGE.value,
gcp_service_account=xds_k8s_flags.GCP_SERVICE_ACCOUNT.value,
td_bootstrap_image=xds_k8s_flags.TD_BOOTSTRAP_IMAGE.value,
gcp_project=project,
gcp_api_manager=gcp.api.GcpApiManager(),
gcp_service_account=gcp_service_account,
xds_server_uri=xds_flags.XDS_SERVER_URI.value,
network=xds_flags.NETWORK.value,
stats_port=xds_flags.CLIENT_PORT.value,

@ -18,6 +18,7 @@ from absl import flags
from framework import xds_flags
from framework import xds_k8s_flags
from framework.infrastructure import gcp
from framework.infrastructure import k8s
from framework.test_app import server_app
@ -45,6 +46,10 @@ def main(argv):
if len(argv) > 1:
raise app.UsageError('Too many command-line arguments.')
# Flag shortcuts.
project: str = xds_flags.PROJECT.value
# GCP Service Account email
gcp_service_account: str = xds_k8s_flags.GCP_SERVICE_ACCOUNT.value
# Base namespace
namespace = xds_flags.NAMESPACE.value
server_namespace = namespace
@ -52,13 +57,15 @@ def main(argv):
runner_kwargs = dict(
deployment_name=xds_flags.SERVER_NAME.value,
image_name=xds_k8s_flags.SERVER_IMAGE.value,
gcp_service_account=xds_k8s_flags.GCP_SERVICE_ACCOUNT.value,
td_bootstrap_image=xds_k8s_flags.TD_BOOTSTRAP_IMAGE.value,
gcp_project=project,
gcp_api_manager=gcp.api.GcpApiManager(),
gcp_service_account=gcp_service_account,
network=xds_flags.NETWORK.value,
reuse_namespace=_REUSE_NAMESPACE.value)
if _SECURE.value:
runner_kwargs.update(
td_bootstrap_image=xds_k8s_flags.TD_BOOTSTRAP_IMAGE.value,
xds_server_uri=xds_flags.XDS_SERVER_URI.value,
deployment_template='server-secure.deployment.yaml')

@ -1,5 +1,5 @@
--flagfile=config/common.cfg
--project=grpc-testing
--network=default-vpc
--gcp_service_account=830293263384-compute@developer.gserviceaccount.com
--gcp_service_account=xds-k8s-interop-tests@grpc-testing.iam.gserviceaccount.com
--private_api_key_secret_name=projects/830293263384/secrets/xds-interop-tests-private-api-access-key

@ -13,5 +13,6 @@
# limitations under the License.
from framework.infrastructure.gcp import api
from framework.infrastructure.gcp import compute
from framework.infrastructure.gcp import iam
from framework.infrastructure.gcp import network_security
from framework.infrastructure.gcp import network_services

@ -139,6 +139,20 @@ class GcpApiManager:
raise NotImplementedError(f'Secret Manager {version} not supported')
@functools.lru_cache(None)
def iam(self, version: str) -> discovery.Resource:
"""Identity and Access Management (IAM) API.
https://cloud.google.com/iam/docs/reference/rest
https://googleapis.github.io/google-api-python-client/docs/dyn/iam_v1.html
"""
api_name = 'iam'
if version == 'v1':
return self._build_from_discovery_v1(api_name, version)
raise NotImplementedError(
f'Identity and Access Management (IAM) {version} not supported')
def _build_from_discovery_v1(self, api_name, version):
api = discovery.build(api_name,
version,

@ -0,0 +1,311 @@
# Copyright 2021 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import dataclasses
import datetime
import functools
import logging
from typing import Any, Dict, FrozenSet, Optional
import googleapiclient.errors
from framework.helpers import retryers
from framework.infrastructure import gcp
logger = logging.getLogger(__name__)
# Type aliases
_timedelta = datetime.timedelta
class EtagConflict(gcp.api.Error):
"""
Indicates concurrent policy changes.
https://cloud.google.com/iam/docs/policies#etag
"""
pass
def handle_etag_conflict(func):
def wrap_retry_on_etag_conflict(*args, **kwargs):
retryer = retryers.exponential_retryer_with_timeout(
retry_on_exceptions=(EtagConflict,),
wait_min=_timedelta(seconds=1),
wait_max=_timedelta(seconds=10),
timeout=_timedelta(minutes=2))
return retryer(func, *args, **kwargs)
return wrap_retry_on_etag_conflict
def _replace_binding(policy: 'Policy', binding: 'Policy.Binding',
new_binding: 'Policy.Binding') -> 'Policy':
new_bindings = set(policy.bindings)
new_bindings.discard(binding)
new_bindings.add(new_binding)
return dataclasses.replace(policy, bindings=frozenset(new_bindings))
@dataclasses.dataclass(frozen=True)
class ServiceAccount:
"""An IAM service account.
https://cloud.google.com/iam/docs/reference/rest/v1/projects.serviceAccounts
Note: "etag" field is skipped because it's deprecated
"""
name: str
projectId: str
uniqueId: str
email: str
oauth2ClientId: str
displayName: str = ''
description: str = ''
disabled: bool = False
@classmethod
def from_response(cls, response: Dict[str, Any]) -> 'ServiceAccount':
return cls(name=response['name'],
projectId=response['projectId'],
uniqueId=response['uniqueId'],
email=response['email'],
oauth2ClientId=response['oauth2ClientId'],
description=response.get('description', ''),
displayName=response.get('displayName', ''),
disabled=response.get('disabled', False))
def as_dict(self) -> Dict[str, Any]:
return dataclasses.asdict(self)
@dataclasses.dataclass(frozen=True)
class Expr:
"""
Represents a textual expression in the Common Expression Language syntax.
https://cloud.google.com/iam/docs/reference/rest/v1/Expr
"""
expression: str
title: str = ''
description: str = ''
location: str = ''
@classmethod
def from_response(cls, response: Dict[str, Any]) -> 'Expr':
return cls(**response)
def as_dict(self) -> Dict[str, Any]:
return dataclasses.asdict(self)
@dataclasses.dataclass(frozen=True)
class Policy:
"""An Identity and Access Management (IAM) policy, which specifies
access controls for Google Cloud resources.
https://cloud.google.com/iam/docs/reference/rest/v1/Policy
Note: auditConfigs not supported by this implementation.
"""
@dataclasses.dataclass(frozen=True)
class Binding:
"""Policy Binding. Associates members with a role.
https://cloud.google.com/iam/docs/reference/rest/v1/Policy#binding
"""
role: str
members: FrozenSet[str]
condition: Optional[Expr] = None
@classmethod
def from_response(cls, response: Dict[str, Any]) -> 'Policy.Binding':
fields = {
'role': response['role'],
'members': frozenset(response.get('members', [])),
}
if 'condition' in response:
fields['condition'] = Expr.from_response(response['condition'])
return cls(**fields)
def as_dict(self) -> Dict[str, Any]:
result = {
'role': self.role,
'members': list(self.members),
}
if self.condition is not None:
result['condition'] = self.condition.as_dict()
return result
bindings: FrozenSet[Binding]
etag: str
version: Optional[int] = None
@functools.lru_cache(maxsize=128)
def find_binding_for_role(
self,
role: str,
condition: Optional[Expr] = None) -> Optional['Policy.Binding']:
results = (binding for binding in self.bindings
if binding.role == role and binding.condition == condition)
return next(results, None)
@classmethod
def from_response(cls, response: Dict[str, Any]) -> 'Policy':
bindings = frozenset(
cls.Binding.from_response(b) for b in response.get('bindings', []))
return cls(bindings=bindings,
etag=response['etag'],
version=response.get('version'))
def as_dict(self) -> Dict[str, Any]:
result = {
'bindings': [binding.as_dict() for binding in self.bindings],
'etag': self.etag,
}
if self.version is not None:
result['version'] = self.version
return result
class IamV1(gcp.api.GcpProjectApiResource):
"""
Identity and Access Management (IAM) API.
https://cloud.google.com/iam/docs/reference/rest
"""
_service_accounts: gcp.api.discovery.Resource
# Operations that affect conditional role bindings must specify version 3.
# Otherwise conditions are omitted, and role names returned with a suffix,
# f.e. roles/iam.workloadIdentityUser_withcond_f1ec33c9beb41857dbf0
# https://cloud.google.com/iam/docs/reference/rest/v1/Policy#FIELDS.version
POLICY_VERSION: str = 3
def __init__(self, api_manager: gcp.api.GcpApiManager, project: str):
super().__init__(api_manager.iam('v1'), project)
# Shortcut to projects/*/serviceAccounts/ endpoints
self._service_accounts = self.api.projects().serviceAccounts()
def service_account_resource_name(self, account):
"""
Returns full resource name of the service account.
The resource name of the service account in the following format:
projects/{PROJECT_ID}/serviceAccounts/{ACCOUNT}.
The ACCOUNT value can be the email address or the uniqueId of the
service account.
Ref https://cloud.google.com/iam/docs/reference/rest/v1/projects.serviceAccounts/get
Args:
account: The ACCOUNT value
"""
return f'projects/{self.project}/serviceAccounts/{account}'
def get_service_account(self, account: str) -> ServiceAccount:
response: Dict[str, Any] = self._service_accounts.get(
name=self.service_account_resource_name(account)).execute()
logger.debug('Loaded Service Account:\n%s',
self._resource_pretty_format(response))
return ServiceAccount.from_response(response)
def get_service_account_iam_policy(self, account: str) -> Policy:
response: Dict[str, Any] = self._service_accounts.getIamPolicy(
resource=self.service_account_resource_name(account),
options_requestedPolicyVersion=self.POLICY_VERSION).execute()
logger.debug('Loaded Service Account Policy:\n%s',
self._resource_pretty_format(response))
return Policy.from_response(response)
def set_service_account_iam_policy(self, account: str,
policy: Policy) -> Policy:
"""Sets the IAM policy that is attached to a service account.
https://cloud.google.com/iam/docs/reference/rest/v1/projects.serviceAccounts/setIamPolicy
"""
body = {'policy': policy.as_dict()}
logger.debug('Updating Service Account %s policy:\n%s', account,
self._resource_pretty_format(body))
try:
response: Dict[str, Any] = self._service_accounts.setIamPolicy(
resource=self.service_account_resource_name(account),
body=body).execute()
return Policy.from_response(response)
except googleapiclient.errors.HttpError as error:
# TODO(sergiitk) use status_code() when we upgrade googleapiclient
if error.resp and error.resp.status == 409:
# https://cloud.google.com/iam/docs/policies#etag
logger.debug(error)
raise EtagConflict from error
else:
raise gcp.api.Error from error
@handle_etag_conflict
def add_service_account_iam_policy_binding(self, account: str, role: str,
member: str) -> None:
"""Add an IAM policy binding to an IAM service account.
See for details on updating policy bindings:
https://cloud.google.com/iam/docs/reference/rest/v1/projects.serviceAccounts/setIamPolicy
"""
policy: Policy = self.get_service_account_iam_policy(account)
binding: Optional[Policy.Binding] = policy.find_binding_for_role(role)
if binding and member in binding.members:
logger.debug('Member %s already has role %s for Service Account %s',
member, role, account)
return
if binding is None:
updated_binding = Policy.Binding(role, frozenset([member]))
else:
updated_members: FrozenSet[str] = binding.members.union({member})
updated_binding: Policy.Binding = dataclasses.replace(
binding, members=updated_members)
updated_policy: Policy = _replace_binding(policy, binding,
updated_binding)
self.set_service_account_iam_policy(account, updated_policy)
logger.debug('Role %s granted to member %s for Service Account %s',
role, member, account)
@handle_etag_conflict
def remove_service_account_iam_policy_binding(self, account: str, role: str,
member: str) -> None:
"""Remove an IAM policy binding from the IAM policy of a service
account.
See for details on updating policy bindings:
https://cloud.google.com/iam/docs/reference/rest/v1/projects.serviceAccounts/setIamPolicy
"""
policy: Policy = self.get_service_account_iam_policy(account)
binding: Optional[Policy.Binding] = policy.find_binding_for_role(role)
if binding is None:
logger.debug('Noop: Service Account %s has no bindings for role %s',
account, role)
return
if member not in binding.members:
logger.debug(
'Noop: Service Account %s binding for role %s has no member %s',
account, role, member)
return
updated_members: FrozenSet[str] = binding.members.difference({member})
updated_binding: Policy.Binding = dataclasses.replace(
binding, members=updated_members)
updated_policy: Policy = _replace_binding(policy, binding,
updated_binding)
self.set_service_account_iam_policy(account, updated_policy)
logger.debug('Role %s revoked from member %s for Service Account %s',
role, member, account)

@ -19,6 +19,7 @@ from typing import Optional
import mako.template
import yaml
from framework.infrastructure import gcp
from framework.infrastructure import k8s
logger = logging.getLogger(__name__)
@ -31,6 +32,7 @@ class RunnerError(Exception):
class KubernetesBaseRunner:
TEMPLATE_DIR_NAME = 'kubernetes-manifests'
TEMPLATE_DIR_RELATIVE_PATH = f'../../{TEMPLATE_DIR_NAME}'
ROLE_WORKLOAD_IDENTITY_USER = 'roles/iam.workloadIdentityUser'
def __init__(self,
k8s_namespace,
@ -129,6 +131,46 @@ class KubernetesBaseRunner:
namespace.metadata.creation_timestamp)
return namespace
@staticmethod
def _get_workload_identity_member_name(project, namespace_name,
service_account_name):
"""
Returns workload identity member name used to authenticate Kubernetes
service accounts.
https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity
"""
return (f'serviceAccount:{project}.svc.id.goog'
f'[{namespace_name}/{service_account_name}]')
def _grant_workload_identity_user(self, *, gcp_iam, gcp_service_account,
service_account_name):
workload_identity_member = self._get_workload_identity_member_name(
gcp_iam.project, self.k8s_namespace.name, service_account_name)
logger.info('Granting %s to %s for GCP Service Account %s',
self.ROLE_WORKLOAD_IDENTITY_USER, workload_identity_member,
gcp_service_account)
gcp_iam.add_service_account_iam_policy_binding(
gcp_service_account, self.ROLE_WORKLOAD_IDENTITY_USER,
workload_identity_member)
def _revoke_workload_identity_user(self, *, gcp_iam, gcp_service_account,
service_account_name):
workload_identity_member = self._get_workload_identity_member_name(
gcp_iam.project, self.k8s_namespace.name, service_account_name)
logger.info('Revoking %s from %s for GCP Service Account %s',
self.ROLE_WORKLOAD_IDENTITY_USER, workload_identity_member,
gcp_service_account)
try:
gcp_iam.remove_service_account_iam_policy_binding(
gcp_service_account, self.ROLE_WORKLOAD_IDENTITY_USER,
workload_identity_member)
except gcp.api.Error as error:
logger.warning('Failed %s from %s for Service Account %s: %r',
self.ROLE_WORKLOAD_IDENTITY_USER,
workload_identity_member, gcp_service_account, error)
def _create_service_account(self, template,
**kwargs) -> k8s.V1ServiceAccount:
resource = self._create_from_template(template, **kwargs)

@ -23,6 +23,7 @@ import logging
from typing import Iterator, Optional
from framework.helpers import retryers
from framework.infrastructure import gcp
from framework.infrastructure import k8s
import framework.rpc
from framework.rpc import grpc_channelz
@ -204,8 +205,10 @@ class KubernetesClientRunner(base_runner.KubernetesBaseRunner):
*,
deployment_name,
image_name,
gcp_service_account,
td_bootstrap_image,
gcp_api_manager: gcp.api.GcpApiManager,
gcp_project: str,
gcp_service_account: str,
xds_server_uri=None,
network='default',
service_account_name=None,
@ -220,16 +223,22 @@ class KubernetesClientRunner(base_runner.KubernetesBaseRunner):
# Settings
self.deployment_name = deployment_name
self.image_name = image_name
self.gcp_service_account = gcp_service_account
self.service_account_name = service_account_name or deployment_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.service_account_template = service_account_template
self.debug_use_port_forwarding = debug_use_port_forwarding
# Service account settings:
# Kubernetes service account
self.service_account_name = service_account_name or deployment_name
self.service_account_template = service_account_template
# 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
@ -246,6 +255,13 @@ class KubernetesClientRunner(base_runner.KubernetesBaseRunner):
super().run()
# TODO(sergiitk): make rpc UnaryCall enum or get it from proto
# Allow Kubernetes service account to use the GCP service account
# identity.
self._grant_workload_identity_user(
gcp_iam=self.gcp_iam,
gcp_service_account=self.gcp_service_account,
service_account_name=self.service_account_name)
# Create service account
self.service_account = self._create_service_account(
self.service_account_template,
@ -299,6 +315,10 @@ class KubernetesClientRunner(base_runner.KubernetesBaseRunner):
self._delete_deployment(self.deployment_name)
self.deployment = None
if 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)

@ -21,6 +21,7 @@ import functools
import logging
from typing import Iterator, Optional
from framework.infrastructure import gcp
from framework.infrastructure import k8s
import framework.rpc
from framework.rpc import grpc_channelz
@ -135,11 +136,13 @@ class KubernetesServerRunner(base_runner.KubernetesBaseRunner):
*,
deployment_name,
image_name,
gcp_service_account,
td_bootstrap_image,
gcp_api_manager: gcp.api.GcpApiManager,
gcp_project: str,
gcp_service_account: str,
service_account_name=None,
service_name=None,
neg_name=None,
td_bootstrap_image=None,
xds_server_uri=None,
network='default',
deployment_template='server.deployment.yaml',
@ -154,8 +157,6 @@ class KubernetesServerRunner(base_runner.KubernetesBaseRunner):
# Settings
self.deployment_name = deployment_name
self.image_name = image_name
self.gcp_service_account = gcp_service_account
self.service_account_name = service_account_name or deployment_name
self.service_name = service_name or deployment_name
# xDS bootstrap generator
self.td_bootstrap_image = td_bootstrap_image
@ -166,10 +167,18 @@ class KubernetesServerRunner(base_runner.KubernetesBaseRunner):
f'{self.service_name}')
self.network = network
self.deployment_template = deployment_template
self.service_account_template = service_account_template
self.service_template = service_template
self.reuse_service = reuse_service
self.debug_use_port_forwarding = debug_use_port_forwarding
# Service account settings:
# Kubernetes service account
self.service_account_name = service_account_name or deployment_name
self.service_account_template = service_account_template
# 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
@ -223,6 +232,13 @@ class KubernetesServerRunner(base_runner.KubernetesBaseRunner):
test_port=test_port)
self._wait_service_neg(self.service_name, test_port)
# Allow Kubernetes service account to use the GCP service account
# identity.
self._grant_workload_identity_user(
gcp_iam=self.gcp_iam,
gcp_service_account=self.gcp_service_account,
service_account_name=self.service_account_name)
# Create service account
self.service_account = self._create_service_account(
self.service_account_template,
@ -284,6 +300,10 @@ class KubernetesServerRunner(base_runner.KubernetesBaseRunner):
self._delete_service(self.service_name)
self.service = None
if 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))

@ -232,8 +232,10 @@ class RegularXdsKubernetesTestCase(XdsKubernetesTestCase):
self.server_namespace),
deployment_name=self.server_name,
image_name=self.server_image,
gcp_service_account=self.gcp_service_account,
td_bootstrap_image=self.td_bootstrap_image,
gcp_project=self.project,
gcp_api_manager=self.gcp_api_manager,
gcp_service_account=self.gcp_service_account,
xds_server_uri=self.xds_server_uri,
network=self.network)
@ -243,8 +245,10 @@ class RegularXdsKubernetesTestCase(XdsKubernetesTestCase):
self.client_namespace),
deployment_name=self.client_name,
image_name=self.client_image,
gcp_service_account=self.gcp_service_account,
td_bootstrap_image=self.td_bootstrap_image,
gcp_project=self.project,
gcp_api_manager=self.gcp_api_manager,
gcp_service_account=self.gcp_service_account,
xds_server_uri=self.xds_server_uri,
network=self.network,
debug_use_port_forwarding=self.debug_use_port_forwarding,
@ -307,9 +311,11 @@ class SecurityXdsKubernetesTestCase(XdsKubernetesTestCase):
self.server_namespace),
deployment_name=self.server_name,
image_name=self.server_image,
td_bootstrap_image=self.td_bootstrap_image,
gcp_project=self.project,
gcp_api_manager=self.gcp_api_manager,
gcp_service_account=self.gcp_service_account,
network=self.network,
td_bootstrap_image=self.td_bootstrap_image,
xds_server_uri=self.xds_server_uri,
deployment_template='server-secure.deployment.yaml',
debug_use_port_forwarding=self.debug_use_port_forwarding)
@ -320,8 +326,10 @@ class SecurityXdsKubernetesTestCase(XdsKubernetesTestCase):
self.client_namespace),
deployment_name=self.client_name,
image_name=self.client_image,
gcp_service_account=self.gcp_service_account,
td_bootstrap_image=self.td_bootstrap_image,
gcp_project=self.project,
gcp_api_manager=self.gcp_api_manager,
gcp_service_account=self.gcp_service_account,
xds_server_uri=self.xds_server_uri,
network=self.network,
deployment_template='client-secure.deployment.yaml',

Loading…
Cancel
Save