feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
topic-name:
api_field: topicId
arg_name: topic
required: true
is_positional: true
help_text: |
Topic ID.
subscription-name:
api_field: subscriptionId
arg_name: subscription
is_positional: true
required: true
help_text: |
Subscription ID.
reservation-name:
api_field: reservationId
arg_name: reservation
required: true
is_positional: true
help_text: |
Reservation ID.
subscription-topic-name:
api_field: subscription.topic
arg_name: topic
required: true
help_text: |
Topic ID associated with the subscription.
topic-throughput-reservation-name:
api_field: topic.reservationConfig.throughputReservation
arg_name: throughput-reservation
is_positional: false
help_text: |
Reservation ID to use for topic throughput.
partitions:
api_field: topic.partitionConfig.count
arg_name: partitions
help_text: |
Number of partitions in the topic.
per-partition-publish-mib:
api_field: topic.partitionConfig.capacity.publishMibPerSec
arg_name: per-partition-publish-mib
help_text: |
Topic partition publish throughput capacity in MiB/s. Must be between 4 and 16.
per-partition-subscribe-mib:
api_field: topic.partitionConfig.capacity.subscribeMibPerSec
arg_name: per-partition-subscribe-mib
help_text: |
Topic partition subscribe throughput capacity in MiB/s. Must be between 4 and 32.
per-partition-bytes:
api_field: topic.retentionConfig.perPartitionBytes
arg_name: per-partition-bytes
type: googlecloudsdk.core.util.scaled_integer:ParseInteger
help_text: |
Provisioned storage, in bytes, per partition. If the number of bytes
stored in any of the topic's partitions exceeds this value, older
messages will be dropped to make room for newer ones, regardless of the
value of `message-retention-period`.
A valid example value of this flag would be `per-partition-bytes=30GiB`.
message-retention-period:
api_field: topic.retentionConfig.period
arg_name: message-retention-period
type: googlecloudsdk.core.util.times:ParseDuration
processor: googlecloudsdk.command_lib.pubsub.lite_util:DurationToSeconds
help_text: |
How long a published message is retained. If unset, messages will only be
dropped to make space for new ones once the `per-partition-bytes` limit is
reached.
A valid example value of this flag would be `message-retention-period="2w"`.
delivery-requirement:
api_field: subscription.deliveryConfig.deliveryRequirement
arg_name: delivery-requirement
choices:
- arg_value: deliver-immediately
enum_value: DELIVER_IMMEDIATELY
- arg_value: deliver-after-stored
enum_value: DELIVER_AFTER_STORED
help_text: |
When this subscription should send messages to subscribers relative to
messages persistence in storage.
See https://cloud.google.com/pubsub/lite/docs/subscriptions#creating_lite_subscriptions
for more info.
throughput-capacity:
api_field: reservation.throughputCapacity
arg_name: throughput-capacity
help_text: |
Reservation throughput capacity. Every unit of throughput capacity is equivalent to 1 MiB/s of
published messages or 2 MiB/s of subscribed messages.
starting-offset:
arg_name: starting-offset
choices:
- arg_value: beginning
- arg_value: end
type: googlecloudsdk.command_lib.util.hooks.types:LowerCaseType
help_text: |
The offset at which a newly created or seeked subscription starts receiving messages. A
subscription can be initialized at the offset of the oldest retained message (`beginning`), or
at the current HEAD offset (`end`).
publish-time:
arg_name: publish-time
type: googlecloudsdk.calliope.arg_parsers:Datetime.Parse
help_text: |
The publish time to which you seek a subscription. Messages with publish time greater than or
equal to the specified time are delivered after the seek operation.
Run $ gcloud topic datetimes for information on time formats.
event-time:
arg_name: event-time
type: googlecloudsdk.calliope.arg_parsers:Datetime.Parse
help_text: |
The event time to which you seek a subscription. The subscription seeks to the first message
with event time greater than or equal to the specified event time. Messages missing an event
time use publish time as a fallback. As event times are user supplied, subsequent messages may
have event times less than the specified event time and must be filtered by the client, if
necessary.
Run $ gcloud topic datetimes for information on time formats.
operation-done:
arg_name: done
# Not a bool type to allow one of: true|false|unspecified
choices:
- arg_value: 'true'
- arg_value: 'false'
type: googlecloudsdk.command_lib.util.hooks.types:LowerCaseType
help_text: |
Filter operations by completion status. This flag is ignored if `--filter` is set.
operation-subscription:
arg_name: subscription
help_text: |
Filter operations by target subscription. This flag is ignored if `--filter` is set.
partition:
api_field: commitCursorRequest.partition
arg_name: partition
required: true
help_text: |
The topic partition. Partitions are zero indexed, so the partition must be in the range
[0, topic.num_partitions). If you do not know your topic.num_partitions, run `gcloud pubsub
lite-topic describe TOPIC --location=ZONE`.
offset:
api_field: commitCursorRequest.cursor.offset
arg_name: offset
required: true
help_text: |
The offset of a message within a topic partition. Must be greater than or equal to 0.
export-pubsub-topic:
api_field: subscription.exportConfig.pubsubConfig.topic
arg_name: export-pubsub-topic
help_text: |
The name of the destination Pub/Sub topic to which messages are exported. Must be the topic's
fully specified path if it is not in the same project as the subscription to be created.
export-desired-state:
api_field: subscription.exportConfig.desiredState
arg_name: export-desired-state
choices:
- arg_value: active
enum_value: ACTIVE
- arg_value: paused
enum_value: PAUSED
type: googlecloudsdk.command_lib.util.hooks.types:LowerCaseType
help_text: |
The desired state of the export. Process messages by setting the value to ACTIVE or pause
message processing by setting the value to PAUSED.
export-dead-letter-topic:
api_field: subscription.exportConfig.deadLetterTopic
arg_name: export-dead-letter-topic
help_text: |
The name of the Pub/Sub Lite topic to write messages that cannot be exported. Must be in the
same project and location as the subscription to be created. Note that this is a Lite topic.
# Deprecated flag, do not use
zone:
arg_name: zone
hidden: true
help_text: |
ID of the location of the Pub/Sub Lite resource.
action:
deprecated:
removed: false
warn: |
zone is deprecated and will be removed in an upcoming release. Please use --location instead.

View File

@@ -0,0 +1,611 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""A library that is used to support Cloud Pub/Sub Lite commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import sys
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.command_lib.pubsub import util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_io
import six
from six.moves.urllib.parse import urlparse
# Resource path constants
PROJECTS_RESOURCE_PATH = 'projects/'
LOCATIONS_RESOURCE_PATH = 'locations/'
RESERVATIONS_RESOURCE_PATH = 'reservations/'
TOPICS_RESOURCE_PATH = 'topics/'
SUBSCRIPTIONS_RESOURCE_PATH = 'subscriptions/'
PUBSUBLITE_API_NAME = 'pubsublite'
PUBSUBLITE_API_VERSION = 'v1'
class UnexpectedResourceField(exceptions.Error):
"""Error for having and unknown resource field."""
class InvalidPythonVersion(exceptions.Error):
"""Error for an invalid python version."""
class NoGrpcInstalled(exceptions.Error):
"""Error that occurs when the grpc module is not installed."""
def __init__(self):
super(NoGrpcInstalled, self).__init__(
'Please ensure that the gRPC module is installed and the environment '
'is correctly configured. Run `sudo pip3 install grpcio` and set the '
'environment variable CLOUDSDK_PYTHON_SITEPACKAGES=1.')
class InvalidSeekTarget(exceptions.Error):
"""Error for specifying an invalid seek target."""
class InvalidResourcePath(exceptions.Error):
"""Error for specifying an invalid fully qualified resource path."""
def PubsubLiteClient():
"""Returns the Pub/Sub Lite v1 client module."""
return apis.GetClientInstance(PUBSUBLITE_API_NAME, PUBSUBLITE_API_VERSION)
def PubsubLiteMessages():
"""Returns the Pub/Sub Lite v1 messages module."""
return apis.GetMessagesModule(PUBSUBLITE_API_NAME, PUBSUBLITE_API_VERSION)
def DurationToSeconds(duration):
"""Convert Duration object to total seconds for backend compatibility."""
return six.text_type(int(duration.total_seconds)) + 's'
def DeriveRegionFromLocation(location):
"""Returns the region from a location string.
Args:
location: A str of the form `<region>-<zone>` or `<region>`, such as
`us-central1-a` or `us-central1`. Any other form will cause undefined
behavior.
Returns:
The str region. Example: `us-central1`.
"""
splits = location.split('-')
return '-'.join(splits[:2])
def DeriveRegionFromEndpoint(endpoint):
"""Returns the region from a endpoint string.
Args:
endpoint: A str of the form `https://<region-><environment->base.url.com/`.
Example `https://us-central-base.url.com/`,
`https://us-central-autopush-base.url.com/`, or `https://base.url.com/`.
Returns:
The str region if it exists, otherwise None.
"""
parsed = urlparse(endpoint)
dash_splits = parsed.netloc.split('-')
if len(dash_splits) > 2:
return dash_splits[0] + '-' + dash_splits[1]
else:
return None
def CreateRegionalEndpoint(region, url):
"""Returns a new endpoint string with the defined `region` prefixed to the netlocation."""
url_parts = url.split('://')
url_scheme = url_parts[0]
return url_scheme + '://' + region + '-' + url_parts[1]
def RemoveRegionFromEndpoint(endpoint):
"""Returns a new endpoint string stripped of the region if one exists."""
region = DeriveRegionFromEndpoint(endpoint)
if region:
endpoint = endpoint.replace(region + '-', '')
return endpoint
def GetResourceInfo(request):
"""Returns a tuple of the resource and resource name from the `request`.
Args:
request: A Request object instance.
Returns:
A tuple of the resource string path and the resource name.
Raises:
UnexpectedResourceField: The `request` had a unsupported resource.
"""
resource = ''
resource_name = ''
if hasattr(request, 'parent'):
resource = request.parent
resource_name = 'parent'
elif hasattr(request, 'name'):
resource = request.name
resource_name = 'name'
elif hasattr(request, 'subscription'):
resource = request.subscription
resource_name = 'subscription'
else:
raise UnexpectedResourceField(
'The resource specified for this command is unknown!')
return resource, resource_name
def LocationToZoneOrRegion(location_id):
# pylint: disable=g-import-not-at-top
from google.cloud.pubsublite import types
# pylint: enable=g-import-not-at-top
if len(location_id.split('-')) == 3:
return types.CloudZone.parse(location_id)
else:
return types.CloudRegion.parse(location_id)
def DeriveLocationFromResource(resource):
"""Returns the location from a resource string."""
location = resource[resource.index(LOCATIONS_RESOURCE_PATH) +
len(LOCATIONS_RESOURCE_PATH):]
location = location.split('/')[0]
return location
def DeriveProjectFromResource(resource):
"""Returns the project from a resource string."""
project = resource[resource.index(PROJECTS_RESOURCE_PATH) +
len(PROJECTS_RESOURCE_PATH):]
project = project.split('/')[0]
return project
def ParseResource(request):
"""Returns an updated `request` with the resource path parsed."""
resource, resource_name = GetResourceInfo(request)
new_resource = resource[resource.rindex(PROJECTS_RESOURCE_PATH):]
setattr(request, resource_name, new_resource)
return request
def OverrideEndpointWithRegion(request):
"""Sets the pubsublite endpoint override to include the region."""
resource, _ = GetResourceInfo(request)
region = DeriveRegionFromLocation(DeriveLocationFromResource(resource))
endpoint = apis.GetEffectiveApiEndpoint(PUBSUBLITE_API_NAME,
PUBSUBLITE_API_VERSION)
# Remove any region from the endpoint in case it was previously set.
# Specifically this effects scenario tests where multiple tests are run in a
# single instance.
endpoint = RemoveRegionFromEndpoint(endpoint)
regional_endpoint = CreateRegionalEndpoint(region, endpoint)
properties.VALUES.api_endpoint_overrides.pubsublite.Set(regional_endpoint)
def ProjectIdToProjectNumber(project_id):
"""Returns the Cloud project number associated with the `project_id`."""
crm_message_module = apis.GetMessagesModule('cloudresourcemanager', 'v1')
resource_manager = apis.GetClientInstance('cloudresourcemanager', 'v1')
req = crm_message_module.CloudresourcemanagerProjectsGetRequest(
projectId=project_id)
project = resource_manager.projects.Get(req)
return project.projectNumber
def OverrideProjectIdToProjectNumber(request):
"""Returns an updated `request` with the Cloud project number."""
resource, resource_name = GetResourceInfo(request)
project_id = DeriveProjectFromResource(resource)
project_number = ProjectIdToProjectNumber(project_id)
setattr(request, resource_name,
resource.replace(project_id, six.text_type(project_number)))
return request
def UpdateAdminRequest(resource_ref, args, request):
"""Returns an updated `request` with values for a valid Admin request."""
# Unused resource reference.
del resource_ref, args
request = ParseResource(request)
request = OverrideProjectIdToProjectNumber(request)
OverrideEndpointWithRegion(request)
return request
def UpdateCommitCursorRequest(resource_ref, args, request):
"""Updates a CommitCursorRequest by adding 1 to the provided offset."""
# Unused resource reference.
del resource_ref, args
request = ParseResource(request)
# Add 1 to the offset so that the message corresponding to the provided offset
# is included in the list of messages that are acknowledged.
request.commitCursorRequest.cursor.offset += 1
OverrideEndpointWithRegion(request)
return request
def _HasReservation(topic):
"""Returns whether the topic has a reservation set."""
if topic.reservationConfig is None:
return False
return bool(topic.reservationConfig.throughputReservation)
def AddTopicDefaultsWithoutReservation(resource_ref, args, request):
"""Adds the default values for topic throughput fields with no reservation."""
# Unused resource reference and arguments.
del resource_ref, args
topic = request.topic
if not _HasReservation(topic):
if topic.partitionConfig is None:
topic.partitionConfig = {}
if topic.partitionConfig.capacity is None:
topic.partitionConfig.capacity = {}
capacity = topic.partitionConfig.capacity
if capacity.publishMibPerSec is None:
capacity.publishMibPerSec = 4
if capacity.subscribeMibPerSec is None:
capacity.subscribeMibPerSec = 8
return request
def AddTopicReservationResource(resource_ref, args, request):
"""Returns an updated `request` with a resource path on the reservation."""
# Unused resource reference and arguments.
del resource_ref, args
topic = request.topic
if not _HasReservation(topic):
return request
resource, _ = GetResourceInfo(request)
project = DeriveProjectFromResource(resource)
region = DeriveRegionFromLocation(DeriveLocationFromResource(resource))
reservation = topic.reservationConfig.throughputReservation
request.topic.reservationConfig.throughputReservation = (
'{}{}/{}{}/{}{}'.format(
PROJECTS_RESOURCE_PATH, project, LOCATIONS_RESOURCE_PATH, region,
RESERVATIONS_RESOURCE_PATH, reservation))
return request
def AddSubscriptionTopicResource(resource_ref, args, request):
"""Returns an updated `request` with a resource path on the topic."""
# Unused resource reference and arguments.
del resource_ref, args
resource, _ = GetResourceInfo(request)
request.subscription.topic = '{}/{}{}'.format(resource, TOPICS_RESOURCE_PATH,
request.subscription.topic)
return request
def ConfirmPartitionsUpdate(resource_ref, args, request):
"""Prompts to confirm an update to a topic's partition count."""
del resource_ref
if 'partitions' not in args or not args.partitions:
return request
console_io.PromptContinue(
message=(
'Warning: The number of partitions in a topic can be increased but'
' not decreased. Additionally message order is not guaranteed across'
' a topic resize. See'
' https://cloud.google.com/pubsub/lite/docs/topics#scaling_capacity'
' for more details'),
default=True,
cancel_on_no=True)
return request
def UpdateSkipBacklogField(resource_ref, args, request):
# Unused resource reference
del resource_ref
if hasattr(args, 'starting_offset'):
request.skipBacklog = (args.starting_offset == 'end')
return request
def GetLocationValue(args):
"""Returns the raw location argument."""
return args.location or args.zone
def GetLocation(args):
"""Returns the resource location (zone or region) extracted from arguments.
Args:
args: argparse.Namespace, the parsed commandline arguments.
Raises:
InvalidResourcePath: if the location component in a fully qualified path is
invalid.
"""
location = GetLocationValue(args)
if LOCATIONS_RESOURCE_PATH not in location:
return location
parsed_location = DeriveLocationFromResource(location)
if not parsed_location:
raise InvalidResourcePath(
'The location component in the specified location path is invalid: `{}`.'
.format(location))
return parsed_location
def GetProject(args):
"""Returns the project from either arguments or attributes.
Args:
args: argparse.Namespace, the parsed commandline arguments.
Raises:
InvalidResourcePath: if the project component in a fully qualified path is
invalid.
"""
location = GetLocationValue(args)
if not location.startswith(PROJECTS_RESOURCE_PATH):
return args.project or properties.VALUES.core.project.GetOrFail()
parsed_project = DeriveProjectFromResource(location)
if not parsed_project:
raise InvalidResourcePath(
'The project component in the specified location path is invalid: `{}`.'
.format(location))
return parsed_project
def GetDeliveryRequirement(args, psl):
"""Returns the DeliveryRequirement enum from arguments."""
if args.delivery_requirement == 'deliver-after-stored':
return psl.DeliveryConfig.DeliveryRequirementValueValuesEnum.DELIVER_AFTER_STORED
return psl.DeliveryConfig.DeliveryRequirementValueValuesEnum.DELIVER_IMMEDIATELY
def GetDesiredExportState(args, psl):
"""Returns the export DesiredState enum from arguments."""
if args.export_desired_state == 'paused':
return psl.ExportConfig.DesiredStateValueValuesEnum.PAUSED
return psl.ExportConfig.DesiredStateValueValuesEnum.ACTIVE
def GetSeekRequest(args, psl):
"""Returns a SeekSubscriptionRequest from arguments."""
if args.publish_time:
return psl.SeekSubscriptionRequest(
timeTarget=psl.TimeTarget(
publishTime=util.FormatSeekTime(args.publish_time)))
elif args.event_time:
return psl.SeekSubscriptionRequest(
timeTarget=psl.TimeTarget(
eventTime=util.FormatSeekTime(args.event_time)))
elif args.starting_offset:
if args.starting_offset == 'beginning':
return psl.SeekSubscriptionRequest(namedTarget=psl.SeekSubscriptionRequest
.NamedTargetValueValuesEnum.TAIL)
elif args.starting_offset == 'end':
return psl.SeekSubscriptionRequest(namedTarget=psl.SeekSubscriptionRequest
.NamedTargetValueValuesEnum.HEAD)
else:
# Should already be validated.
raise InvalidSeekTarget(
'Invalid starting offset value! Must be one of: [beginning, end].')
else:
# Should already be validated.
raise InvalidSeekTarget('Seek target must be specified!')
def SetExportConfigResources(args, psl, project, location, export_config):
"""Sets fully qualified resource paths for an ExportConfig."""
if args.export_pubsub_topic:
topic = args.export_pubsub_topic
if not topic.startswith(PROJECTS_RESOURCE_PATH):
topic = ('{}{}/{}{}'.format(PROJECTS_RESOURCE_PATH, project,
TOPICS_RESOURCE_PATH, topic))
export_config.pubsubConfig = psl.PubSubConfig(topic=topic)
if args.export_dead_letter_topic:
topic = args.export_dead_letter_topic
if not topic.startswith(PROJECTS_RESOURCE_PATH):
topic = ('{}{}/{}{}/{}{}'.format(PROJECTS_RESOURCE_PATH, project,
LOCATIONS_RESOURCE_PATH, location,
TOPICS_RESOURCE_PATH, topic))
export_config.deadLetterTopic = topic
def GetExportConfig(args, psl, project, location, requires_seek):
"""Returns an ExportConfig from arguments."""
if args.export_pubsub_topic is None:
return None
desired_state = GetDesiredExportState(args, psl)
if requires_seek:
# Will be updated to Active after seek.
desired_state = psl.ExportConfig.DesiredStateValueValuesEnum.PAUSED
export_config = psl.ExportConfig(desiredState=desired_state)
SetExportConfigResources(args, psl, project, location, export_config)
return export_config
def ExecuteCreateSubscriptionRequest(resource_ref, args):
"""Issues a CreateSubscriptionRequest and potentially other requests.
Args:
resource_ref: resources.Resource, the resource reference for the resource
being operated on.
args: argparse.Namespace, the parsed commandline arguments.
Returns:
The created Pub/Sub Lite Subscription.
"""
psl = PubsubLiteMessages()
location = GetLocation(args)
project_id = GetProject(args)
project_number = six.text_type(ProjectIdToProjectNumber(project_id))
requires_seek = args.publish_time or args.event_time
# Request 1 - Create the subscription.
create_request = psl.PubsubliteAdminProjectsLocationsSubscriptionsCreateRequest(
parent=('{}{}/{}{}'.format(PROJECTS_RESOURCE_PATH, project_number,
LOCATIONS_RESOURCE_PATH, location)),
subscription=psl.Subscription(
topic=args.topic,
deliveryConfig=psl.DeliveryConfig(
deliveryRequirement=GetDeliveryRequirement(args, psl)),
exportConfig=GetExportConfig(args, psl, project_number, location,
requires_seek)),
subscriptionId=args.subscription)
OverrideEndpointWithRegion(create_request)
AddSubscriptionTopicResource(resource_ref, args, create_request)
if not requires_seek:
UpdateSkipBacklogField(resource_ref, args, create_request)
client = PubsubLiteClient()
response = client.admin_projects_locations_subscriptions.Create(
create_request)
# Request 2 (optional) - seek the subscription.
if requires_seek:
seek_request = psl.PubsubliteAdminProjectsLocationsSubscriptionsSeekRequest(
name=response.name, seekSubscriptionRequest=GetSeekRequest(args, psl))
client.admin_projects_locations_subscriptions.Seek(seek_request)
# Request 3 (optional) - make the export subscription active.
if requires_seek and create_request.subscription.exportConfig and args.export_desired_state == 'active':
update_request = psl.PubsubliteAdminProjectsLocationsSubscriptionsPatchRequest(
name=response.name,
updateMask='export_config.desired_state',
subscription=psl.Subscription(
exportConfig=psl.ExportConfig(desiredState=psl.ExportConfig
.DesiredStateValueValuesEnum.ACTIVE)))
response = client.admin_projects_locations_subscriptions.Patch(
update_request)
return response
def AddExportResources(resource_ref, args, request):
"""Sets export resource paths for an UpdateSubscriptionRequest.
Args:
resource_ref: resources.Resource, the resource reference for the resource
being operated on.
args: argparse.Namespace, the parsed commandline arguments.
request: An UpdateSubscriptionRequest.
Returns:
The UpdateSubscriptionRequest.
"""
# Unused resource reference
del resource_ref
if request.subscription.exportConfig is None:
return request
resource, _ = GetResourceInfo(request)
project = DeriveProjectFromResource(resource)
location = DeriveLocationFromResource(resource)
psl = PubsubLiteMessages()
SetExportConfigResources(args, psl, project, location,
request.subscription.exportConfig)
return request
def SetSeekTarget(resource_ref, args, request):
"""Sets the target for a SeekSubscriptionRequest."""
# Unused resource reference
del resource_ref
psl = PubsubLiteMessages()
request.seekSubscriptionRequest = GetSeekRequest(args, psl)
log.warning(
'The seek operation will complete once subscribers react to the seek. ' +
'If subscribers are offline, `pubsub lite-operations describe` can be ' +
'used to check the operation status later.')
return request
def UpdateListOperationsFilter(resource_ref, args, request):
"""Updates the filter for a ListOperationsRequest."""
# Unused resource reference
del resource_ref
# If the --filter arg is specified, let gcloud handle it client-side.
if args.filter:
return request
# Otherwise build the filter if the --subscription and/or --done flags are
# specified. The server will handle filtering.
if args.subscription:
# Note: This relies on project ids replaced with project numbers until
# b/194764731 is fixed.
request.filter = 'target={}/{}{}'.format(request.name,
SUBSCRIPTIONS_RESOURCE_PATH,
args.subscription)
if args.done:
if request.filter:
request.filter += ' AND '
else:
request.filter = ''
request.filter += 'done={}'.format(args.done)
return request
def RequirePython36(cmd='gcloud'):
"""Verifies that the python version is 3.6+.
Args:
cmd: The string command that requires python 3.6+.
Raises:
InvalidPythonVersion: if the python version is not 3.6+.
"""
if sys.version_info.major < 3 or (sys.version_info.major == 3 and
sys.version_info.minor < 6):
raise InvalidPythonVersion(
"""The `{}` command requires python 3.6 or greater. Please update the
python version to use this command.""".format(cmd))

View File

@@ -0,0 +1,225 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 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.
"""Shared resource flags for Cloud Pub/Sub commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.command_lib.util.concepts import presentation_specs
def SubscriptionAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='subscription',
help_text='Name of the subscription.')
def TopicAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='topic',
help_text='Name of the topic.')
def SchemaAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='schema', help_text='Name of the schema.')
def GetSubscriptionResourceSpec():
return concepts.ResourceSpec(
'pubsub.projects.subscriptions',
resource_name='subscription',
subscriptionsId=SubscriptionAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG)
def GetTopicResourceSpec(name='topic'):
return concepts.ResourceSpec(
'pubsub.projects.topics',
resource_name=name,
topicsId=TopicAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG)
def GetSchemaResourceSpec(name='schema'):
return concepts.ResourceSpec(
'pubsub.projects.schemas',
resource_name=name,
schemasId=SchemaAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG)
def CreateSubscriptionResourceArg(
verb, plural=False, required=True, positional=True
):
"""Create a resource argument for a Cloud Pub/Sub Subscription.
Args:
verb: str, the verb to describe the resource, such as 'to update'.
plural: bool, if True, use a resource argument that returns a list.
required: bool, if True, create subscription resource arg will be required.
positional: bool, if True, means that the subscription ID is a positional
rather than a flag.
Returns:
the PresentationSpec for the resource argument.
"""
if positional:
name = 'subscription'
else:
name = '--subscription'
if plural:
help_stem = 'One or more subscriptions'
else:
help_stem = 'Name of the subscription'
return presentation_specs.ResourcePresentationSpec(
name,
GetSubscriptionResourceSpec(),
'{} {}'.format(help_stem, verb),
required=required,
plural=plural,
prefixes=True,
)
def AddSubscriptionResourceArg(parser, verb, plural=False):
"""Add a resource argument for a Cloud Pub/Sub Subscription.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to update'.
plural: bool, if True, use a resource argument that returns a list.
"""
concept_parsers.ConceptParser(
[CreateSubscriptionResourceArg(verb, plural=plural)]
).AddToParser(parser)
def AddSchemaResourceArg(parser, verb, plural=False):
"""Add a resource argument for a Cloud Pub/Sub Schema.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to update'.
plural: bool, if True, use a resource argument that returns a list.
"""
concept_parsers.ConceptParser([CreateSchemaResourceArg(verb, plural=plural)
]).AddToParser(parser)
def CreateTopicResourceArg(verb,
positional=True,
plural=False,
required=True,
flag_name='topic'):
"""Create a resource argument for a Cloud Pub/Sub Topic.
Args:
verb: str, the verb to describe the resource, such as 'to update'.
positional: bool, if True, means that the topic ID is a positional rather
than a flag. If not positional, this also creates a '--topic-project' flag
as subscriptions and topics do not need to be in the same project.
plural: bool, if True, use a resource argument that returns a list.
required: bool, if True, create topic resource arg will be required.
flag_name: str, name of the topic resource arg (singular).
Returns:
the PresentationSpec for the resource argument.
"""
if positional:
name = flag_name
flag_name_overrides = {}
else:
name = '--' + flag_name if not plural else '--' + flag_name + 's'
flag_name_overrides = {'project': '--' + flag_name + '-project'}
help_stem = 'Name of the topic'
if plural:
help_stem = 'One or more topics'
return presentation_specs.ResourcePresentationSpec(
name,
GetTopicResourceSpec(flag_name),
'{} {}'.format(help_stem, verb),
required=required,
flag_name_overrides=flag_name_overrides,
plural=plural,
prefixes=True)
def AddTopicResourceArg(parser, verb, positional=True, plural=False):
"""Add a resource argument for a Cloud Pub/Sub Topic.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to update'.
positional: bool, if True, means that the topic ID is a positional rather
than a flag. If not positional, this also creates a '--topic-project' flag
as subscriptions and topics do not need to be in the same project.
plural: bool, if True, use a resource argument that returns a list.
"""
concept_parsers.ConceptParser(
[CreateTopicResourceArg(verb, positional=positional, plural=plural)]
).AddToParser(parser)
def CreateSchemaResourceArg(verb,
positional=True,
plural=False,
required=True,
flag_name='schema'):
"""Create a resource argument for a Cloud Pub/Sub Schema.
Args:
verb: str, the verb to describe the resource, such as 'to update'.
positional: bool, if True, means that the schema ID is a positional rather
than a flag. If not positional, this also creates a '--schema-project'
flag as schemas and topics do not need to be in the same project.
plural: bool, if True, use a resource argument that returns a list.
required: bool, if True, schema resource arg will be required.
flag_name: str, name of the schema resource arg (singular).
Returns:
the PresentationSpec for the resource argument.
"""
if positional:
name = flag_name
flag_name_overrides = {}
else:
name = '--' + flag_name if not plural else '--' + flag_name + 's'
flag_name_overrides = {'project': '--' + flag_name + '-project'}
help_stem = 'Name of the schema'
if plural:
help_stem = 'One or more schemas'
return presentation_specs.ResourcePresentationSpec(
name,
GetSchemaResourceSpec(flag_name),
'{} {}'.format(help_stem, verb),
required=required,
flag_name_overrides=flag_name_overrides,
plural=plural,
prefixes=True)
def AddResourceArgs(parser, resources):
"""Add resource arguments for commands that have topic and subscriptions.
Args:
parser: the parser for the command.
resources: a list of resource args to add.
"""
concept_parsers.ConceptParser(resources).AddToParser(parser)

View File

@@ -0,0 +1,97 @@
project:
name: project
collection: pubsub.projects
attributes:
- parameter_name: projectsId
attribute_name: project
help: |
The project name.
property: core/project
subscription:
name: subscription
collection: pubsub.projects.subscriptions
attributes:
- parameter_name: subscriptionsId
attribute_name: subscription
help: |
The subscription name.
topic:
name: topic
collection: pubsub.projects.topics
attributes:
- parameter_name: topicsId
attribute_name: topic
help: |
The topic name.
schema:
name: schema
collection: pubsub.projects.schemas
request_id_field: schemaId
attributes:
- parameter_name: schemasId
attribute_name: schema
help: |
The schema name.
location:
name: location
collection: pubsublite.admin.projects.locations
attributes:
- &location
parameter_name: locationsId
attribute_name: location
help: |
ID of the location of the Pub/Sub Lite resource.
lite_reservation:
name: reservation
collection: pubsublite.admin.projects.locations.reservations
attributes:
- *location
- parameter_name: reservationsId
attribute_name: reservation
help: |
The reservation name.
lite_topic:
name: topic
collection: pubsublite.admin.projects.locations.topics
attributes:
- *location
- parameter_name: topicsId
attribute_name: topic
help: |
The topic name.
lite_subscription:
name: subscription
collection: pubsublite.admin.projects.locations.subscriptions
attributes:
- *location
- parameter_name: subscriptionsId
attribute_name: subscription
help: |
The subscription name.
lite_operation:
name: operation
collection: pubsublite.admin.projects.locations.operations
attributes:
- *location
- parameter_name: operationsId
attribute_name: operation
help: |
The operation name.
lite_cursor_subscription:
name: subscription
collection: pubsublite.cursor.projects.locations.subscriptions
attributes:
- *location
- parameter_name: subscriptionsId
attribute_name: subscription
help: |
The subscription name.

View File

@@ -0,0 +1,426 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 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.
"""A library that is used to support Cloud Pub/Sub commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.pubsub import subscriptions
from googlecloudsdk.api_lib.pubsub import topics
from googlecloudsdk.api_lib.util import exceptions as exc
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 import resources
from googlecloudsdk.core.resource import resource_projector
from googlecloudsdk.core.util import times
import six
# Format for the seek time argument.
SEEK_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
# Collection for various subcommands.
TOPICS_COLLECTION = 'pubsub.projects.topics'
TOPICS_PUBLISH_COLLECTION = 'pubsub.topics.publish'
SNAPSHOTS_COLLECTION = 'pubsub.projects.snapshots'
SNAPSHOTS_LIST_COLLECTION = 'pubsub.snapshots.list'
SUBSCRIPTIONS_COLLECTION = 'pubsub.projects.subscriptions'
SUBSCRIPTIONS_ACK_COLLECTION = 'pubsub.subscriptions.ack'
SUBSCRIPTIONS_LIST_COLLECTION = 'pubsub.subscriptions.list'
SUBSCRIPTIONS_MOD_ACK_COLLECTION = 'pubsub.subscriptions.mod_ack'
SUBSCRIPTIONS_MOD_CONFIG_COLLECTION = 'pubsub.subscriptions.mod_config'
SUBSCRIPTIONS_PULL_COLLECTION = 'pubsub.subscriptions.pull'
SUBSCRIPTIONS_SEEK_COLLECTION = 'pubsub.subscriptions.seek'
SCHEMAS_COLLECTION = 'pubsub.projects.schemas'
PUSH_AUTH_SERVICE_ACCOUNT_MISSING_ENDPOINT_WARNING = """\
Using --push-auth-service-account requires specifying --push-endpoint. This
command will continue to run while ignoring --push-auth-service-account, but
will fail in a future version. To correct a subscription configuration, run:
$ gcloud pubsub subscriptions update SUBSCRIPTION \\
--push-endpoint=PUSH_ENDPOINT \\
--push-auth-service-account={SERVICE_ACCOUNT_EMAIL} [...]
"""
PUSH_AUTH_TOKEN_AUDIENCE_MISSING_REQUIRED_FLAGS_WARNING = """\
Using --push-auth-token-audience requires specifying both --push-endpoint and
--push-auth-service-account. This command will continue to run while ignoring
--push-auth-token-audience, but will fail in a future version. To correct a
subscription configuration, run:
$ gcloud pubsub subscriptions update SUBSCRIPTION \\
--push-endpoint={PUSH_ENDPOINT} \\
--push-auth-service-account={SERVICE_ACCOUNT_EMAIL} \\
--push-auth-token-audience={OPTIONAL_AUDIENCE_OVERRIDE} [...]
"""
class InvalidArgumentError(exceptions.Error):
"""The user provides invalid arguments."""
class RequestsFailedError(exceptions.Error):
"""Indicates that some requests to the API have failed."""
def __init__(self, requests, action):
super(RequestsFailedError, self).__init__(
'Failed to {action} the following: [{requests}].'.format(
action=action, requests=','.join(requests)))
def CreateFailureErrorMessage(
original_message, default_message='Internal Error'
):
return original_message if original_message else default_message
def ParseSnapshot(snapshot_name, project_id=''):
project_id = _GetProject(project_id)
return resources.REGISTRY.Parse(snapshot_name,
params={'projectsId': project_id},
collection=SNAPSHOTS_COLLECTION)
def ParseSubscription(subscription_name, project_id=''):
project_id = _GetProject(project_id)
return resources.REGISTRY.Parse(subscription_name,
params={'projectsId': project_id},
collection=SUBSCRIPTIONS_COLLECTION)
def ParseTopic(topic_name, project_id=''):
project_id = _GetProject(project_id)
return resources.REGISTRY.Parse(topic_name,
params={'projectsId': project_id},
collection=TOPICS_COLLECTION)
def ParseProject(project_id=None):
project_id = _GetProject(project_id)
return projects_util.ParseProject(project_id)
def _GetProject(project_id):
return project_id or properties.VALUES.core.project.Get(required=True)
def SnapshotUriFunc(snapshot):
if isinstance(snapshot, dict):
name = snapshot['name']
else:
name = snapshot
return ParseSnapshot(name).SelfLink()
def SubscriptionUriFunc(subscription):
project = None
if isinstance(subscription, dict):
name = subscription['subscriptionId']
project = subscription['projectId']
elif isinstance(subscription, str):
name = subscription
else:
name = subscription.name
return ParseSubscription(name, project).SelfLink()
def TopicUriFunc(topic):
if isinstance(topic, dict):
name = topic['topicId']
else:
name = topic.name
return ParseTopic(name).SelfLink()
def ParsePushConfig(args, client=None):
"""Parses configs of push subscription from args."""
push_endpoint = args.push_endpoint
service_account_email = getattr(args, 'SERVICE_ACCOUNT_EMAIL', None)
audience = getattr(args, 'OPTIONAL_AUDIENCE_OVERRIDE', None)
# TODO(b/284985002): Remove warnings when argument groups are created for
# authenticated push flags.
if audience is not None and (
push_endpoint is None or service_account_email is None
):
log.warning(
PUSH_AUTH_TOKEN_AUDIENCE_MISSING_REQUIRED_FLAGS_WARNING.format(
PUSH_ENDPOINT=push_endpoint or 'PUSH_ENDPOINT',
SERVICE_ACCOUNT_EMAIL=service_account_email
or 'SERVICE_ACCOUNT_EMAIL',
OPTIONAL_AUDIENCE_OVERRIDE=audience,
)
)
elif service_account_email is not None and push_endpoint is None:
log.warning(
PUSH_AUTH_SERVICE_ACCOUNT_MISSING_ENDPOINT_WARNING.format(
SERVICE_ACCOUNT_EMAIL=service_account_email
)
)
if push_endpoint is None:
if HasNoWrapper(args):
raise InvalidArgumentError(
'argument --push-no-wrapper: --push-endpoint must be specified.'
)
return None
client = client or subscriptions.SubscriptionsClient()
oidc_token = None
# Only set oidc_token when service_account_email is set.
if service_account_email is not None:
oidc_token = client.messages.OidcToken(
serviceAccountEmail=service_account_email, audience=audience)
no_wrapper = None
if HasNoWrapper(args):
write_metadata = getattr(args, 'push_no_wrapper_write_metadata', False)
no_wrapper = client.messages.NoWrapper(writeMetadata=write_metadata)
return client.messages.PushConfig(
pushEndpoint=push_endpoint, oidcToken=oidc_token, noWrapper=no_wrapper)
def HasNoWrapper(args):
return getattr(args, 'push_no_wrapper', False)
def FormatSeekTime(time):
return times.FormatDateTime(time, fmt=SEEK_TIME_FORMAT, tzinfo=times.UTC)
def FormatDuration(duration):
"""Formats a duration argument to be a string with units.
Args:
duration (int): The duration in seconds.
Returns:
unicode: The formatted duration.
"""
return six.text_type(duration) + 's'
def ParseAttributes(attribute_dict, messages=None):
"""Parses attribute_dict into a list of AdditionalProperty messages.
Args:
attribute_dict (Optional[dict]): Dict containing key=value pairs
to parse.
messages (Optional[module]): Module containing pubsub proto messages.
Returns:
list: List of AdditionalProperty messages.
"""
messages = messages or topics.GetMessagesModule()
attributes = []
if attribute_dict:
for key, value in sorted(six.iteritems(attribute_dict)):
attributes.append(
messages.PubsubMessage.AttributesValue.AdditionalProperty(
key=key,
value=value))
return attributes
# TODO(b/32276674): Remove the use of custom *DisplayDict's.
def TopicDisplayDict(topic):
"""Creates a serializable from a Cloud Pub/Sub Topic operation for display.
Args:
topic: (Cloud Pub/Sub Topic) Topic to be serialized.
Returns:
A serialized object representing a Cloud Pub/Sub Topic
operation (create, delete).
"""
topic_display_dict = resource_projector.MakeSerializable(topic)
topic_display_dict['topicId'] = topic.name
del topic_display_dict['name']
return topic_display_dict
def SubscriptionDisplayDict(subscription):
"""Creates a serializable from a Cloud Pub/Sub Subscription op for display.
Args:
subscription: (Cloud Pub/Sub Subscription) Subscription to be serialized.
Returns:
A serialized object representing a Cloud Pub/Sub Subscription
operation (create, delete, update).
"""
push_endpoint = ''
subscription_type = 'pull'
if subscription.pushConfig:
if subscription.pushConfig.pushEndpoint:
push_endpoint = subscription.pushConfig.pushEndpoint
subscription_type = 'push'
return {
'subscriptionId': subscription.name,
'topic': subscription.topic,
'type': subscription_type,
'pushEndpoint': push_endpoint,
'ackDeadlineSeconds': subscription.ackDeadlineSeconds,
'retainAckedMessages': bool(subscription.retainAckedMessages),
'messageRetentionDuration': subscription.messageRetentionDuration,
'enableExactlyOnceDelivery': subscription.enableExactlyOnceDelivery,
}
def SnapshotDisplayDict(snapshot):
"""Creates a serializable from a Cloud Pub/Sub Snapshot operation for display.
Args:
snapshot: (Cloud Pub/Sub Snapshot) Snapshot to be serialized.
Returns:
A serialized object representing a Cloud Pub/Sub Snapshot operation (create,
delete).
"""
return {
'snapshotId': snapshot.name,
'topic': snapshot.topic,
'expireTime': snapshot.expireTime,
}
def ListSubscriptionDisplayDict(subscription):
"""Returns a subscription dict with additional fields."""
result = resource_projector.MakeSerializable(subscription)
result['type'] = 'PUSH' if subscription.pushConfig.pushEndpoint else 'PULL'
subscription_ref = ParseSubscription(subscription.name)
result['projectId'] = subscription_ref.projectsId
result['subscriptionId'] = subscription_ref.subscriptionsId
topic_info = ParseTopic(subscription.topic)
result['topicId'] = topic_info.topicsId
return result
def ListTopicDisplayDict(topic):
topic_dict = resource_projector.MakeSerializable(topic)
topic_ref = ParseTopic(topic.name)
topic_dict['topic'] = topic.name
topic_dict['topicId'] = topic_ref.topicsId
del topic_dict['name']
return topic_dict
def ListTopicSubscriptionDisplayDict(topic_subscription):
"""Returns a topic_subscription dict with additional fields."""
result = resource_projector.MakeSerializable(
{'subscription': topic_subscription})
subscription_ref = ParseSubscription(topic_subscription)
result['projectId'] = subscription_ref.projectsId
result['subscriptionId'] = subscription_ref.subscriptionsId
return result
def ListSnapshotDisplayDict(snapshot):
"""Returns a snapshot dict with additional fields."""
result = resource_projector.MakeSerializable(snapshot)
snapshot_ref = ParseSnapshot(snapshot.name)
result['projectId'] = snapshot_ref.projectsId
result['snapshotId'] = snapshot_ref.snapshotsId
topic_ref = ParseTopic(snapshot.topic)
result['topicId'] = topic_ref.topicsId
result['expireTime'] = snapshot.expireTime
return result
def GetProject():
"""Returns the value of the core/project config property.
Config properties can be overridden with command line flags. If the --project
flag was provided, this will return the value provided with the flag.
"""
return properties.VALUES.core.project.Get(required=True)
def ParseSchemaName(schema):
"""Parses a schema name using configuration properties for fallback.
Args:
schema: str, the schema's ID, fully-qualified URL, or relative name
Returns:
str: the relative name of the schema resource
"""
return resources.REGISTRY.Parse(
schema, params={
'projectsId': GetProject
}, collection='pubsub.projects.schemas').RelativeName()
def OutputSchemaValidated(unused_response, unused_args):
"""Logs a message indicating that a schema is valid."""
log.status.Print('Schema is valid.')
def OutputMessageValidated(unused_response, unused_args):
"""Logs a message indicating that a message is valid."""
log.status.Print('Message is valid.')
def ParseExactlyOnceAckIdsAndFailureReasons(ack_ids_and_failure_reasons,
ack_ids):
failed_ack_ids = [ack['AckId'] for ack in ack_ids_and_failure_reasons]
successfully_processed_ack_ids = [
ack_id for ack_id in ack_ids if ack_id not in failed_ack_ids
]
return failed_ack_ids, successfully_processed_ack_ids
def HandleExactlyOnceDeliveryError(error):
e = exc.HttpException(error)
ack_ids_and_failure_reasons = ParseExactlyOnceErrorInfo(e.payload.details)
# If the failure doesn't have more information (specifically for exactly
# once related failures), re-raise the exception.
if not ack_ids_and_failure_reasons:
raise error
return ack_ids_and_failure_reasons
def ParseExactlyOnceErrorInfo(error_metadata):
"""Parses error metadata for exactly once ack/modAck failures.
Args:
error_metadata: error metadata as dict of format ack_id -> failure_reason.
Returns:
list: error metadata with only exactly once failures.
"""
ack_ids_and_failure_reasons = []
for error_md in error_metadata:
if 'reason' not in error_md or 'EXACTLY_ONCE' not in error_md['reason']:
continue
if 'metadata' not in error_md or not isinstance(error_md['metadata'], dict):
continue
for ack_id, failure_reason in error_md['metadata'].items():
if 'PERMANENT_FAILURE' in failure_reason or ('TEMPORARY_FAILURE'
in failure_reason):
result = resource_projector.MakeSerializable({})
result['AckId'] = ack_id
result['FailureReason'] = failure_reason
ack_ids_and_failure_reasons.append(result)
return ack_ids_and_failure_reasons