def RunCommand(self, args, **kwargs): try: project = properties.VALUES.core.project.GetOrFail() return CreateFeature(project, self.FEATURE_NAME, self.FEATURE_DISPLAY_NAME, **kwargs) except apitools_exceptions.HttpUnauthorizedError as e: raise exceptions.Error( 'You are not authorized to enable {} Feature from project [{}]. ' 'Underlying error: {}'.format(self.FEATURE_DISPLAY_NAME, project, e)) except properties.RequiredPropertyError as e: raise exceptions.Error('Failed to retrieve the project ID.') except apitools_exceptions.HttpConflictError as e: # If the error is not due to the object already existing, re-raise. error = core_api_exceptions.HttpErrorPayload(e) if error.status_description != 'ALREADY_EXISTS': raise else: log.status.Print( '{} Feature for project [{}] is already enabled'.format( self.FEATURE_DISPLAY_NAME, project)) except apitools_exceptions.HttpBadRequestError as e: error = core_api_exceptions.HttpErrorPayload(e) if error.status_description != 'FAILED_PRECONDITION': raise else: log.status.Print(error.status_message)
def WaitForOperation(self, operation, retry_callback=None): """Wait until the operation is complete or times out. This does not use the core api_lib.util.waiter because the cloud build logs serve as a progress tracker. Args: operation: The operation resource to wait on retry_callback: A callback to be executed before each retry, if desired. Returns: The operation resource when it has completed Raises: OperationTimeoutError: when the operation polling times out OperationError: when the operation completed with an error """ completed_operation = self._PollUntilDone(operation, retry_callback) if not completed_operation: raise OperationTimeoutError( ('Operation [{0}] timed out. This operation ' 'may still be underway.').format(operation.name)) if completed_operation.error: message = exceptions.HttpErrorPayload( completed_operation.error).format(_ERROR_FORMAT_STRING) raise OperationError(message) return completed_operation
def Enable(self, feature): project = properties.VALUES.core.project.GetOrFail() enable_api.EnableServiceIfDisabled(project, self.feature.api) parent = util.LocationResourceName(project) try: # Retry if we still get "API not activated"; it can take a few minutes # for Chemist to catch up. See b/28800908. # TODO(b/177098463): Add a spinner here? retryer = retry.Retryer(max_retrials=4, exponential_sleep_multiplier=1.75) op = retryer.RetryOnException( self.hubclient.CreateFeature, args=(parent, self.feature_name, feature), should_retry_if=self._FeatureAPINotEnabled, sleep_ms=1000) except retry.MaxRetrialsException: raise exceptions.Error( 'Retry limit exceeded waiting for {} to enable'.format( self.feature.api)) except apitools_exceptions.HttpConflictError as e: # If the error is not due to the object already existing, re-raise. error = core_api_exceptions.HttpErrorPayload(e) if error.status_description != 'ALREADY_EXISTS': raise # TODO(b/177098463): Decide if this should be a hard error if a spec was # set, but not applied, because the Feature already existed. log.status.Print( '{} Feature for project [{}] is already enabled'.format( self.feature.display_name, project)) return msg = 'Waiting for Feature {} to be created'.format( self.feature.display_name) return self.WaitForHubOp(self.hubclient.feature_waiter, op=op, message=msg)
def _FeatureAPINotEnabled(self, exc_type, exc_value, traceback, state): del traceback, state # Unused if exc_type != apitools_exceptions.HttpBadRequestError: return False error = core_api_exceptions.HttpErrorPayload(exc_value) # TODO(b/188807249): Add a reference to this error in the error package. if not (error.status_description == 'FAILED_PRECONDITION' and self.feature.api in error.message and 'is not enabled' in error.message): return False log.status.Print('Waiting for service API enablement to finish...') return True
def _GetViolationsFromError(error): """Looks for violations descriptions in error message. Args: error: HttpError containing error information. Returns: String of newline-separated violations descriptions. """ error_payload = exceptions_util.HttpErrorPayload(error) field_errors = error_payload.field_violations if not field_errors: return '' return '\n'.join(field_errors.values()) + '\n'
def _GetViolationsFromError(error): """Looks for violations descriptions in error message. Args: error: HttpError containing error information. Returns: String of newline-separated violations descriptions. """ error_payload = exceptions_util.HttpErrorPayload(error) errors = [] errors.extend( ['{}:\n{}'.format(k, v) for k, v in error_payload.violations.items()]) errors.extend([ '{}:\n{}'.format(k, v) for k, v in error_payload.field_violations.items() ]) if errors: return '\n'.join(errors) + '\n' return ''
def _UpdateOrCreateService(self, service_ref, config_changes, with_code, private_endpoint=None): """Apply config_changes to the service. Create it if necessary. Arguments: service_ref: Reference to the service to create or update config_changes: list of ConfigChanger to modify the service with with_code: bool, True if the config_changes contains code to deploy. We can't create the service if we're not deploying code. private_endpoint: bool, True if creating a new Service for Cloud Run on GKE that should only be addressable from within the cluster. False if it should be publicly addressable. None if its existing visibility should remain unchanged. Returns: The Service object we created or modified. """ nonce = _Nonce() config_changes = [_NewRevisionForcingChange(nonce)] + config_changes messages = self._messages_module # GET the Service serv = self.GetService(service_ref) try: if serv: if not with_code: # Avoid changing the running code by making the new revision by digest self._EnsureImageDigest(serv, config_changes) if private_endpoint is None: # Don't change the existing service visibility pass elif private_endpoint: serv.labels[service.ENDPOINT_VISIBILITY] = service.CLUSTER_LOCAL else: del serv.labels[service.ENDPOINT_VISIBILITY] # PUT the changed Service for config_change in config_changes: config_change.AdjustConfiguration(serv.configuration, serv.metadata) serv_name = service_ref.RelativeName() serv_update_req = ( messages.RunNamespacesServicesReplaceServiceRequest( service=serv.Message(), name=serv_name)) with metrics.record_duration(metrics.UPDATE_SERVICE): updated = self._client.namespaces_services.ReplaceService( serv_update_req) return service.Service(updated, messages) else: if not with_code: raise serverless_exceptions.ServiceNotFoundError( 'Service [{}] could not be found.'.format(service_ref.servicesId)) # POST a new Service new_serv = service.Service.New(self._client, service_ref.namespacesId, private_endpoint) new_serv.name = service_ref.servicesId pretty_print.Info('Creating new service [{bold}{service}{reset}]', service=new_serv.name) parent = service_ref.Parent().RelativeName() for config_change in config_changes: config_change.AdjustConfiguration(new_serv.configuration, new_serv.metadata) serv_create_req = ( messages.RunNamespacesServicesCreateRequest( service=new_serv.Message(), parent=parent)) with metrics.record_duration(metrics.CREATE_SERVICE): raw_service = self._client.namespaces_services.Create( serv_create_req) return service.Service(raw_service, messages) except api_exceptions.HttpBadRequestError as e: error_payload = exceptions_util.HttpErrorPayload(e) if error_payload.field_violations: if (serverless_exceptions.BadImageError.IMAGE_ERROR_FIELD in error_payload.field_violations): exceptions.reraise(serverless_exceptions.BadImageError(e)) exceptions.reraise(e) except api_exceptions.HttpNotFoundError as e: # TODO(b/118339293): List available regions to check whether provided # region is invalid or not. raise serverless_exceptions.DeploymentFailedError( 'Deployment endpoint was not found. Perhaps the provided ' 'region was invalid. Set the `run/region` property to a valid ' 'region and retry. Ex: `gcloud config set run/region us-central1`')
def _GetViolations(self, err): payload = exceptions.HttpErrorPayload(err) return payload.violations
def _GetFieldViolations(self, err): payload = exceptions.HttpErrorPayload(err) return payload.field_violations
def Run(self, args): project = arg_utils.GetFromNamespace(args, '--project', use_defaults=True) # This incidentally verifies that the kubeconfig and context args are valid. kube_client = hub_util.KubernetesClient(args) uuid = hub_util.GetClusterUUID(kube_client) self._VerifyClusterExclusivity(kube_client, project, args.context, uuid) # Read the service account files provided in the arguments early, in order # to catch invalid files before performing mutating operations. try: service_account_key_data = hub_util.Base64EncodedFileContents( args.service_account_key_file) except files.Error as e: raise exceptions.Error('Could not process {}: {}'.format( SERVICE_ACCOUNT_KEY_FILE_FLAG, e)) docker_credential_data = None if args.docker_credential_file: try: docker_credential_data = hub_util.Base64EncodedFileContents( args.docker_credential_file) except files.Error as e: raise exceptions.Error('Could not process {}: {}'.format( DOCKER_CREDENTIAL_FILE_FLAG, e)) # The full resource name of the membership for this registration flow. name = 'projects/{}/locations/global/memberships/{}'.format( project, uuid) # Attempt to create a membership. already_exists = False try: hub_util.ApplyMembershipResources(kube_client, project) obj = hub_util.CreateMembership(project, uuid, args.CLUSTER_NAME) except apitools_exceptions.HttpConflictError as e: # If the error is not due to the object already existing, re-raise. error = core_api_exceptions.HttpErrorPayload(e) if error.status_description != 'ALREADY_EXISTS': raise # The membership already exists. Check to see if it has the same # description (i.e., user-visible cluster name). obj = hub_util.GetMembership(name) if obj.description != args.CLUSTER_NAME: # A membership exists, but does not have the same description. This is # possible if two different users attempt to register the same # cluster, or if the user is upgrading and has passed a different # cluster name. Treat this as an error: even in the upgrade case, # this is useful to prevent the user from upgrading the wrong cluster. raise exceptions.Error( 'There is an existing membership, [{}], that conflicts with [{}]. ' 'Please delete it before continuing:\n\n' ' gcloud {}container memberships delete {}'.format( obj.description, args.CLUSTER_NAME, hub_util.ReleaseTrackCommandPrefix( self.ReleaseTrack()), name)) # The membership exists and has the same description. already_exists = True console_io.PromptContinue( message='A membership for [{}] already exists. Continuing will ' 'update the Connect agent deployment to use a new image (if one is ' 'available), or install the Connect agent if it is not already ' 'running.'.format(args.CLUSTER_NAME), cancel_on_no=True) # A membership exists. Attempt to update the existing agent deployment, or # install a new agent if necessary. if already_exists: obj = hub_util.GetMembership(name) hub_util.DeployConnectAgent(args, service_account_key_data, docker_credential_data, upgrade=True) return obj # No membership exists. Attempt to create a new one, and install a new # agent. try: hub_util.DeployConnectAgent(args, service_account_key_data, docker_credential_data, upgrade=False) except: hub_util.DeleteMembership(name) hub_util.DeleteMembershipResources(kube_client) raise return obj
def _UpdateOrCreateService(self, service_ref, config_changes, with_code): """Apply config_changes to the service. Create it if necessary. Arguments: service_ref: Reference to the service to create or update config_changes: list of ConfigChanger to modify the service with with_code: boolean, True if the config_changes contains code to deploy. We can't create the service if we're not deploying code. Returns: The Service object we created or modified. """ nonce = _Nonce() config_changes = [_NewRevisionForcingChange(nonce)] + config_changes messages = self._messages_module # GET the Service serv = self.GetService(service_ref) try: if serv: if not with_code: # Avoid changing the running code by making the new revision by digest self._EnsureImageDigest(serv, config_changes) # PUT the changed Service for config_change in config_changes: config_change.AdjustConfiguration(serv.configuration, serv.metadata) serv_name = service_ref.RelativeName() serv_update_req = ( messages.ServerlessNamespacesServicesReplaceServiceRequest( service=serv.Message(), name=serv_name)) with metrics.record_duration(metrics.UPDATE_SERVICE): updated = self._client.namespaces_services.ReplaceService( serv_update_req) return service.Service(updated, messages) else: if not with_code: raise serverless_exceptions.ServiceNotFoundError( 'Service [{}] could not be found.'.format( service_ref.servicesId)) # POST a new Service new_serv = service.Service.New(self._client, service_ref.namespacesId) new_serv.name = service_ref.servicesId pretty_print.Info( 'Creating new service [{bold}{service}{reset}]', service=new_serv.name) parent = service_ref.Parent().RelativeName() for config_change in config_changes: config_change.AdjustConfiguration(new_serv.configuration, new_serv.metadata) serv_create_req = ( messages.ServerlessNamespacesServicesCreateRequest( service=new_serv.Message(), parent=parent)) with metrics.record_duration(metrics.CREATE_SERVICE): raw_service = self._client.namespaces_services.Create( serv_create_req) return service.Service(raw_service, messages) except api_exceptions.HttpBadRequestError as e: error_payload = exceptions_util.HttpErrorPayload(e) if error_payload.field_violations: if (serverless_exceptions.BadImageError.IMAGE_ERROR_FIELD in error_payload.field_violations): exceptions.reraise(serverless_exceptions.BadImageError(e)) exceptions.reraise(e)
def Run(self, args): project = arg_utils.GetFromNamespace(args, '--project', use_defaults=True) # This incidentally verifies that the kubeconfig and context args are valid. with kube_util.KubernetesClient(args) as kube_client: kube_client.CheckClusterAdminPermissions() kube_util.ValidateClusterIdentifierFlags(kube_client, args) uuid = kube_util.GetClusterUUID(kube_client) # Read the service account files provided in the arguments early, in order # to catch invalid files before performing mutating operations. # Service Account key file is required if Workload Identity is not # enabled. # If Workload Identity is enabled, then the Connect Agent uses # a Kubernetes Service Account token instead and hence a GCP Service # Account key is not required. service_account_key_data = '' if args.service_account_key_file: try: service_account_key_data = hub_util.Base64EncodedFileContents( args.service_account_key_file) except files.Error as e: raise exceptions.Error('Could not process {}: {}'.format( SERVICE_ACCOUNT_KEY_FILE_FLAG, e)) docker_credential_data = None if args.docker_credential_file: try: docker_credential_data = hub_util.Base64EncodedFileContents( args.docker_credential_file) except files.Error as e: raise exceptions.Error('Could not process {}: {}'.format( DOCKER_CREDENTIAL_FILE_FLAG, e)) gke_cluster_self_link = kube_client.processor.gke_cluster_self_link issuer_url = None # enable_workload_identity, public_issuer_url, and # manage_workload_identity_bucket are only properties if we are on the # alpha track if (self.ReleaseTrack() is base.ReleaseTrack.ALPHA and args.enable_workload_identity): if args.public_issuer_url: issuer_url = args.public_issuer_url # Use the user-provided public URL, and ignore the built-in endpoints. try: openid_config_json = kube_client.GetOpenIDConfiguration( issuer_url=args.public_issuer_url) except Exception as e: # pylint: disable=broad-except raise exceptions.Error( 'Please double check that --public-issuer-url was set ' 'correctly: {}'.format(e)) else: # Since the user didn't specify a public URL, try to use the cluster's # built-in endpoints. try: openid_config_json = kube_client.GetOpenIDConfiguration( ) except Exception as e: # pylint: disable=broad-except raise exceptions.Error( 'Please double check that it is possible to access the ' '/.well-known/openid-configuration endpoint on the cluster: ' '{}'.format(e)) # Extract the issuer URL from the discovery doc. issuer_url = json.loads(openid_config_json).get('issuer') if not issuer_url: raise exceptions.Error( 'Invalid OpenID Config: ' 'missing issuer: {}'.format(openid_config_json)) # If a public issuer URL was provided, ensure it matches what came back # in the discovery doc. elif args.public_issuer_url \ and args.public_issuer_url != issuer_url: raise exceptions.Error( '--public-issuer-url {} did not match issuer ' 'returned in discovery doc: {}'.format( args.public_issuer_url, issuer_url)) # Set up the GCS bucket that serves OpenID Provider Config and JWKS. if args.manage_workload_identity_bucket: openid_keyset_json = kube_client.GetOpenIDKeyset() api_util.CreateWorkloadIdentityBucket( project, issuer_url, openid_config_json, openid_keyset_json) # Attempt to create a membership. already_exists = False obj = None # For backward compatiblity, check if a membership was previously created # using the cluster uuid. parent = api_util.ParentRef(project, 'global') membership_id = uuid resource_name = api_util.MembershipRef(project, 'global', uuid) obj = self._CheckMembershipWithUUID(resource_name, args.CLUSTER_NAME) if obj: # The membership exists and has the same description. already_exists = True else: # Attempt to create a new membership using cluster_name. membership_id = args.CLUSTER_NAME resource_name = api_util.MembershipRef(project, 'global', args.CLUSTER_NAME) try: self._VerifyClusterExclusivity(kube_client, parent, membership_id) obj = api_util.CreateMembership(project, args.CLUSTER_NAME, args.CLUSTER_NAME, gke_cluster_self_link, uuid, self.ReleaseTrack(), issuer_url) except apitools_exceptions.HttpConflictError as e: # If the error is not due to the object already existing, re-raise. error = core_api_exceptions.HttpErrorPayload(e) if error.status_description != 'ALREADY_EXISTS': raise obj = api_util.GetMembership(resource_name, self.ReleaseTrack()) if not obj.externalId: raise exceptions.Error( 'invalid membership {} does not have ' 'external_id field set. We cannot determine ' 'if registration is requested against a ' 'valid existing Membership. Consult the ' 'documentation on container hub memberships ' 'update for more information or run gcloud ' 'container hub memberships delete {} if you ' 'are sure that this is an invalid or ' 'otherwise stale Membership'.format( membership_id, membership_id)) if obj.externalId != uuid: raise exceptions.Error( 'membership {} already exists in the project' ' with another cluster. If this operation is' ' intended, please run `gcloud container ' 'hub memberships delete {}` and register ' 'again.'.format(membership_id, membership_id)) # The membership exists with same cluster_name. already_exists = True # In case of an existing membership, check with the user to upgrade the # Connect-Agent. if already_exists: console_io.PromptContinue( message= 'A membership [{}] for the cluster [{}] already exists. ' 'Continuing will reinstall the Connect agent deployment to use a ' 'new image (if one is available).'.format( resource_name, args.CLUSTER_NAME), cancel_on_no=True) else: log.status.Print( 'Created a new membership [{}] for the cluster [{}]'. format(resource_name, args.CLUSTER_NAME)) # Attempt to update the existing agent deployment, or install a new agent # if necessary. try: self._InstallOrUpdateExclusivityArtifacts( kube_client, resource_name) agent_util.DeployConnectAgent(kube_client, args, service_account_key_data, docker_credential_data, resource_name, self.ReleaseTrack()) except Exception as e: log.status.Print( 'Error in installing the Connect Agent: {}'.format(e)) # In case of a new membership, we need to clean up membership and # resources if we failed to install the Connect Agent. if not already_exists: api_util.DeleteMembership(resource_name, self.ReleaseTrack()) exclusivity_util.DeleteMembershipResources(kube_client) raise log.status.Print( 'Finished registering the cluster [{}] with the Hub.'.format( args.CLUSTER_NAME)) return obj
def Run(self, args): project = arg_utils.GetFromNamespace(args, '--project', use_defaults=True) # This incidentally verifies that the kubeconfig and context args are valid. with kube_util.KubernetesClient(args) as kube_client: kube_client.CheckClusterAdminPermissions() kube_util.ValidateClusterIdentifierFlags(kube_client, args) uuid = kube_util.GetClusterUUID(kube_client) # Read the service account files provided in the arguments early, in order # to catch invalid files before performing mutating operations. try: service_account_key_data = hub_util.Base64EncodedFileContents( args.service_account_key_file) except files.Error as e: raise exceptions.Error('Could not process {}: {}'.format( SERVICE_ACCOUNT_KEY_FILE_FLAG, e)) docker_credential_data = None if args.docker_credential_file: try: docker_credential_data = hub_util.Base64EncodedFileContents( args.docker_credential_file) except files.Error as e: raise exceptions.Error('Could not process {}: {}'.format( DOCKER_CREDENTIAL_FILE_FLAG, e)) gke_cluster_self_link = kube_client.processor.gke_cluster_self_link issuer_url = None # public_issuer_url is only a property if we are on the alpha track if self.ReleaseTrack() is base.ReleaseTrack.ALPHA and \ args.public_issuer_url: issuer_url = args.public_issuer_url # Attempt to create a membership. already_exists = False obj = None # For backward compatiblity, check if a membership was previously created # using the cluster uuid. parent = api_util.ParentRef(project, 'global') membership_id = uuid resource_name = api_util.MembershipRef(project, 'global', uuid) obj = self._CheckMembershipWithUUID(resource_name, args.CLUSTER_NAME) if obj: # The membership exists and has the same description. already_exists = True else: # Attempt to create a new membership using cluster_name. membership_id = args.CLUSTER_NAME resource_name = api_util.MembershipRef(project, 'global', args.CLUSTER_NAME) try: self._VerifyClusterExclusivity(kube_client, parent, membership_id) obj = api_util.CreateMembership(project, args.CLUSTER_NAME, args.CLUSTER_NAME, gke_cluster_self_link, uuid, self.ReleaseTrack(), issuer_url) except apitools_exceptions.HttpConflictError as e: # If the error is not due to the object already existing, re-raise. error = core_api_exceptions.HttpErrorPayload(e) if error.status_description != 'ALREADY_EXISTS': raise obj = api_util.GetMembership(resource_name, self.ReleaseTrack()) if not obj.externalId: raise exceptions.Error( 'invalid membership {} does not have ' 'external_id field set. We cannot determine ' 'if registration is requested against a ' 'valid existing Membership. Consult the ' 'documentation on container hub memberships ' 'update for more information or run gcloud ' 'container hub memberships delete {} if you ' 'are sure that this is an invalid or ' 'otherwise stale Membership'.format( membership_id, membership_id)) if obj.externalId != uuid: raise exceptions.Error( 'membership {} already exists in the project' ' with another cluster. If this operation is' ' intended, please run `gcloud container ' 'hub memberships delete {}` and register ' 'again.'.format(membership_id, membership_id)) # The membership exists with same cluster_name. already_exists = True # In case of an existing membership, check with the user to upgrade the # Connect-Agent. if already_exists: console_io.PromptContinue( message= 'A membership [{}] for the cluster [{}] already exists. ' 'Continuing will reinstall the Connect agent deployment to use a ' 'new image (if one is available).'.format( resource_name, args.CLUSTER_NAME), cancel_on_no=True) else: log.status.Print( 'Created a new membership [{}] for the cluster [{}]'. format(resource_name, args.CLUSTER_NAME)) # Attempt to update the existing agent deployment, or install a new agent # if necessary. try: self._InstallOrUpdateExclusivityArtifacts( kube_client, resource_name) agent_util.DeployConnectAgent(kube_client, args, service_account_key_data, docker_credential_data, resource_name, self.ReleaseTrack()) except Exception as e: log.status.Print( 'Error in installing the Connect Agent: {}'.format(e)) # In case of a new membership, we need to clean up membership and # resources if we failed to install the Connect Agent. if not already_exists: api_util.DeleteMembership(resource_name, self.ReleaseTrack()) exclusivity_util.DeleteMembershipResources(kube_client) raise log.status.Print( 'Finished registering the cluster [{}] with the Hub.'.format( args.CLUSTER_NAME)) return obj
def Run(self, args): project = arg_utils.GetFromNamespace(args, '--project', use_defaults=True) # This incidentally verifies that the kubeconfig and context args are valid. kube_client = kube_util.KubernetesClient(args) uuid = kube_util.GetClusterUUID(kube_client) gke_cluster_self_link = api_util.GKEClusterSelfLink(args) # Read the service account files provided in the arguments early, in order # to catch invalid files before performing mutating operations. try: service_account_key_data = hub_util.Base64EncodedFileContents( args.service_account_key_file) except files.Error as e: raise exceptions.Error('Could not process {}: {}'.format( SERVICE_ACCOUNT_KEY_FILE_FLAG, e)) docker_credential_data = None if args.docker_credential_file: try: docker_credential_data = hub_util.Base64EncodedFileContents( args.docker_credential_file) except files.Error as e: raise exceptions.Error('Could not process {}: {}'.format( DOCKER_CREDENTIAL_FILE_FLAG, e)) gke_cluster_self_link = api_util.GKEClusterSelfLink(args) # Attempt to create a membership. already_exists = False obj = None # For backward compatiblity, check if a membership was previously created # using the cluster uuid. parent = api_util.ParentRef(project, 'global') membership_id = uuid resource_name = api_util.MembershipRef(project, 'global', uuid) obj = self._CheckMembershipWithUUID(resource_name, args.CLUSTER_NAME) if obj: # The membership exists and has the same description. already_exists = True else: # Attempt to create a new membership using cluster_name. membership_id = args.CLUSTER_NAME resource_name = api_util.MembershipRef(project, 'global', args.CLUSTER_NAME) try: self._VerifyClusterExclusivity(kube_client, parent, membership_id) obj = api_util.CreateMembership(project, args.CLUSTER_NAME, args.CLUSTER_NAME, gke_cluster_self_link, uuid, self.ReleaseTrack()) except apitools_exceptions.HttpConflictError as e: # If the error is not due to the object already existing, re-raise. error = core_api_exceptions.HttpErrorPayload(e) if error.status_description != 'ALREADY_EXISTS': raise # The membership exists with same cluster_name. already_exists = True obj = api_util.GetMembership(resource_name, self.ReleaseTrack()) # In case of an existing membership, check with the user to upgrade the # Connect-Agent. if already_exists: console_io.PromptContinue( message='A membership for [{}] already exists. Continuing will ' 'reinstall the Connect agent deployment to use a new image (if one ' 'is available).'.format(resource_name), cancel_on_no=True) # No membership exists. Attempt to create a new one, and install a new # agent. try: self._InstallOrUpdateExclusivityArtifacts(kube_client, resource_name) agent_util.DeployConnectAgent(args, service_account_key_data, docker_credential_data, resource_name, self.ReleaseTrack()) except: # In case of a new membership, we need to clean up membership and # resources if we failed to install the Connect Agent. if not already_exists: api_util.DeleteMembership(resource_name, self.ReleaseTrack()) exclusivity_util.DeleteMembershipResources(kube_client) raise return obj
def _UpdateOrCreateService(self, service_ref, config_changes, with_code, private_endpoint=None): """Apply config_changes to the service. Create it if necessary. Arguments: service_ref: Reference to the service to create or update config_changes: list of ConfigChanger to modify the service with with_code: bool, True if the config_changes contains code to deploy. We can't create the service if we're not deploying code. private_endpoint: bool, True if creating a new Service for Cloud Run on GKE that should only be addressable from within the cluster. False if it should be publicly addressable. None if its existing visibility should remain unchanged. Returns: The Service object we created or modified. """ nonce = _Nonce() config_changes = [_NewRevisionForcingChange(nonce)] + config_changes messages = self._messages_module # GET the Service serv = self.GetService(service_ref) try: if serv: if not with_code: # Avoid changing the running code by making the new revision by digest self._EnsureImageDigest(serv, config_changes) if private_endpoint is None: # Don't change the existing service visibility pass elif private_endpoint: serv.labels[ service.ENDPOINT_VISIBILITY] = service.CLUSTER_LOCAL else: del serv.labels[service.ENDPOINT_VISIBILITY] # PUT the changed Service for config_change in config_changes: config_change.AdjustConfiguration(serv.configuration, serv.metadata) serv_name = service_ref.RelativeName() serv_update_req = ( messages.RunNamespacesServicesReplaceServiceRequest( service=serv.Message(), name=serv_name)) with metrics.RecordDuration(metric_names.UPDATE_SERVICE): updated = self._client.namespaces_services.ReplaceService( serv_update_req) return service.Service(updated, messages) else: if not with_code: raise serverless_exceptions.ServiceNotFoundError( 'Service [{}] could not be found.'.format( service_ref.servicesId)) # POST a new Service new_serv = service.Service.New(self._client, service_ref.namespacesId, private_endpoint) new_serv.name = service_ref.servicesId parent = service_ref.Parent().RelativeName() for config_change in config_changes: config_change.AdjustConfiguration(new_serv.configuration, new_serv.metadata) serv_create_req = (messages.RunNamespacesServicesCreateRequest( service=new_serv.Message(), parent=parent)) with metrics.RecordDuration(metric_names.CREATE_SERVICE): raw_service = self._client.namespaces_services.Create( serv_create_req) return service.Service(raw_service, messages) except api_exceptions.HttpBadRequestError as e: error_payload = exceptions_util.HttpErrorPayload(e) if error_payload.field_violations: if (serverless_exceptions.BadImageError.IMAGE_ERROR_FIELD in error_payload.field_violations): exceptions.reraise(serverless_exceptions.BadImageError(e)) exceptions.reraise(e) except api_exceptions.HttpNotFoundError as e: error_msg = 'Deployment endpoint was not found.' if not self._region: all_clusters = global_methods.ListClusters() clusters = [ '* {} in {}'.format(c.name, c.zone) for c in all_clusters ] error_msg += ( ' Perhaps the provided cluster was invalid or ' 'does not have Cloud Run enabled. Pass the ' '`--cluster` and `--cluster-location` flags or set the ' '`run/cluster` and `run/cluster_location` properties to ' 'a valid cluster and zone and retry.' '\nAvailable clusters:\n{}'.format('\n'.join(clusters))) else: all_regions = global_methods.ListRegions(self._op_client) if self._region not in all_regions: regions = ['* {}'.format(r) for r in all_regions] error_msg += ( ' The provided region was invalid. ' 'Pass the `--region` flag or set the ' '`run/region` property to a valid region and retry.' '\nAvailable regions:\n{}'.format('\n'.join(regions))) raise serverless_exceptions.DeploymentFailedError(error_msg)
def Run(self, args): project = arg_utils.GetFromNamespace(args, '--project', use_defaults=True) # This incidentally verifies that the kubeconfig and context args are valid. with kube_util.KubernetesClient(args) as kube_client: kube_client.CheckClusterAdminPermissions() kube_util.ValidateClusterIdentifierFlags(kube_client, args) uuid = kube_util.GetClusterUUID(kube_client) # Read the service account files provided in the arguments early, in order # to catch invalid files before performing mutating operations. # Service Account key file is required if Workload Identity is not # enabled. # If Workload Identity is enabled, then the Connect Agent uses # a Kubernetes Service Account token instead and hence a GCP Service # Account key is not required. service_account_key_data = '' if args.service_account_key_file: try: service_account_key_data = hub_util.Base64EncodedFileContents( args.service_account_key_file) except files.Error as e: raise exceptions.Error('Could not process {}: {}'.format( SERVICE_ACCOUNT_KEY_FILE_FLAG, e)) docker_credential_data = None if args.docker_credential_file: try: docker_credential_data = hub_util.Base64EncodedFileContents( args.docker_credential_file) except files.Error as e: raise exceptions.Error('Could not process {}: {}'.format( DOCKER_CREDENTIAL_FILE_FLAG, e)) gke_cluster_self_link = kube_client.processor.gke_cluster_self_link issuer_url = None private_keyset_json = None if args.enable_workload_identity: # public_issuer_url can be None or given by user or gke_cluster_uri # (incase of a gke cluster). # args.public_issuer_url takes precedence over gke_cluster_uri. public_issuer_url = args.public_issuer_url or kube_client.processor.gke_cluster_uri or None try: openid_config_json = six.ensure_str( kube_client.GetOpenIDConfiguration( issuer_url=public_issuer_url), encoding='utf-8') except Exception as e: # pylint: disable=broad-except raise exceptions.Error( 'Error getting the OpenID Provider Configuration: ' '{}'.format(e)) # Extract the issuer URL from the discovery doc. issuer_url = json.loads(openid_config_json).get('issuer') if not issuer_url: raise exceptions.Error( 'Invalid OpenID Config: ' 'missing issuer: {}'.format(openid_config_json)) # Ensure public_issuer_url (only non-empty) matches what came back in # the discovery doc. if public_issuer_url and (public_issuer_url != issuer_url): raise exceptions.Error( '--public-issuer-url {} did not match issuer ' 'returned in discovery doc: {}'.format( public_issuer_url, issuer_url)) # Request the JWKS from the cluster if we need it (either for setting # up the GCS bucket or getting public keys for private issuers). In # the private issuer case, we set private_keyset_json, which is used # later to upload the JWKS in the Hub Membership. if self.ReleaseTrack() is base.ReleaseTrack.ALPHA: if args.manage_workload_identity_bucket: api_util.CreateWorkloadIdentityBucket( project, issuer_url, openid_config_json, kube_client.GetOpenIDKeyset()) elif args.has_private_issuer: private_keyset_json = kube_client.GetOpenIDKeyset() # Attempt to create a membership. already_exists = False obj = None # For backward compatiblity, check if a membership was previously created # using the cluster uuid. parent = api_util.ParentRef(project, 'global') membership_id = uuid resource_name = api_util.MembershipRef(project, 'global', uuid) obj = self._CheckMembershipWithUUID(resource_name, args.CLUSTER_NAME) if obj: # The membership exists and has the same description. already_exists = True else: # Attempt to create a new membership using cluster_name. membership_id = args.CLUSTER_NAME resource_name = api_util.MembershipRef(project, 'global', args.CLUSTER_NAME) try: self._VerifyClusterExclusivity(kube_client, parent, membership_id) obj = api_util.CreateMembership( project, args.CLUSTER_NAME, args.CLUSTER_NAME, gke_cluster_self_link, uuid, self.ReleaseTrack(), issuer_url, private_keyset_json) except apitools_exceptions.HttpConflictError as e: # If the error is not due to the object already existing, re-raise. error = core_api_exceptions.HttpErrorPayload(e) if error.status_description != 'ALREADY_EXISTS': raise obj = api_util.GetMembership(resource_name, self.ReleaseTrack()) if not obj.externalId: raise exceptions.Error( 'invalid membership {0} does not have ' 'external_id field set. We cannot determine ' 'if registration is requested against a ' 'valid existing Membership. Consult the ' 'documentation on container hub memberships ' 'update for more information or run gcloud ' 'container hub memberships delete {0} if you ' 'are sure that this is an invalid or ' 'otherwise stale Membership'.format(membership_id)) if obj.externalId != uuid: raise exceptions.Error( 'membership {0} already exists in the project' ' with another cluster. If this operation is' ' intended, please run `gcloud container ' 'hub memberships delete {0}` and register ' 'again.'.format(membership_id)) # The membership exists with same cluster_name. already_exists = True # In case of an existing membership, check with the user to upgrade the # Connect-Agent. if already_exists: # Update Membership when required. Scenarios that require updates: # 1. membership.authority is set, but there is now no issuer URL. # This means the user is disabling Workload Identity. # 2. membership.authority is not set, but there is now an # issuer URL. This means the user is enabling Workload Identity. # 3. membership.authority is set, but the issuer URL is different # from that set in membership.authority.issuer. This is technically # an error, but we defer to validation in the API. # 4. membership.authority.oidcJwks is set, but the private keyset # we got from the cluster differs from the keyset in the membership. # This means the user is updating the public keys, and we should # update to the latest keyset in the membership. if ( # scenario 1, disabling WI (obj.authority and not issuer_url) or # scenario 2, enabling WI (issuer_url and not obj.authority) or (obj.authority and # scenario 3, issuer changed ((obj.authority.issuer != issuer_url) or # scenario 4, JWKS changed (private_keyset_json and obj.authority.oidcJwks and (obj.authority.oidcJwks.decode('utf-8') != private_keyset_json))))): console_io.PromptContinue( message=hub_util.GenerateWIUpdateMsgString( obj, issuer_url, resource_name, args.CLUSTER_NAME), cancel_on_no=True) try: api_util.UpdateMembership( resource_name, obj, 'authority', self.ReleaseTrack(), issuer_url=issuer_url, oidc_jwks=private_keyset_json) log.status.Print( 'Updated the membership [{}] for the cluster [{}]'. format(resource_name, args.CLUSTER_NAME)) except Exception as e: raise exceptions.Error( 'Error in updating the membership [{}]:{}'.format( resource_name, e)) else: console_io.PromptContinue( message= 'A membership [{}] for the cluster [{}] already exists. ' 'Continuing will reinstall the Connect agent deployment to use a ' 'new image (if one is available).'.format( resource_name, args.CLUSTER_NAME), cancel_on_no=True) else: log.status.Print( 'Created a new membership [{}] for the cluster [{}]'. format(resource_name, args.CLUSTER_NAME)) # Attempt to update the existing agent deployment, or install a new agent # if necessary. try: self._InstallOrUpdateExclusivityArtifacts( kube_client, resource_name) agent_util.DeployConnectAgent(kube_client, args, service_account_key_data, docker_credential_data, resource_name, self.ReleaseTrack()) except Exception as e: log.status.Print( 'Error in installing the Connect Agent: {}'.format(e)) # In case of a new membership, we need to clean up membership and # resources if we failed to install the Connect Agent. if not already_exists: api_util.DeleteMembership(resource_name, self.ReleaseTrack()) exclusivity_util.DeleteMembershipResources(kube_client) raise log.status.Print( 'Finished registering the cluster [{}] with the Hub.'.format( args.CLUSTER_NAME)) return obj
def _UpdateOrCreateService(self, service_ref, config_changes, with_code, serv): """Apply config_changes to the service. Create it if necessary. Arguments: service_ref: Reference to the service to create or update config_changes: list of ConfigChanger to modify the service with with_code: bool, True if the config_changes contains code to deploy. We can't create the service if we're not deploying code. serv: service.Service, For update the Service to update and for create None. Returns: The Service object we created or modified. """ messages = self._messages_module config_changes = [_SetClientNameAndVersion()] + config_changes try: if serv: if not with_code: # Avoid changing the running code by making the new revision by digest self._EnsureImageDigest(serv, config_changes) # Revision names must be unique across the namespace. # To prevent the revision name being unchanged from the last revision, # we reset the value so the default naming scheme will be used instead. serv.template.name = None # PUT the changed Service for config_change in config_changes: serv = config_change.Adjust(serv) serv_name = service_ref.RelativeName() serv_update_req = ( messages.RunNamespacesServicesReplaceServiceRequest( service=serv.Message(), name=serv_name)) with metrics.RecordDuration(metric_names.UPDATE_SERVICE): updated = self._client.namespaces_services.ReplaceService( serv_update_req) return service.Service(updated, messages) else: if not with_code: raise serverless_exceptions.ServiceNotFoundError( 'Service [{}] could not be found.'.format( service_ref.servicesId)) # POST a new Service new_serv = service.Service.New(self._client, service_ref.namespacesId) new_serv.name = service_ref.servicesId parent = service_ref.Parent().RelativeName() for config_change in config_changes: new_serv = config_change.Adjust(new_serv) serv_create_req = (messages.RunNamespacesServicesCreateRequest( service=new_serv.Message(), parent=parent)) with metrics.RecordDuration(metric_names.CREATE_SERVICE): raw_service = self._client.namespaces_services.Create( serv_create_req) return service.Service(raw_service, messages) except api_exceptions.HttpBadRequestError as e: error_payload = exceptions_util.HttpErrorPayload(e) if error_payload.field_violations: if (serverless_exceptions.BadImageError.IMAGE_ERROR_FIELD in error_payload.field_violations): exceptions.reraise(serverless_exceptions.BadImageError(e)) elif (serverless_exceptions.MalformedLabelError. LABEL_ERROR_FIELD in error_payload.field_violations): exceptions.reraise( serverless_exceptions.MalformedLabelError(e)) exceptions.reraise(e) except api_exceptions.HttpNotFoundError as e: platform = properties.VALUES.run.platform.Get() error_msg = 'Deployment endpoint was not found.' if platform == 'gke': all_clusters = global_methods.ListClusters() clusters = [ '* {} in {}'.format(c.name, c.zone) for c in all_clusters ] error_msg += ( ' Perhaps the provided cluster was invalid or ' 'does not have Cloud Run enabled. Pass the ' '`--cluster` and `--cluster-location` flags or set the ' '`run/cluster` and `run/cluster_location` properties to ' 'a valid cluster and zone and retry.' '\nAvailable clusters:\n{}'.format('\n'.join(clusters))) elif platform == 'managed': all_regions = global_methods.ListRegions(self._op_client) if self._region not in all_regions: regions = ['* {}'.format(r) for r in all_regions] error_msg += ( ' The provided region was invalid. ' 'Pass the `--region` flag or set the ' '`run/region` property to a valid region and retry.' '\nAvailable regions:\n{}'.format('\n'.join(regions))) elif platform == 'kubernetes': error_msg += ( ' Perhaps the provided cluster was invalid or ' 'does not have Cloud Run enabled. Ensure in your ' 'kubeconfig file that the cluster referenced in ' 'the current context or the specified context ' 'is a valid cluster and retry.') raise serverless_exceptions.DeploymentFailedError(error_msg) except api_exceptions.HttpError as e: k8s_error = serverless_exceptions.KubernetesExceptionParser(e) causes = '\n\n'.join([c['message'] for c in k8s_error.causes]) if not causes: causes = k8s_error.error raise serverless_exceptions.KubernetesError( 'Error{}:\n{}\n'.format( 's' if len(k8s_error.causes) > 1 else '', causes))