173 lines
6.5 KiB
Python
173 lines
6.5 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2021 Google LLC. All Rights Reserved.
|
|
#
|
|
# 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.
|
|
"""Utilities for Eventarc gke-destinations command."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
from apitools.base.py import exceptions as apitools_exceptions
|
|
from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
|
|
from googlecloudsdk.api_lib.eventarc import common
|
|
from googlecloudsdk.api_lib.services import serviceusage
|
|
from googlecloudsdk.api_lib.util import apis
|
|
from googlecloudsdk.command_lib.projects import util as projects_util
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core.console import console_io
|
|
from googlecloudsdk.core.util import retry
|
|
|
|
_LOCATION = 'us-central1'
|
|
_TRIGGER_ID = 'fake-trigger-id'
|
|
_ROLES = ('roles/container.developer', 'roles/iam.serviceAccountAdmin',
|
|
'roles/compute.viewer')
|
|
# Wait till service account is available for setIamPolicy
|
|
_MAX_WAIT_TIME_IN_MS = 20 * 1000
|
|
|
|
|
|
class GKEDestinationInitializationError(exceptions.InternalError):
|
|
"""Error when failing to initialize project for Cloud Run for Anthos/GKE destinations."""
|
|
|
|
|
|
def _ShouldRetryHttpError(exc_type, exc_value, exc_traceback, state):
|
|
"""Whether to retry the request when receiving errors.
|
|
|
|
Args:
|
|
exc_type: type of the raised exception.
|
|
exc_value: the instance of the raise the exception.
|
|
exc_traceback: Traceback, traceback encapsulating the call stack at the the
|
|
point where the exception occurred.
|
|
state: RetryerState, state of the retryer.
|
|
|
|
Returns:
|
|
True if exception and is due to NOT_FOUND or INVALID_ARGUMENT.
|
|
"""
|
|
del exc_value, exc_traceback, state
|
|
return (exc_type == apitools_exceptions.HttpBadRequestError or
|
|
exc_type == apitools_exceptions.HttpNotFoundError)
|
|
|
|
|
|
def _GetOrCreateP4SA(service_name):
|
|
"""Gets (or creates) the P4SA for Eventarc in the given project.
|
|
|
|
If the P4SA does not exist for this project, it will be created. Otherwise,
|
|
the email address of the existing P4SA will be returned.
|
|
|
|
Args:
|
|
service_name: str, name of the service for the P4SA, e.g.
|
|
eventarc.googleapis.com
|
|
|
|
Returns:
|
|
Email address of the Eventarc P4SA for the given project.
|
|
"""
|
|
project_name = properties.VALUES.core.project.Get(required=True)
|
|
response = serviceusage.GenerateServiceIdentity(project_name, service_name)
|
|
return response['email']
|
|
|
|
|
|
class GKEDestinationsClient(object):
|
|
"""Wrapper client for setting up Eventarc Cloud Run for Anthos/GKE destinations."""
|
|
|
|
def __init__(self, release_track):
|
|
self._api_version = common.GetApiVersion(release_track)
|
|
client = apis.GetClientInstance(common.API_NAME, self._api_version)
|
|
self._messages = client.MESSAGES_MODULE
|
|
self._service = client.projects_locations_triggers
|
|
|
|
def InitServiceAccount(self):
|
|
"""Force create the Eventarc P4SA, and grant IAM roles to it.
|
|
|
|
1) First, trigger the P4SA JIT provision by trying to create an empty
|
|
trigger, ignore the HttpBadRequestError exception, then call
|
|
GenerateServiceIdentity to verify that P4SA creation is completed.
|
|
2) Then grant necessary roles needed to the P4SA for creating GKE triggers.
|
|
|
|
Raises:
|
|
GKEDestinationInitializationError: P4SA failed to be created.
|
|
"""
|
|
try:
|
|
self._CreateEmptyTrigger()
|
|
except apitools_exceptions.HttpBadRequestError:
|
|
pass
|
|
|
|
service_name = common.GetApiServiceName(self._api_version)
|
|
p4sa_email = _GetOrCreateP4SA(service_name)
|
|
if not p4sa_email:
|
|
raise GKEDestinationInitializationError(
|
|
'Failed to initialize project for Cloud Run for Anthos/GKE destinations.'
|
|
)
|
|
|
|
self._BindRolesToServiceAccount(p4sa_email, _ROLES)
|
|
|
|
def _CreateEmptyTrigger(self):
|
|
"""Attempt to create an empty trigger in us-central1 to kick off P4SA JIT provision.
|
|
|
|
The create request will always fail due to the empty trigger message
|
|
payload, but it will trigger the P4SA JIT provision.
|
|
|
|
Returns:
|
|
A long-running operation for create.
|
|
"""
|
|
project = properties.VALUES.core.project.Get(required=True)
|
|
parent = 'projects/{}/locations/{}'.format(project, _LOCATION)
|
|
req = self._messages.EventarcProjectsLocationsTriggersCreateRequest(
|
|
parent=parent, triggerId=_TRIGGER_ID)
|
|
return self._service.Create(req)
|
|
|
|
def _BindRolesToServiceAccount(self, sa_email, roles):
|
|
"""Binds roles to the provided service account.
|
|
|
|
Args:
|
|
sa_email: str, the service account to bind roles to.
|
|
roles: iterable, the roles to be bound to the service account.
|
|
"""
|
|
formatted_roles = '\n'.join(['- {}'.format(role) for role in sorted(roles)])
|
|
log.status.Print(
|
|
'To use Eventarc with Cloud Run for Anthos/GKE destinations, Eventarc Service Agent [{}] '
|
|
'needs to be bound to the following required roles:\n{}'.format(
|
|
sa_email, formatted_roles))
|
|
|
|
console_io.PromptContinue(
|
|
default=False,
|
|
throw_if_unattended=True,
|
|
prompt_string='\nWould you like to bind these roles?',
|
|
cancel_on_no=True)
|
|
|
|
project_ref = projects_util.ParseProject(
|
|
properties.VALUES.core.project.Get(required=True))
|
|
member_str = 'serviceAccount:{}'.format(sa_email)
|
|
member_roles = [(member_str, role) for role in roles]
|
|
self._AddIamPolicyBindingsWithRetry(project_ref, member_roles)
|
|
log.status.Print('Roles successfully bound.')
|
|
|
|
@retry.RetryOnException(
|
|
max_retrials=10,
|
|
max_wait_ms=_MAX_WAIT_TIME_IN_MS,
|
|
exponential_sleep_multiplier=1.6,
|
|
sleep_ms=100,
|
|
should_retry_if=_ShouldRetryHttpError)
|
|
def _AddIamPolicyBindingsWithRetry(self, project_ref, member_roles):
|
|
"""Adds iam bindings to project_ref's iam policy, with retry.
|
|
|
|
Args:
|
|
project_ref: The project for the binding
|
|
member_roles: List of 2-tuples of the form [(member, role), ...].
|
|
|
|
Returns:
|
|
The updated IAM Policy
|
|
"""
|
|
return projects_api.AddIamPolicyBindings(project_ref, member_roles)
|