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,241 @@
# -*- 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.
"""Contains shared methods for container and volume printing."""
import collections
import json
from typing import Mapping, Sequence
from googlecloudsdk.api_lib.run import container_resource
from googlecloudsdk.api_lib.run import k8s_object
from googlecloudsdk.api_lib.run import revision
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.core.resource import custom_printer_base as cp
def _FormatSecretKeyRef(v):
return '{}:{}'.format(v.secretKeyRef.name, v.secretKeyRef.key)
def _FormatSecretVolumeSource(v):
if v.items:
return '{}:{}'.format(v.secretName, v.items[0].key)
else:
return v.secretName
def _FormatConfigMapKeyRef(v):
return '{}:{}'.format(v.configMapKeyRef.name, v.configMapKeyRef.key)
def _FormatConfigMapVolumeSource(v):
if v.items:
return '{}:{}'.format(v.name, v.items[0].key)
else:
return v.name
def GetContainer(
container_name: str,
container: container_resource.Container,
gpu_type: str,
labels: Mapping[str, str],
dependencies: Sequence[str],
annotations: Mapping[str, str],
is_primary: bool,
) -> cp.Table:
limits = GetLimits(container)
return cp.Labeled([
('Image', container.image),
('Base Image', GetBaseImage(annotations, container_name)),
('Command', ' '.join(container.command)),
('Args', ' '.join(container.args)),
(
'Port',
' '.join(str(p.containerPort) for p in container.ports),
),
('Memory', limits['memory']),
('CPU', limits['cpu']),
('GPU', limits['nvidia.com/gpu']),
('GPU Type', gpu_type if limits['nvidia.com/gpu'] else None),
(
'Env vars',
GetUserEnvironmentVariables(container),
),
('Volume Mounts', GetVolumeMounts(container)),
('Secrets', GetSecrets(container)),
('Config Maps', GetConfigMaps(container)),
(
'Startup Probe',
k8s_util.GetStartupProbe(container, labels, is_primary),
),
('Liveness Probe', k8s_util.GetLivenessProbe(container)),
('Readiness Probe', k8s_util.GetReadinessProbe(container)),
('Container Dependencies', ', '.join(dependencies)),
])
def GetContainers(record: container_resource.ContainerResource) -> cp.Table:
"""Returns a formatted table of a resource's containers."""
dependencies = collections.defaultdict(list, record.dependencies)
gpu_type = None
if k8s_object.GPU_TYPE_NODE_SELECTOR in record.node_selector:
gpu_type = record.node_selector[k8s_object.GPU_TYPE_NODE_SELECTOR]
def Containers():
for name, container in k8s_util.OrderByKey(record.containers):
key = 'Container {name}'.format(name=container.name)
value = GetContainer(
name,
container,
gpu_type,
record.labels,
dependencies[name],
record.annotations,
len(record.containers) == 1 or container.ports,
)
yield (key, value)
return cp.Mapped(Containers())
def GetBaseImage(annotations: Mapping[str, str], container_name: str) -> str:
if revision.BASE_IMAGES_ANNOTATION not in annotations:
return ''
container_name = container_name if container_name else ''
base_images = json.loads(annotations.get(revision.BASE_IMAGES_ANNOTATION))
return base_images.get(container_name, '')
def GetLimits(record):
return collections.defaultdict(str, record.resource_limits)
def GetUserEnvironmentVariables(record):
return cp.Mapped(k8s_util.OrderByKey(record.env_vars.literals))
def GetSecrets(container: container_resource.Container) -> cp.Table:
"""Returns a print mapping for env var and volume-mounted secrets."""
secrets = {}
secrets.update(
{k: _FormatSecretKeyRef(v) for k, v in container.env_vars.secrets.items()}
)
secrets.update({
k: _FormatSecretVolumeSource(v)
for k, v in container.MountedVolumeJoin('secrets').items()
})
return cp.Mapped(k8s_util.OrderByKey(secrets))
def GetVolumeMounts(container: container_resource.Container) -> cp.Table:
"""Returns a print mapping for volumes."""
volumes = {
k: _FormatVolumeMount(*v)
for k, v in container.NamedMountedVolumeJoin().items()
}
volumes = {k: v for k, v in volumes.items() if v}
return cp.Mapped(k8s_util.OrderByKey(volumes))
def _FormatVolumeMount(name, volume):
"""Format details about a volume mount."""
if not volume:
return 'volume not found'
if volume.emptyDir:
return cp.Labeled([
('name', name),
('type', 'in-memory'),
])
elif volume.nfs:
return cp.Labeled([
('name', name),
('type', 'nfs'),
('location', '{}:{}'.format(volume.nfs.server, volume.nfs.path)),
])
elif volume.csi:
if volume.csi.driver == 'gcsfuse.run.googleapis.com':
bucket = None
for prop in volume.csi.volumeAttributes.additionalProperties:
if prop.key == 'bucketName':
bucket = prop.value
return cp.Labeled(
[('name', name), ('type', 'cloud-storage'), ('bucket', bucket)]
)
def GetVolumes(record):
"""Returns a print mapping for volumes."""
volumes = {v.name: _FormatVolume(v) for v in record.spec.volumes}
volumes = {k: v for k, v in volumes.items() if v}
return cp.Mapped(k8s_util.OrderByKey(volumes))
def _FormatVolume(volume):
"""Format a volume for the volumes list."""
if volume.emptyDir:
return cp.Labeled([
('type', 'in-memory'),
('size-limit', volume.emptyDir.sizeLimit),
])
elif volume.nfs:
return cp.Labeled([
('type', 'nfs'),
('location', '{}:{}'.format(volume.nfs.server, volume.nfs.path)),
('read-only', volume.nfs.readOnly),
])
elif volume.csi:
if volume.csi.driver == 'gcsfuse.run.googleapis.com':
bucket = None
mount_options = None
for prop in volume.csi.volumeAttributes.additionalProperties:
if prop.key == 'bucketName':
bucket = prop.value
if prop.key == 'mountOptions':
mount_options_list = prop.value.split(',')
mount_options = cp.Table(
[tuple(opt.split('=', 1)) for opt in mount_options_list]
)
return cp.Labeled([
('type', 'cloud-storage'),
('bucket', bucket),
('read-only', volume.csi.readOnly),
('mount-options', mount_options),
])
elif volume.csi.driver == 'cloudsql.run.googleapis.com':
instances = None
for prop in volume.csi.volumeAttributes.additionalProperties:
if prop.key == 'instances':
instances = prop.value
return cp.Labeled([
('type', 'cloudsql'),
('instances', instances),
])
def GetConfigMaps(container: container_resource.Container) -> cp.Table:
"""Returns a print mapping for env var and volume-mounted config maps."""
config_maps = {}
config_maps.update({
k: _FormatConfigMapKeyRef(v)
for k, v in container.env_vars.config_maps.items()
})
config_maps.update({
k: _FormatConfigMapVolumeSource(v)
for k, v in container.MountedVolumeJoin('config_maps').items()
})
return cp.Mapped(k8s_util.OrderByKey(config_maps))

View File

@@ -0,0 +1,70 @@
# -*- 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.
"""Printer for exporting k8s resources."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import copy
from googlecloudsdk.api_lib.run import k8s_object
from googlecloudsdk.core.resource import yaml_printer
_OMITTED_ANNOTATIONS = [
k8s_object.SERVING_GROUP + '/creator',
k8s_object.SERVING_GROUP + '/lastModifier',
k8s_object.RUN_GROUP + '/client-name',
k8s_object.RUN_GROUP + '/client-version',
k8s_object.RUN_GROUP + '/creatorEmail',
k8s_object.RUN_GROUP + '/lastModifierEmail',
k8s_object.RUN_GROUP + '/operation-id'
]
EXPORT_PRINTER_FORMAT = 'export'
# To be replaced with the code that does gcloud export once that feature of
# Cloud SDK is ready.
# go/gcloud-export-import
class ExportPrinter(yaml_printer.YamlPrinter):
"""Printer for k8s_objects to export.
Omits status information, and metadata that isn't consistent across
deployments, like project or region.
"""
def _AddRecord(self, record, delimit=True):
record = self._FilterForExport(record)
super(ExportPrinter, self)._AddRecord(record, delimit)
def _FilterForExport(self, record):
m = copy.deepcopy(record)
meta = m.get('metadata')
if meta:
meta.pop('creationTimestamp', None)
meta.pop('generation', None)
meta.pop('resourceVersion', None)
meta.pop('selfLink', None)
meta.pop('uid', None)
for k in _OMITTED_ANNOTATIONS:
meta.get('annotations', {}).pop(k, None)
m.pop('status', None)
return m

View File

@@ -0,0 +1,58 @@
# third_party/py/googlecloudsdk/command_lib/run/printers/instance_printer.py
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Instance-specific printer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.command_lib.run.printers import container_and_volume_printer_util as container_util
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.core.resource import custom_printer_base as cp
INSTANCE_PRINTER_FORMAT = 'instance'
# TODO: b/456195460 - Add more fields to the instance printer.
class InstancePrinter(cp.CustomPrinterBase):
"""Prints the run Instance in a custom human-readable format.
Format specific to Cloud Run instances. Only available on Cloud Run commands
that print instances.
"""
@staticmethod
def _formatOutput(record):
output = []
header = k8s_util.BuildHeader(record)
labels = k8s_util.GetLabels(record.labels)
ready_message = k8s_util.FormatReadyMessage(record)
if header:
output.append(header)
output.append(' ')
if labels:
output.append(labels)
output.append(' ')
if ready_message:
output.append(ready_message)
output.append(container_util.GetContainers(record))
return output
def Transform(self, record):
"""Transform a instance into the output structure of marker classes."""
return cp.Lines(InstancePrinter._formatOutput(record))

View File

@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Instance-split-specific printer and functions for generating instance split formats."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.api_lib.run import instance_split_pair
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.resource import custom_printer_base as cp
INSTANCE_SPLIT_PRINTER_FORMAT = 'instancesplit'
_LATEST_READY_REV_UNSPECIFIED = '-'
def _TransformInstanceSplitPair(pair):
"""Transforms a single InstanceSplitPair into a marker class structure."""
console = console_attr.GetConsoleAttr()
return (
pair.displayPercent,
console.Emphasize(pair.displayRevisionId),
)
def _TransformInstanceSplitPairs(instance_split_pairs):
"""Transforms a List[InstanceSplitPair] into a marker class structure."""
instance_split_section = cp.Section(
[cp.Table(_TransformInstanceSplitPair(p) for p in instance_split_pairs)]
)
return cp.Section(
[cp.Labeled([('Instance Split', instance_split_section)])],
max_column_width=60,
)
def TransformInstanceSplitFields(worker_pool_record):
"""Transforms a worker's instance split fields into a marker class structure to print.
Generates the custom printing format for a worker's instance split using the
marker classes defined in custom_printer_base.
Args:
worker_pool_record: A WorkerPool object.
Returns:
A custom printer marker object describing the instance split fields
print format.
"""
no_status = worker_pool_record.status is None
instance_split_pairs = instance_split_pair.GetInstanceSplitPairs(
worker_pool_record.spec_split,
worker_pool_record.status_split,
(
_LATEST_READY_REV_UNSPECIFIED
if no_status
else worker_pool_record.status.latestReadyRevisionName
),
)
return _TransformInstanceSplitPairs(instance_split_pairs)
class InstanceSplitPrinter(cp.CustomPrinterBase):
"""Prints a worker pool's instance split in a custom human-readable format."""
def Print(self, resources, single=False, intermediate=False):
"""Overrides ResourcePrinter.Print to set single=True."""
# The update-instance-split command returns a List[InstanceSplitPair] as its
# result. In order to print the custom human-readable format, this printer
# needs to process all records in the result at once (to compute column
# widths). By default, ResourcePrinter interprets a List[*] as a list of
# separate records and passes the contents of the list to this printer
# one-by-one. Setting single=True forces ResourcePrinter to treat the
# result as one record and pass the entire list to this printer in one call.
super(InstanceSplitPrinter, self).Print(resources, True, intermediate)
def Transform(self, record):
"""Transforms a List[InstanceSplitPair] into a marker class format."""
return _TransformInstanceSplitPairs(record)

View File

@@ -0,0 +1,334 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 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.
"""Job-specific printer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import datetime
from googlecloudsdk.command_lib.run.printers import container_and_volume_printer_util as container_util
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.command_lib.util import time_util
from googlecloudsdk.core.resource import custom_printer_base as cp
EXECUTION_PRINTER_FORMAT = 'execution'
JOB_PRINTER_FORMAT = 'job'
TASK_PRINTER_FORMAT = 'task'
def _PluralizedWord(word, count):
return '{count} {word}{plural}'.format(
count=count or 0, word=word, plural='' if count == 1 else 's'
)
def FormatDurationShort(duration_seconds: int) -> str:
"""Format duration from seconds into shorthand string.
Duration will be represented of the form `#d#h#m$s` for days, hours, minutes
and seconds. Any field that's 0 will be excluded. So 3660 seconds (1 hour and
1 minute) will be represented as "1h1m" with no days or seconds listed.
Args:
duration_seconds: the total time in seconds to format
Returns:
a string representing the duration in more human-friendly units.
"""
if duration_seconds == 0:
return '0s'
duration = datetime.timedelta(seconds=duration_seconds)
remaining = duration.seconds
hours = remaining // 3600
remaining = remaining % 3600
minutes = remaining // 60
seconds = remaining % 60
res = ''
if duration.days:
res += '{}d'.format(duration.days)
if hours:
res += '{}h'.format(hours)
if minutes:
res += '{}m'.format(minutes)
if seconds:
res += '{}s'.format(seconds)
return res
class JobPrinter(cp.CustomPrinterBase):
"""Prints the run Job in a custom human-readable format.
Format specific to Cloud Run jobs. Only available on Cloud Run commands
that print jobs.
"""
@staticmethod
def TransformSpec(record):
return ExecutionPrinter.TransformSpec(record.execution_template, record)
@staticmethod
def TransformStatus(record):
if record.status is None:
return ''
lines = [
'Executed {}'.format(
_PluralizedWord('time', record.status.executionCount)
)
]
if record.status.latestCreatedExecution is not None:
lines.append(
'Last executed {} with execution {}'.format(
record.status.latestCreatedExecution.creationTimestamp,
record.status.latestCreatedExecution.name,
)
)
lines.append(k8s_util.LastUpdatedMessageForJob(record))
return cp.Lines(lines)
@staticmethod
def _formatOutput(record):
output = []
header = k8s_util.BuildHeader(record)
status = JobPrinter.TransformStatus(record)
labels = k8s_util.GetLabels(record.labels)
spec = JobPrinter.TransformSpec(record)
ready_message = k8s_util.FormatReadyMessage(record)
if header:
output.append(header)
if status:
output.append(status)
output.append(' ')
if labels:
output.append(labels)
output.append(' ')
if spec:
output.append(spec)
if ready_message:
output.append(ready_message)
return output
def Transform(self, record):
"""Transform a job into the output structure of marker classes."""
fmt = cp.Lines(JobPrinter._formatOutput(record))
return fmt
class TaskPrinter(cp.CustomPrinterBase):
"""Prints the run execution Task in a custom human-readable format.
Format specific to Cloud Run jobs. Only available on Cloud Run commands
that print tasks.
"""
@staticmethod
def TransformSpec(record):
labels = [
(
'Task Timeout',
FormatDurationShort(record.spec.timeoutSeconds)
if record.spec.timeoutSeconds
else None,
),
(
'Max Retries',
'{}'.format(record.spec.maxRetries)
if record.spec.maxRetries is not None
else None,
),
('Service account', record.service_account),
('VPC access', k8s_util.GetVpcNetwork(record.annotations)),
('SQL connections', k8s_util.GetCloudSqlInstances(record.annotations)),
(
'Volumes',
container_util.GetVolumes(record),
),
]
return cp.Lines([container_util.GetContainers(record), cp.Labeled(labels)])
@staticmethod
def TransformStatus(record):
status = [
('Running state', record.running_state),
]
if record.last_exit_code is not None:
status.extend([
(
'Last Attempt Result',
cp.Labeled([
('Exit Code', record.last_exit_code),
('Message', record.last_exit_message),
]),
),
])
return cp.Labeled(status)
def Transform(self, record):
"""Transform a job into the output structure of marker classes."""
return cp.Lines([
k8s_util.BuildHeader(record),
self.TransformStatus(record),
' ',
self.TransformSpec(record),
k8s_util.FormatReadyMessage(record),
])
class ExecutionPrinter(cp.CustomPrinterBase):
"""Prints the run Execution in a custom human-readable format.
Format specific to Cloud Run jobs. Only available on Cloud Run commands
that print executions.
"""
@staticmethod
def TransformSpec(record, record_for_annotations):
"""Transforms the execution spec into a custom human-readable format.
Args:
record: The execution or execution template to transform.
record_for_annotations: The resource whose annotations should be used to
extract Cloud Run feature settings. It should be an execution or a job.
Returns:
A custom printer Marker class for a list of lines.
"""
breakglass_value = k8s_util.GetBinAuthzBreakglass(record_for_annotations)
return cp.Lines([
cp.Labeled([
('Tasks', record.spec.taskCount),
(
'Parallelism',
record.parallelism if record.parallelism else 'No limit',
),
]),
TaskPrinter.TransformSpec(record.template),
cp.Labeled([
(
'Binary Authorization',
k8s_util.GetBinAuthzPolicy(record_for_annotations),
),
# pylint: disable=g-explicit-bool-comparison
# Empty breakglass string is valid, space is used to force it
# showing
(
'Breakglass Justification',
' ' if breakglass_value == '' else breakglass_value,
),
(
'Threat Detection',
k8s_util.GetThreatDetectionEnabled(record_for_annotations),
),
]),
])
@staticmethod
def TransformStatus(record):
if record.status is None:
return ''
lines = []
if record.ready_condition['status'] is None:
lines.append(
'{} currently running'.format(
_PluralizedWord('task', record.status.runningCount)
)
)
lines.append(
'{} completed successfully'.format(
_PluralizedWord('task', record.status.succeededCount)
)
)
if record.status.failedCount is not None and record.status.failedCount > 0:
lines.append(
'{} failed to complete'.format(
_PluralizedWord('task', record.status.failedCount)
)
)
if (
record.status.cancelledCount is not None
and record.status.cancelledCount > 0
):
lines.append(
'{} cancelled'.format(
_PluralizedWord('task', record.status.cancelledCount)
)
)
if (
record.status.completionTime is not None
and record.creation_timestamp is not None
):
lines.append(
'Elapsed time: '
+ ExecutionPrinter._elapsedTime(
record.creation_timestamp, record.status.completionTime
)
)
if record.status.logUri is not None:
# adding a blank line before Log URI
lines.append(' ')
lines.append('Log URI: {}'.format(record.status.logUri))
return cp.Lines(lines)
@staticmethod
def _elapsedTime(start, end):
duration = datetime.timedelta(
seconds=time_util.Strptime(end) - time_util.Strptime(start)
).seconds
hours = duration // 3600
duration = duration % 3600
minutes = duration // 60
seconds = duration % 60
if hours > 0:
# Only hours and minutes for short message
return '{} and {}'.format(
_PluralizedWord('hour', hours), _PluralizedWord('minute', minutes)
)
if minutes > 0:
return '{} and {}'.format(
_PluralizedWord('minute', minutes), _PluralizedWord('second', seconds)
)
return _PluralizedWord('second', seconds)
@staticmethod
def _formatOutput(record):
output = []
header = k8s_util.BuildHeader(record)
status = ExecutionPrinter.TransformStatus(record)
labels = k8s_util.GetLabels(record.labels)
spec = ExecutionPrinter.TransformSpec(record, record)
ready_message = k8s_util.FormatReadyMessage(record)
if header:
output.append(header)
if status:
output.append(status)
output.append(' ')
if labels:
output.append(labels)
output.append(' ')
if spec:
output.append(spec)
if ready_message:
output.append(ready_message)
return output
def Transform(self, record):
"""Transform a job into the output structure of marker classes."""
fmt = cp.Lines(ExecutionPrinter._formatOutput(record))
return fmt

View File

@@ -0,0 +1,241 @@
# -*- coding: utf-8 -*- #
# Copyright 2019 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.
"""Contains shared methods for printing k8s object in a human-readable way."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import json
import textwrap
from typing import Mapping, Union
from googlecloudsdk.api_lib.run import container_resource
from googlecloudsdk.api_lib.run import k8s_object
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.resource import custom_printer_base as cp
def OrderByKey(map_):
for k in sorted(k if k is not None else '' for k in map_):
yield k, (map_.get(k) if map_.get(k) is not None else '')
def FormatReadyMessage(record):
"""Returns the record's status condition Ready (or equivalent) message."""
if record.ready_condition and record.ready_condition['message']:
symbol, color = record.ReadySymbolAndColor()
return console_attr.GetConsoleAttr().Colorize(
textwrap.fill(
'{} {}'.format(symbol, record.ready_condition['message']), 100
),
color,
)
elif record.status is None:
return console_attr.GetConsoleAttr().Colorize(
'Error getting status information', 'red'
)
else:
return ''
def LastUpdatedMessage(record):
if record.status is None:
return 'Unknown update information'
modifier = record.last_modifier or '?'
last_transition_time = '?'
for condition in record.status.conditions:
if condition.type == 'Ready' and condition.lastTransitionTime:
last_transition_time = condition.lastTransitionTime
return 'Last updated on {} by {}'.format(last_transition_time, modifier)
def LastUpdatedMessageForJob(record):
modifier = record.last_modifier or '?'
last_updated_time = record.last_modified_timestamp or '?'
return 'Last updated on {} by {}'.format(last_updated_time, modifier)
def GetLabels(labels):
"""Returns a human readable description of user provided labels if any."""
if not labels:
return ''
return ' '.join(
sorted(
[
'{}:{}'.format(k, v)
for k, v in labels.items()
if not k.startswith(k8s_object.INTERNAL_GROUPS)
]
)
)
def BuildHeader(record, is_multi_region=False):
con = console_attr.GetConsoleAttr()
status = con.Colorize(*record.ReadySymbolAndColor())
try:
place = ('regions ' if is_multi_region else 'region ') + record.region
except KeyError:
place = 'namespace ' + record.namespace
kind = ('Multi-Region ' if is_multi_region else '') + record.Kind()
return con.Emphasize(
'{} {} {} in {}'.format(status, kind, record.name, place)
)
def GetCloudSqlInstances(record):
"""Returns the value of the cloudsql-instances.
Args:
record: A dictionary-like object containing the CLOUDSQL_ANNOTATION.
"""
instances = record.get(container_resource.CLOUDSQL_ANNOTATION, '')
return instances.replace(',', ', ')
def GetVpcNetwork(record):
"""Returns the VPC Network setting.
Either the values of the vpc-access-connector and vpc-access-egress, or the
values of the network and subnetwork in network-interfaces annotation and
vpc-access-egress.
Args:
record: A dictionary-like object containing the VPC_ACCESS_ANNOTATION and
EGRESS_SETTINGS_ANNOTATION keys.
"""
connector = record.get(container_resource.VPC_ACCESS_ANNOTATION, '')
if connector:
return cp.Labeled([
('Connector', connector),
(
'Egress',
record.get(container_resource.EGRESS_SETTINGS_ANNOTATION, ''),
),
])
# Direct VPC case if annoation exists.
original_value = record.get(k8s_object.NETWORK_INTERFACES_ANNOTATION, '')
if not original_value:
return ''
try:
network_interface = json.loads(original_value)[0]
return cp.Labeled([
('Network', network_interface.get('network', '')),
('Subnet', network_interface.get('subnetwork', '')),
(
'Egress',
record.get(container_resource.EGRESS_SETTINGS_ANNOTATION, ''),
),
])
except Exception: # pylint: disable=broad-except
return ''
def GetBinAuthzPolicy(record):
return record.annotations.get(k8s_object.BINAUTHZ_POLICY_ANNOTATION, '')
def GetBinAuthzBreakglass(record):
return record.annotations.get(k8s_object.BINAUTHZ_BREAKGLASS_ANNOTATION)
def GetDescription(record):
return record.annotations.get(k8s_object.DESCRIPTION_ANNOTATION)
def GetExecutionEnvironment(record):
return record.annotations.get(k8s_object.EXECUTION_ENVIRONMENT_ANNOTATION, '')
def GetThreatDetectionEnabled(record):
if record.annotations.get(
k8s_object.THREAT_DETECTION_ANNOTATION, '').lower() == 'true':
return 'Enabled'
return ''
def GetStartupProbe(
container: container_resource.Container,
labels: Mapping[str, str],
is_primary: bool,
) -> Union[str, cp.Lines]:
probe_type = ''
if is_primary:
probe_type = labels.get('run.googleapis.com/startupProbeType', '')
return _GetProbe(
container.startupProbe,
probe_type,
)
def GetLivenessProbe(
container: container_resource.Container,
) -> Union[str, cp.Lines]:
return _GetProbe(container.livenessProbe)
def GetReadinessProbe(
container: container_resource.Container,
) -> Union[str, cp.Lines]:
return _GetProbe(container.readinessProbe)
def _GetProbe(probe, probe_type=''):
"""Returns the information message for the given probe."""
if not probe:
return ''
probe_action = 'TCP'
port = ''
path = ''
if probe.httpGet:
probe_action = 'HTTP'
path = probe.httpGet.path
if probe.tcpSocket:
probe_action = 'TCP'
port = probe.tcpSocket.port
if probe.grpc:
probe_action = 'GRPC'
port = probe.grpc.port
return cp.Lines([
'{probe_action} every {period}s'.format(
probe_action=probe_action, period=probe.periodSeconds
),
cp.Labeled([
('Path', path),
('Port', port),
(
'Initial delay',
'{initial_delay}s'.format(
initial_delay=probe.initialDelaySeconds or '0'
),
),
(
'Timeout',
'{timeout}s'.format(timeout=probe.timeoutSeconds),
),
(
'Success threshold',
'{successes}'.format(successes=probe.successThreshold or ''),
),
(
'Failure threshold',
'{failures}'.format(failures=probe.failureThreshold),
),
('Type', probe_type),
]),
])

View File

@@ -0,0 +1,169 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Custom printer for Cloud Run presets."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import textwrap
from googlecloudsdk.core.resource import custom_printer_base as cp
PRESETS_PRINTER_FORMAT = 'presets'
_NAME_COL_WIDTH = 21
_DESC_COL_WIDTH = 38
_REQ_COL_WIDTH = 7
_INFO_INDENT_WIDTH = 16
_MAX_WIDTH = 80
PRESETS_ENUM_MAP = {
'KIND_UNSPECIFIED': 'Unspecified',
'CATEGORY_UNSPECIFIED': 'Unspecified',
'CATEGORY_QUICKSTART': 'Quickstart',
'CATEGORY_ADDON': 'Add-on',
'CATEGORY_OPTIMIZATION': 'Optimization',
}
def _format_enum(enum_string):
"""Formats a generic enum string into a title."""
if not enum_string:
return ''
if enum_string in PRESETS_ENUM_MAP:
return PRESETS_ENUM_MAP[enum_string]
return enum_string.replace('_', ' ').title()
def _format_kind_list(kind_list):
"""Formats a list of kind enum strings for display."""
if not kind_list:
return 'None'
return ', '.join(_format_enum(kind) for kind in kind_list)
class PresetsPrinter(cp.CustomPrinterBase):
"""Prints a Cloud Run preset in a custom human-readable format."""
def Transform(self, preset):
"""Transforms a preset into a structured output."""
return cp.Lines([
cp.Lines([' ']),
self._get_preset_info(preset),
cp.Lines([' ']),
self._get_preset_inputs(preset),
cp.Lines([' ']),
self._get_key_preset_config(preset),
cp.Lines([' ']),
self._get_usage(preset),
cp.Lines([' ']),
])
def _get_preset_info(self, preset):
"""Formats the preset info section."""
fields = [
('Name:', str(preset.get('name', ''))),
('Category:', _format_enum(preset.get('category', ''))),
(
'Applies to:',
_format_kind_list(preset.get('supported_resources', [])),
),
('Description:', str(preset.get('description', ''))),
('Preset Version:', str(preset.get('version', ''))),
]
lines = []
for label, value in fields:
if not value:
continue
wrapped_lines = textwrap.wrap(
value, width=_MAX_WIDTH - _INFO_INDENT_WIDTH
)
first_line = wrapped_lines[0] if wrapped_lines else ''
lines.append(label.ljust(_INFO_INDENT_WIDTH) + first_line)
for line in wrapped_lines[1:]:
lines.append(' ' * _INFO_INDENT_WIDTH + line)
return cp.Section([cp.Lines(lines)])
def _format_row(self, name, desc, required):
"""Helper to format a single row with specific padding."""
return (
' '
+ name.ljust(_NAME_COL_WIDTH)
+ ' '
+ desc.ljust(_DESC_COL_WIDTH)
+ ' '
+ required.ljust(_REQ_COL_WIDTH)
)
def _get_preset_inputs_header(self):
"""Returns the header for the preset inputs section."""
return [
'Inputs:',
' ',
self._format_row('Name', 'Description', 'Required'),
self._format_row(
'-' * _NAME_COL_WIDTH, '-' * _DESC_COL_WIDTH, '-' * _REQ_COL_WIDTH
),
]
def _get_preset_inputs(self, preset):
"""Formats the preset inputs section."""
parameters = preset.get('parameters', [])
if not parameters:
return None
inputs = self._get_preset_inputs_header()
for param in parameters:
name = param.get('name', '')
desc = param.get('description', '')
required = 'Yes' if param.get('required', False) else 'No'
wrapped_desc = textwrap.wrap(desc, width=_DESC_COL_WIDTH)
first_desc_line = wrapped_desc[0] if wrapped_desc else ''
inputs.append(self._format_row(name, first_desc_line, required))
for line in wrapped_desc[1:]:
inputs.append(self._format_row('', line, ''))
return cp.Section([cp.Lines(inputs)])
def _get_key_preset_config(self, preset):
"""Formats the key preset configuration section."""
config_values = preset.get('config_values', {})
if not config_values:
return None
labeled_data = cp.Labeled(config_values.items())
return cp.Section([
cp.Labeled(
[('Key Preset Configuration', cp.Lines([' ', labeled_data, ' ']))]
)
])
def _get_usage(self, preset):
"""Formats the preset gcloud usage section."""
usage_lines = preset.get('example_gcloud_usage', [])
if not usage_lines:
return None
if isinstance(usage_lines, str):
usage_lines = [usage_lines]
full_usage_string = [' ']
full_usage_string.extend(usage_lines)
full_usage_string.append(' ')
return cp.Section(
[cp.Labeled([('Usage', cp.Lines(full_usage_string))])]
)

View File

@@ -0,0 +1,119 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Profiles-specific printer and functions for generating CSV formats."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import csv
import sys
from googlecloudsdk.core.resource import custom_printer_base as cp
PROFILES_PRINTER_FORMAT = "csvprofile"
def amount_to_decimal(cost):
"""Converts cost to a decimal representation."""
units = cost.units
if not units:
units = 0
decimal_value = +(units + cost.nanos / 1e9)
return f"{decimal_value:.3f}"
def get_decimal_cost(costs):
"""Returns the cost per million normalized output tokens as a decimal.
Args:
costs: The costs to convert.
"""
output_token_cost = "N/A"
if costs and costs[0].costPerMillionOutputTokens:
output_token_cost = amount_to_decimal(
costs[0].costPerMillionOutputTokens
)
input_token_cost = "N/A"
if costs and costs[0].costPerMillionInputTokens:
input_token_cost = amount_to_decimal(costs[0].costPerMillionInputTokens)
return (input_token_cost, output_token_cost)
def _transform_profiles(profiles):
"""Transforms profiles to a CSV format, including cost conversions."""
csv_data = []
header = [
"Instance Type",
"Accelerator Type",
"Model Name",
"Model Server Name",
"Model Server Version",
"Output Tokens/s",
"NTPOT (ms)",
"TTFT (ms)",
"ITL (ms)",
"QPS",
"Cost/M Input Tokens",
"Cost/M Output Tokens",
"Use Case",
"Average Input Length",
"Average Output Length",
"Serving Stack",
"Serving Stack Version",
]
csv_data.append(header)
for profile in profiles:
if profile.performanceStats:
for stats in profile.performanceStats:
input_token_cost, output_token_cost = get_decimal_cost(
stats.cost
)
row = [
profile.instanceType,
profile.acceleratorType,
profile.modelServerInfo.model,
profile.modelServerInfo.modelServer,
profile.modelServerInfo.modelServerVersion,
stats.outputTokensPerSecond,
stats.ntpotMilliseconds,
stats.ttftMilliseconds,
getattr(stats, "itlMilliseconds", ""),
stats.queriesPerSecond,
input_token_cost,
output_token_cost,
profile.workloadSpec.useCase,
profile.workloadSpec.averageInputLength,
profile.workloadSpec.averageOutputLength,
profile.servingStack.name if profile.servingStack else None,
profile.servingStack.version if profile.servingStack else None,
]
csv_data.append(row)
return csv_data
class ProfileCSVPrinter(cp.CustomPrinterBase):
"""Prints a service's profile in a custom human-readable format."""
def Transform(self, profiles):
"""Transforms a List[TrafficTargetPair] into a CSV format."""
return _transform_profiles(profiles)
def Print(self, resources, single=True, intermediate=False):
"""Overrides ResourcePrinter.Print to set single=True."""
writer = csv.writer(sys.stdout, lineterminator="\n")
writer.writerows(self.Transform(resources))

View File

@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Traffic-specific printer and functions for generating traffic formats."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.core.resource import custom_printer_base as cp
PROFILES_PRINTER_FORMAT = 'profile'
def amount_to_decimal(cost):
"""Converts cost to a decimal representation."""
units = cost.units
if not units:
units = 0
decimal_value = +(units + cost.nanos / 1e9)
return f'{decimal_value:.3f}'
def get_decimal_cost(costs):
"""Returns the cost per million normalized output tokens as a decimal.
Args:
costs: The costs to convert.
"""
output_token_cost = 'N/A'
if costs and costs[0].costPerMillionNormalizedOutputTokens:
output_token_cost = amount_to_decimal(
costs[0].costPerMillionNormalizedOutputTokens
)
input_token_cost = 'N/A'
if costs and costs[0].costPerMillionInputTokens:
input_token_cost = amount_to_decimal(costs[0].costPerMillionInputTokens)
return (input_token_cost, output_token_cost)
def _transform_profiles(profiles):
"""Transforms a List[AcceleratorOption] into a table with decimal representation of cost."""
header = [
'Accelerator',
'Cost/M Input Tokens',
'Cost/M Output Tokens',
'Output Tokens/s',
'NTPOT(ms)',
'Accelerator Count',
'Model Server',
'Model Server Version',
'Model',
]
rows = [header]
for p in profiles:
input_token_cost, output_token_cost = get_decimal_cost(
p.performanceStats.cost if p.performanceStats else None
)
row = [
p.acceleratorType,
input_token_cost,
output_token_cost,
p.performanceStats.outputTokensPerSecond
if p.performanceStats
else None,
p.performanceStats.ntpotMilliseconds if p.performanceStats else None,
p.resourcesUsed.acceleratorCount if p.resourcesUsed else None,
p.modelAndModelServerInfo.modelServerName,
p.modelAndModelServerInfo.modelServerVersion,
p.modelAndModelServerInfo.modelName,
]
rows.append(row)
profiles_table = cp.Table(rows)
return cp.Section([profiles_table], max_column_width=60)
class ProfilePrinter(cp.CustomPrinterBase):
"""Prints a service's profile in a custom human-readable format."""
def Print(self, resources, single=True, intermediate=False):
"""Overrides ResourcePrinter.Print to set single=True."""
super(ProfilePrinter, self).Print(resources, True, intermediate)
def Transform(self, response):
"""Transforms a List[TrafficTargetPair] into a marker class format."""
profiles = [_transform_profiles(response.acceleratorOptions)]
profiles.append(response.comments)
return cp.Section(profiles, max_column_width=60)

View File

@@ -0,0 +1,117 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Traffic-specific printer and functions for generating traffic formats."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.core.resource import custom_printer_base as cp
PROFILES_PRINTER_FORMAT = 'profile'
def amount_to_decimal(cost):
"""Converts cost to a decimal representation."""
units = cost.units
if not units:
units = 0
decimal_value = +(units + cost.nanos / 1e9)
return f'{decimal_value:.3f}'
def get_decimal_cost(costs):
"""Returns the cost per million normalized output tokens as a decimal.
Args:
costs: The costs to convert.
"""
output_token_cost = 'N/A'
if costs and costs[0].costPerMillionOutputTokens:
output_token_cost = amount_to_decimal(
costs[0].costPerMillionOutputTokens
)
input_token_cost = 'N/A'
if costs and costs[0].costPerMillionInputTokens:
input_token_cost = amount_to_decimal(costs[0].costPerMillionInputTokens)
return (input_token_cost, output_token_cost)
def _transform_profiles(profiles):
"""Transforms a List[AcceleratorOption] into a table with decimal representation of cost."""
header = [
'Instance Type',
'Accelerator',
'Cost/M Input Tokens',
'Cost/M Output Tokens',
'Output Tokens/s',
'NTPOT(ms)',
'TTFT(ms)',
'ITL(ms)',
'Model Server',
'Model Server Version',
'Model',
'Use Case',
'Average Input Length',
'Average Output Length',
'Serving Stack',
'Serving Stack Version',
]
rows = [header]
for p in profiles:
input_token_cost, output_token_cost = get_decimal_cost(
p.performanceStats[0].cost if p.performanceStats else None
)
row = [
p.instanceType,
p.acceleratorType,
input_token_cost,
output_token_cost,
p.performanceStats[0].outputTokensPerSecond
if p.performanceStats[0]
else None,
p.performanceStats[0].ntpotMilliseconds if p.performanceStats else None,
p.performanceStats[0].ttftMilliseconds if p.performanceStats else None,
p.performanceStats[0].itlMilliseconds if p.performanceStats else None,
p.modelServerInfo.modelServer,
p.modelServerInfo.modelServerVersion,
p.modelServerInfo.model,
p.workloadSpec.useCase,
p.workloadSpec.averageInputLength,
p.workloadSpec.averageOutputLength,
p.servingStack.name if p.servingStack else None,
p.servingStack.version if p.servingStack else None,
]
rows.append(row)
profiles_table = cp.Table(rows)
return cp.Section([profiles_table], max_column_width=60)
class ProfilePrinter(cp.CustomPrinterBase):
"""Prints a service's profile in a custom human-readable format."""
def Print(self, resources, single=True, intermediate=False):
"""Overrides ResourcePrinter.Print to set single=True."""
super(ProfilePrinter, self).Print(resources, True, intermediate)
def Transform(self, profiles):
"""Transforms a List[TrafficTargetPair] into a marker class format."""
return _transform_profiles(profiles)

View File

@@ -0,0 +1,170 @@
# -*- 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.
"""Revision-specific printer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.api_lib.run import container_resource
from googlecloudsdk.api_lib.run import revision
from googlecloudsdk.command_lib.run.printers import container_and_volume_printer_util as container_util
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.core.resource import custom_printer_base as cp
REVISION_PRINTER_FORMAT = 'revision'
CPU_ALWAYS_ALLOCATED_MESSAGE = 'CPU is always allocated'
CPU_THROTTLED_MESSAGE = 'CPU is only allocated during request processing'
HTTP2_PORT_NAME = 'h2c'
EXECUTION_ENV_VALS = {'gen1': 'First Generation', 'gen2': 'Second Generation'}
class RevisionPrinter(cp.CustomPrinterBase):
"""Prints the run Revision in a custom human-readable format.
Format specific to Cloud Run revisions. Only available on Cloud Run commands
that print revisions.
"""
def Transform(self, record):
"""Transform a service into the output structure of marker classes."""
fmt = cp.Lines([
k8s_util.BuildHeader(record),
k8s_util.GetLabels(record.labels),
' ',
self.TransformSpec(record),
k8s_util.FormatReadyMessage(record),
RevisionPrinter.CurrentMinInstances(record),
])
return fmt
@staticmethod
def GetTimeout(record):
if record.timeout is not None:
return '{}s'.format(record.timeout)
return None
@staticmethod
def GetMinInstances(record):
return record.annotations.get(revision.MIN_SCALE_ANNOTATION, '')
@staticmethod
def GetMaxInstances(record):
return record.annotations.get(revision.MAX_SCALE_ANNOTATION, '')
@staticmethod
def GetTargetCpuUtilization(record):
return record.annotations.get(revision.CPU_UTILIZATION_ANNOTATION, '')
@staticmethod
def GetTargetConcurrencyUtilization(record):
return record.annotations.get(
revision.CONCURRENCY_UTILIZATION_ANNOTATION, ''
)
@staticmethod
def GetCMEK(record):
cmek_key = record.annotations.get(container_resource.CMEK_KEY_ANNOTATION)
if not cmek_key:
return ''
cmek_name = cmek_key.split('/')[-1]
return cmek_name
@staticmethod
def GetCpuAllocation(record):
cpu_throttled = record.annotations.get(
container_resource.CPU_THROTTLE_ANNOTATION
)
if not cpu_throttled:
return ''
elif cpu_throttled.lower() == 'false':
return CPU_ALWAYS_ALLOCATED_MESSAGE
else:
return CPU_THROTTLED_MESSAGE
@staticmethod
def GetHttp2Enabled(record):
for port in record.container.ports:
if port.name == HTTP2_PORT_NAME:
return 'Enabled'
return ''
@staticmethod
def GetExecutionEnv(record):
execution_env_value = k8s_util.GetExecutionEnvironment(record)
if execution_env_value in EXECUTION_ENV_VALS:
return EXECUTION_ENV_VALS[execution_env_value]
return execution_env_value
@staticmethod
def GetSessionAffinity(record):
return record.annotations.get(revision.SESSION_AFFINITY_ANNOTATION, '')
@staticmethod
def GetThreatDetectionEnabled(record):
return k8s_util.GetThreatDetectionEnabled(record)
@staticmethod
def TransformSpec(
record: revision.Revision, manual_scaling_enabled=False
) -> cp.Lines:
labels = [
('Service account', record.spec.serviceAccountName),
('Concurrency', record.concurrency)]
if not manual_scaling_enabled:
labels.extend([
('Min instances', RevisionPrinter.GetMinInstances(record)),
('Max instances', RevisionPrinter.GetMaxInstances(record)),
(
'Target CPU utilization',
RevisionPrinter.GetTargetCpuUtilization(record),
),
(
'Target concurrency utilization',
RevisionPrinter.GetTargetConcurrencyUtilization(record),
),
])
labels.extend([
(
'SQL connections',
k8s_util.GetCloudSqlInstances(record.annotations),
),
('Timeout', RevisionPrinter.GetTimeout(record)),
('VPC access', k8s_util.GetVpcNetwork(record.annotations)),
('CMEK', RevisionPrinter.GetCMEK(record)),
('HTTP/2 Enabled', RevisionPrinter.GetHttp2Enabled(record)),
('CPU Allocation', RevisionPrinter.GetCpuAllocation(record)),
(
'Execution Environment',
RevisionPrinter.GetExecutionEnv(record),
),
(
'Session Affinity',
RevisionPrinter.GetSessionAffinity(record),
),
('Volumes', container_util.GetVolumes(record)),
('Threat Detection', RevisionPrinter.GetThreatDetectionEnabled(record))
])
return cp.Lines([container_util.GetContainers(record), cp.Labeled(labels)])
@staticmethod
def CurrentMinInstances(record):
return cp.Labeled([
(
'Current Min Instances',
getattr(record.status, 'desiredReplicas', None),
),
])

View File

@@ -0,0 +1,201 @@
# -*- 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.
"""Service-specific printer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import json
from googlecloudsdk.api_lib.run import service
from googlecloudsdk.command_lib.run import threat_detection_util as crtd_util
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.command_lib.run.printers import revision_printer
from googlecloudsdk.command_lib.run.printers import traffic_printer
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.resource import custom_printer_base as cp
SERVICE_PRINTER_FORMAT = 'service'
PRESET_ANNOTATION = 'run.googleapis.com/presets'
class ServicePrinter(cp.CustomPrinterBase):
"""Prints the run Service in a custom human-readable format.
Format specific to Cloud Run services. Only available on Cloud Run commands
that print services.
"""
with_presets = False
def _GetRevisionHeader(self, record):
header = ''
if record.status is None:
header = 'Unknown revision'
else:
header = 'Revision {}'.format(record.status.latestCreatedRevisionName)
return console_attr.GetConsoleAttr().Emphasize(header)
def _RevisionPrinters(self, record):
"""Adds printers for the revision."""
manual_scaling_enabled = False
if (
record.annotations.get(service.SERVICE_SCALING_MODE_ANNOTATION, '')
== 'manual'
):
manual_scaling_enabled = True
return cp.Lines([
self._GetPresetInfo(record) if self.with_presets else '',
self._GetRevisionHeader(record),
k8s_util.GetLabels(record.template.labels),
revision_printer.RevisionPrinter.TransformSpec(
record.template, manual_scaling_enabled
),
])
def _GetServiceSettings(self, record):
"""Adds service-level values."""
labels = [
cp.Labeled([
('Binary Authorization', k8s_util.GetBinAuthzPolicy(record)),
])
]
scaling_mode = self._GetScalingMode(record)
if scaling_mode:
scaling_mode_label = cp.Labeled([
('Scaling', scaling_mode),
])
labels.append(scaling_mode_label)
breakglass_value = k8s_util.GetBinAuthzBreakglass(record)
if breakglass_value is not None:
# Show breakglass even if empty, but only if set. There's no skip_none
# option so this the workaround.
breakglass_label = cp.Labeled([
('Breakglass Justification', breakglass_value),
])
breakglass_label.skip_empty = False
labels.append(breakglass_label)
description = k8s_util.GetDescription(record)
if description is not None:
description_label = cp.Labeled([
('Description', description),
])
labels.append(description_label)
labels.append(
cp.Labeled([
(
'Threat Detection',
crtd_util.PrintThreatDetectionState(
record.threat_detection_state
),
),
])
)
return cp.Section(labels)
def _GetPresetInfo(self, record):
"""Adds preset information if available."""
preset_annotation = record.annotations.get(PRESET_ANNOTATION)
if preset_annotation:
try:
presets_list = json.loads(preset_annotation)
if isinstance(presets_list, list) and presets_list:
preset_sections = []
for p in presets_list:
if isinstance(p, dict) and p.get('type'):
preset_type = p.get('type')
params = []
for key, value in p.items():
if key == 'config' and isinstance(value, dict):
for config_key, config_value in value.items():
params.append((config_key, config_value))
elif key != 'type':
params.append((key, value))
preset_sections.append((preset_type, cp.Labeled(params)))
if preset_sections:
return cp.Labeled([('Presets', cp.Table(preset_sections))])
except (ValueError, TypeError):
# Silently ignore if the annotation is not valid JSON.
pass
return ''
def BuildHeader(self, record):
return k8s_util.BuildHeader(record)
def _GetScalingMode(self, record):
"""Returns the scaling mode of the service."""
scaling_mode = record.annotations.get(
service.SERVICE_SCALING_MODE_ANNOTATION, ''
)
if scaling_mode == 'manual':
instance_count = record.annotations.get(
service.MANUAL_INSTANCE_COUNT_ANNOTATION, ''
)
return 'Manual (Instances: %s)' % instance_count
else:
min_instance_count = record.annotations.get(
service.SERVICE_MIN_SCALE_ANNOTATION, '0'
)
max_instance_count = record.annotations.get(
service.SERVICE_MAX_SCALE_ANNOTATION, ''
)
if max_instance_count:
return 'Auto (Min: %s, Max: %s)' % (
min_instance_count,
max_instance_count,
)
return 'Auto (Min: %s)' % min_instance_count
def Transform(self, record):
"""Transform a service into the output structure of marker classes."""
service_settings = self._GetServiceSettings(record)
lines = [
self.BuildHeader(record),
k8s_util.GetLabels(record.labels),
]
lines.extend([
' ',
traffic_printer.TransformRouteFields(record),
' ',
service_settings,
(' ' if service_settings.WillPrintOutput() else ''),
cp.Labeled([(
k8s_util.LastUpdatedMessage(record),
self._RevisionPrinters(record),
)]),
k8s_util.FormatReadyMessage(record),
])
return cp.Lines(lines)
class ServicePrinterAlpha(ServicePrinter):
"""Prints the run Service in a custom human-readable format."""
with_presets = True
class MultiRegionServicePrinter(ServicePrinter):
"""Prints the run MultiRegionService in a custom human-readable format."""
def BuildHeader(self, record):
return k8s_util.BuildHeader(record, is_multi_region=True)

View File

@@ -0,0 +1,135 @@
# -*- 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.
"""Traffic-specific printer and functions for generating traffic formats."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.api_lib.run import service
from googlecloudsdk.api_lib.run import traffic_pair
from googlecloudsdk.command_lib.run import platforms
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.resource import custom_printer_base as cp
TRAFFIC_PRINTER_FORMAT = 'traffic'
_INGRESS_UNSPECIFIED = '-'
def _GetIngress(record):
"""Gets the ingress traffic allowed to call the service."""
if platforms.GetPlatform() == platforms.PLATFORM_MANAGED:
spec_ingress = record.annotations.get(service.INGRESS_ANNOTATION)
status_ingress = record.annotations.get(service.INGRESS_STATUS_ANNOTATION)
if spec_ingress == status_ingress:
return spec_ingress
else:
spec_ingress = spec_ingress or _INGRESS_UNSPECIFIED
status_ingress = status_ingress or _INGRESS_UNSPECIFIED
return '{} (currently {})'.format(spec_ingress, status_ingress)
elif (record.labels.get(
service.ENDPOINT_VISIBILITY) == service.CLUSTER_LOCAL):
return service.INGRESS_INTERNAL
else:
return service.INGRESS_ALL
def _GetIap(record):
"""Gets the IAP traffic allowed to call the service."""
return record.annotations.get(service.IAP_ANNOTATION)
def _GetTagAndStatus(tag):
"""Returns the tag with padding and an adding/removing indicator if needed."""
if tag.inSpec and not tag.inStatus:
return ' {} (Adding):'.format(tag.tag)
elif not tag.inSpec and tag.inStatus:
return ' {} (Deleting):'.format(tag.tag)
else:
return ' {}:'.format(tag.tag)
def _TransformTrafficPair(pair):
"""Transforms a single TrafficTargetPair into a marker class structure."""
console = console_attr.GetConsoleAttr()
return (pair.displayPercent, console.Emphasize(pair.displayRevisionId),
cp.Table([('', _GetTagAndStatus(t), t.url) for t in pair.tags]))
def _TransformTrafficPairs(
traffic_pairs, service_url, service_ingress=None, service_iap=None
):
"""Transforms a List[TrafficTargetPair] into a marker class structure."""
traffic_section = cp.Section(
[cp.Table(_TransformTrafficPair(p) for p in traffic_pairs)]
)
route_section = [cp.Labeled([('URL', service_url)])]
if service_ingress is not None:
route_section.append(cp.Labeled([('Ingress', service_ingress)]))
if service_iap is not None:
route_section.append(cp.Labeled([('Iap Enabled', service_iap)]))
route_section.append(cp.Labeled([('Traffic', traffic_section)]))
return cp.Section(route_section, max_column_width=60)
def TransformRouteFields(service_record):
"""Transforms a service's route fields into a marker class structure to print.
Generates the custom printing format for a service's url, ingress, and traffic
using the marker classes defined in custom_printer_base.
Args:
service_record: A Service object.
Returns:
A custom printer marker object describing the route fields print format.
"""
no_status = service_record.status is None
traffic_pairs = traffic_pair.GetTrafficTargetPairs(
service_record.spec_traffic, service_record.status_traffic,
service_record.is_managed,
(_INGRESS_UNSPECIFIED
if no_status else service_record.status.latestReadyRevisionName))
return _TransformTrafficPairs(
traffic_pairs,
'' if no_status else service_record.domain,
_GetIngress(service_record),
_GetIap(service_record),
)
class TrafficPrinter(cp.CustomPrinterBase):
"""Prints a service's traffic in a custom human-readable format."""
def Print(self, resources, single=False, intermediate=False):
"""Overrides ResourcePrinter.Print to set single=True."""
# The update-traffic command returns a List[TrafficTargetPair] as its
# result. In order to print the custom human-readable format, this printer
# needs to process all records in the result at once (to compute column
# widths). By default, ResourcePrinter interprets a List[*] as a list of
# separate records and passes the contents of the list to this printer
# one-by-one. Setting single=True forces ResourcePrinter to treat the
# result as one record and pass the entire list to this printer in one call.
super(TrafficPrinter, self).Print(resources, True, intermediate)
def Transform(self, record):
"""Transforms a List[TrafficTargetPair] into a marker class format."""
if record:
service_url = record[0].serviceUrl
else:
service_url = ''
return _TransformTrafficPairs(record, service_url)

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Contains shared methods for container printing."""
from typing import Sequence
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.core.resource import custom_printer_base as cp
from googlecloudsdk.generated_clients.gapic_clients.run_v2.types import k8s_min
def _GetUserEnvironmentVariables(container: k8s_min.Container):
return cp.Mapped(
k8s_util.OrderByKey(
{env_var.name: env_var.value for env_var in container.env}
)
)
def _GetContainer(container: k8s_min.Container) -> cp.Table:
return cp.Labeled([
('Image', container.image),
('Command', ' '.join(container.command)),
('Args', ' '.join(container.args)),
('Memory', container.resources.limits['memory']),
('CPU', container.resources.limits['cpu']),
(
'Env vars',
_GetUserEnvironmentVariables(container),
),
# TODO(b/366115709): add volume mounts
# TODO(b/366115709): add secrets
('Container Dependencies', ', '.join(container.depends_on)),
])
def GetContainers(containers: Sequence[k8s_min.Container]) -> cp.Table:
"""Returns a formatted table of a resource's containers.
Args:
containers: A list of containers.
Returns:
A formatted table of a resource's containers.
"""
def Containers():
containers_dict = {container.name: container for container in containers}
for _, container in k8s_util.OrderByKey(containers_dict):
key = f'Container {container.name}'
value = _GetContainer(container)
yield (key, value)
return cp.Mapped(Containers())

View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""V2 WorkerPool instance split specific printer."""
from typing import List
from googlecloudsdk.command_lib.run.v2 import instance_split
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.resource import custom_printer_base as cp
from googlecloudsdk.generated_clients.gapic_clients.run_v2.types import worker_pool as worker_pool_objects
INSTANCE_SPLIT_PRINTER_FORMAT = 'instancesplit'
def TransformWorkerPoolInstanceSplit(
record: worker_pool_objects.WorkerPool,
) -> cp.Section:
"""Transforms a worker pool into the output structure of instance split marker classes."""
instance_split_pairs = instance_split.GetInstanceSplitPairs(record)
split_section = _TransformInstanceSplitPairs(instance_split_pairs)
return cp.Section(
[cp.Labeled([('Instance Split', split_section)])], max_column_width=60
)
def _TransformInstanceSplitPair(
pair: instance_split.InstanceSplitPair,
):
"""Transforms a single InstanceSplitPair into a marker class structure."""
console = console_attr.GetConsoleAttr()
return (pair.display_percent, console.Emphasize(pair.display_revision_id))
def _TransformInstanceSplitPairs(
pairs: List[instance_split.InstanceSplitPair],
) -> cp.Section:
"""Transforms a list of InstanceSplitPairs into a marker class structure."""
return cp.Section([cp.Table(_TransformInstanceSplitPair(p) for p in pairs)])
class InstanceSplitPrinter(cp.CustomPrinterBase):
"""Prints the Run v2 WorkerPool instance split in a custom human-readable format."""
def Print(self, resources, intermediate=False):
"""Overrides ResourcePrinter.Print to set single=True."""
# The update-instance-split command returns a List[InstanceSplitPair] as its
# result. In order to print the custom human-readable format, this printer
# needs to process all records in the result at once (to compute column
# widths). By default, ResourcePrinter interprets a List[*] as a list of
# separate records and passes the contents of the list to this printer
# one-by-one. Setting single=True forces ResourcePrinter to treat the
# result as one record and pass the entire list to this printer in one call.
super(InstanceSplitPrinter, self).Print(
resources, single=True, intermediate=intermediate
)
def Transform(self, record: List[instance_split.InstanceSplitPair]):
"""Transform instance split pairs into the output structure of instance split marker classes."""
split_section = _TransformInstanceSplitPairs(record)
return cp.Section(
[cp.Labeled([('Instance Split', split_section)])], max_column_width=60
)

View File

@@ -0,0 +1,290 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Contains shared methods for printing k8s object in a human-readable way."""
import textwrap
from googlecloudsdk.command_lib.run import resource_name_conversion
from googlecloudsdk.command_lib.run.v2 import conditions
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.resource import custom_printer_base as cp
from googlecloudsdk.generated_clients.gapic_clients.run_v2.types import condition as condition_objects
from googlecloudsdk.generated_clients.gapic_clients.run_v2.types import vendor_settings
_CONDITION_SUCCEEDED_VALUE = (
condition_objects.Condition.State.CONDITION_SUCCEEDED.value
)
def _PickSymbol(best, alt, encoding):
"""Choose the best symbol (if it's in this encoding) or an alternate."""
try:
best.encode(encoding)
return best
except UnicodeError:
return alt
def ReadySymbolAndColor(record):
"""Return a tuple of ready_symbol and display color for this object."""
encoding = console_attr.GetConsoleAttr().GetEncoding()
terminal_condition = conditions.GetTerminalCondition(record)
if terminal_condition is None:
return (
_PickSymbol('\N{HORIZONTAL ELLIPSIS}', '.', encoding),
'yellow',
)
elif conditions.IsConditionReady(terminal_condition):
return _PickSymbol('\N{HEAVY CHECK MARK}', '+', encoding), 'green'
else:
return 'X', 'red'
def FormatReadyMessage(record):
"""Returns the record's status condition Ready (or equivalent) message."""
terminal_condition = conditions.GetTerminalCondition(record)
if terminal_condition and terminal_condition.message:
symbol, color = ReadySymbolAndColor(record)
return console_attr.GetConsoleAttr().Colorize(
textwrap.fill('{} {}'.format(symbol, terminal_condition.message), 100),
color,
)
elif terminal_condition is None:
return console_attr.GetConsoleAttr().Colorize(
'Error getting status information', 'red'
)
else:
return ''
def LastUpdatedMessage(record):
if not record.terminal_condition:
return 'Unknown update information'
modifier = record.last_modifier or '?'
last_transition_time = '?'
if record.terminal_condition.last_transition_time:
last_transition_time = record.terminal_condition.last_transition_time
return 'Last updated on {} by {}'.format(last_transition_time, modifier)
def BuildHeader(record, is_multi_region=False, is_child_resource=False):
"""Returns a display header for a resource."""
con = console_attr.GetConsoleAttr()
status = con.Colorize(*ReadySymbolAndColor(record))
if is_child_resource:
_, region, _, _, resource_kind, name = (
resource_name_conversion.GetInfoFromFullChildName(record.name)
)
else:
_, region, resource_kind, name = (
resource_name_conversion.GetInfoFromFullName(record.name)
)
place = ('regions ' if is_multi_region else 'region ') + region
kind = ('Multi-Region ' if is_multi_region else '') + resource_kind
return con.Emphasize('{} {} {} in {}'.format(status, kind, name, place))
def GetVpcNetwork(record):
"""Returns the VPC Network setting.
Either the values of the vpc-access-connector and vpc-access-egress, or the
values of the network and subnetwork in network-interfaces annotation and
vpc-access-egress.
Args:
record:
googlecloudsdk.generated_clients.gapic_clients.run_v2.types.vendor_settings.VpcAccess.
"""
def _GetEgress(egress):
if egress == vendor_settings.VpcAccess.VpcEgress.ALL_TRAFFIC:
return 'all-traffic'
elif egress == vendor_settings.VpcAccess.VpcEgress.PRIVATE_RANGES_ONLY:
return 'private-ranges-only'
return ''
connector = record.connector
if connector:
return cp.Labeled([
('Connector', connector),
(
'Egress',
_GetEgress(record.egress),
),
])
# Direct VPC case if annoation exists.
if not record.network_interfaces:
return ''
try:
network_interface = record.network_interfaces[0]
return cp.Labeled([
(
'Network',
network_interface.network if network_interface.network else '',
),
(
'Subnet',
network_interface.subnetwork
if network_interface.subnetwork
else '',
),
(
'Egress',
_GetEgress(record.egress),
),
])
except Exception: # pylint: disable=broad-except
return ''
def GetNameFromDict(resource):
"""Extracts short name from a resource.
Args:
resource: dict representing a Cloud Run v2 resource.
Returns:
Short name of the resource.
"""
_, _, _, name = resource_name_conversion.GetInfoFromFullName(
resource.get('name')
)
return name
def GetChildNameFromDict(resource):
"""Extracts short name from a resource.
Args:
resource: dict representing a Cloud Run v2 child resource.
Returns:
Short name of the resource.
"""
_, _, _, _, _, name = resource_name_conversion.GetInfoFromFullChildName(
resource.get('name')
)
return name
def GetRegionFromDict(resource):
"""Extracts region from a resource.
Args:
resource: dict representing a Cloud Run v2 resource.
Returns:
Region of the resource.
"""
_, region, _, _ = resource_name_conversion.GetInfoFromFullName(
resource.get('name')
)
return region
def GetParentFromDict(resource):
"""Extracts region from a child resource.
Args:
resource: dict representing a Cloud Run v2 child resource.
Returns:
Region of the resource.
"""
_, _, _, parent, _, _ = resource_name_conversion.GetInfoFromFullChildName(
resource.get('name')
)
return parent
def GetLastTransitionTimeFromDict(resource):
"""Extracts last transition time from a resource.
Args:
resource: dict representing a Cloud Run v2 resource.
Returns:
Last transition time of the resource if it exists, otherwise None.
"""
if resource.get('terminal_condition'):
result = resource.get('terminal_condition').get('last_transition_time')
if result:
return result
return None
def _GetConditionFromDict(resource, condition_type):
"""Returns the condition matching the given type from a resource."""
for condition in resource.get('conditions'):
if condition.get('type_') == condition_type:
return condition
return None
def _GetReadyConditionFromDict(resource):
"""Returns the ready condition of a resource."""
if resource.get('terminal_condition'):
return resource.get('terminal_condition')
return _GetConditionFromDict(resource, 'Ready')
def GetReadySymbolFromDict(resource):
"""Return a ready_symbol for a resource.
Args:
resource: dict representing a Cloud Run v2 resource.
Returns:
A string representing the symbol for the resource ready state.
"""
encoding = console_attr.GetConsoleAttr().GetEncoding()
ready_condition = _GetReadyConditionFromDict(resource)
if ready_condition is None:
return _PickSymbol('\N{HORIZONTAL ELLIPSIS}', '.', encoding)
elif ready_condition.get('state') == _CONDITION_SUCCEEDED_VALUE:
return _PickSymbol('\N{HEAVY CHECK MARK}', '+', encoding)
else:
return 'X'
def GetActiveStateFromDict(resource):
"""Return active state for a resource.
Args:
resource: dict representing a Cloud Run v2 resource.
Returns:
True if the resource is active, false otherwise.
"""
active_condition = _GetConditionFromDict(resource, 'Active')
if active_condition:
return active_condition.get('state') == _CONDITION_SUCCEEDED_VALUE
return False
def GetCMEK(cmek_key: str) -> str:
"""Returns the CMEK name from a full CMEK key name.
Args:
cmek_key: The full CMEK key name.
Returns:
The CMEK name.
"""
if not cmek_key:
return ''
cmek_name = cmek_key.split('/')[-1]
return cmek_name

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""V2 Revision specific printer."""
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.command_lib.run.printers.v2 import container_printer
from googlecloudsdk.command_lib.run.printers.v2 import printer_util
from googlecloudsdk.command_lib.run.printers.v2 import volume_printer
from googlecloudsdk.core.resource import custom_printer_base as cp
from googlecloudsdk.generated_clients.gapic_clients.run_v2.types import revision
REVISION_PRINTER_FORMAT = 'revision'
class RevisionPrinter(cp.CustomPrinterBase):
"""Prints the Run v2 Revision in a custom human-readable format.
Format specific to Cloud Run revisions. Only available on Cloud Run
commands that print worker revisions.
"""
def Transform(self, record: revision.Revision):
"""Transform a revision into the output structure of marker classes."""
fmt = cp.Lines([
printer_util.BuildHeader(record, is_child_resource=True),
k8s_util.GetLabels(record.labels),
' ',
self.TransformSpec(record),
printer_util.FormatReadyMessage(record),
])
return fmt
@staticmethod
def TransformSpec(record: revision.Revision) -> cp.Lines:
labels = [('Service account', record.service_account)]
labels.extend([
# TODO(b/366115709): add SQL connections printer.
('VPC access', printer_util.GetVpcNetwork(record.vpc_access)),
('CMEK', printer_util.GetCMEK(record.encryption_key)),
('Session Affinity', 'True' if record.session_affinity else ''),
('Volumes', volume_printer.GetVolumes(record.volumes)),
])
return cp.Lines(
[container_printer.GetContainers(record.containers), cp.Labeled(labels)]
)

View File

@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Contains shared methods for volume printing."""
from typing import Sequence
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.core.resource import custom_printer_base as cp
from googlecloudsdk.generated_clients.gapic_clients.run_v2.types import k8s_min
def _FormatVersionToPath(
version_to_path: k8s_min.VersionToPath,
) -> str:
return (
f'path: {version_to_path.path}, version: {version_to_path.version}, mode:'
f' {version_to_path.mode}'
)
def _FormatVolume(volume: k8s_min.Volume) -> cp.Table:
"""Format a volume for the volumes list."""
if volume.empty_dir:
return cp.Labeled([
('type', 'in-memory'),
('size-limit', volume.empty_dir.size_limit),
])
elif volume.nfs:
return cp.Labeled([
('type', 'nfs'),
('location', '{}:{}'.format(volume.nfs.server, volume.nfs.path)),
('read-only', volume.nfs.read_only),
])
elif volume.gcs:
return cp.Labeled([
('type', 'cloud-storage'),
('bucket', volume.gcs.bucket),
('read-only', volume.gcs.read_only),
('mount-options', volume.gcs.mount_options),
])
elif volume.secret:
return cp.Labeled([
('type', 'secret'),
('secret', volume.secret.secret),
('default-mode', volume.secret.default_mode),
('items', [_FormatVersionToPath(i) for i in volume.secret.items]),
])
elif volume.cloud_sql_instance:
return cp.Labeled([
('type', 'cloudsql'),
('instances', volume.cloud_sql_instance.instances),
])
else:
return cp.Labeled([('type', 'unknown')])
def GetVolumes(volumes: Sequence[k8s_min.Volume]) -> cp.Table:
"""Returns a formatted table of a resource's volumes.
Args:
volumes: A list of volumes.
Returns:
A formatted table of a resource's volumes.
"""
def Volumes():
volumes_dict = {volume.name: volume for volume in volumes}
for _, volume in k8s_util.OrderByKey(volumes_dict):
key = f'volume {volume.name}'
value = _FormatVolume(volume)
yield (key, value)
return cp.Mapped(Volumes())

View File

@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""V2 WorkerPool specific printer."""
from googlecloudsdk.command_lib.run import resource_name_conversion
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.command_lib.run.printers.v2 import container_printer
from googlecloudsdk.command_lib.run.printers.v2 import instance_split_printer
from googlecloudsdk.command_lib.run.printers.v2 import printer_util
from googlecloudsdk.command_lib.run.printers.v2 import volume_printer
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.resource import custom_printer_base as cp
from googlecloudsdk.generated_clients.gapic_clients.run_v2.types import vendor_settings
from googlecloudsdk.generated_clients.gapic_clients.run_v2.types import worker_pool as worker_pool_objects
from googlecloudsdk.generated_clients.gapic_clients.run_v2.types import worker_pool_revision_template as revision_template_objects
WORKER_POOL_PRINTER_FORMAT = 'workerpool'
class WorkerPoolPrinter(cp.CustomPrinterBase):
"""Prints the Run v2 WorkerPool in a custom human-readable format.
Format specific to Cloud Run worker pools. Only available on Cloud Run
commands that print worker pools.
"""
def _GetRevisionHeader(self, record: worker_pool_objects.WorkerPool):
header = 'Unknown revision'
if record.latest_created_revision:
header = 'Revision {}'.format(
resource_name_conversion.GetNameFromFullChildName(
record.latest_created_revision
)
)
return console_attr.GetConsoleAttr().Emphasize(header)
def _TransformTemplate(
self, record: revision_template_objects.WorkerPoolRevisionTemplate
):
labels = [('Service account', record.service_account)]
labels.extend([
# TODO(b/366115709): add SQL connections printer.
('VPC access', printer_util.GetVpcNetwork(record.vpc_access)),
('CMEK', printer_util.GetCMEK(record.encryption_key)),
('Session Affinity', 'True' if record.session_affinity else ''),
('Volumes', volume_printer.GetVolumes(record.volumes)),
])
return cp.Lines([
container_printer.GetContainers(record.containers),
cp.Labeled(labels),
])
def _RevisionPrinters(self, record: worker_pool_objects.WorkerPool):
"""Adds printers for the revision."""
return cp.Lines([
self._GetRevisionHeader(record),
k8s_util.GetLabels(record.template.labels),
self._TransformTemplate(record.template),
])
def _GetBinaryAuthorization(self, record: worker_pool_objects.WorkerPool):
"""Adds worker pool level values."""
if record.binary_authorization is None:
return None
if record.binary_authorization.use_default:
return 'Default'
return record.binary_authorization.policy
def _GetWorkerPoolSettings(self, record: worker_pool_objects.WorkerPool):
"""Adds worker pool level values."""
labels = [
cp.Labeled([
('Binary Authorization', self._GetBinaryAuthorization(record)),
])
]
breakglass_value = record.binary_authorization.breakglass_justification
if breakglass_value:
# Show breakglass even if empty, but only if set. There's no skip_none
# option so this the workaround.
breakglass_label = cp.Labeled([
('Breakglass Justification', breakglass_value),
])
breakglass_label.skip_empty = False
labels.append(breakglass_label)
description = record.description
if description:
description_label = cp.Labeled([
('Description', description),
])
labels.append(description_label)
scaling_mode = self._GetScalingMode(record)
if scaling_mode:
scaling_mode_label = cp.Labeled([
('Scaling', scaling_mode),
])
labels.append(scaling_mode_label)
return cp.Section(labels)
def _GetScalingMode(self, record: worker_pool_objects.WorkerPool):
"""Returns the scaling mode of the worker pool."""
scaling_mode = record.scaling.scaling_mode
if scaling_mode == vendor_settings.WorkerPoolScaling.ScalingMode.MANUAL:
instance_count = record.scaling.manual_instance_count
return 'Manual (Instances: %s)' % instance_count
else:
instance_count = record.scaling.min_instance_count
if record.scaling.max_instance_count:
return 'Auto (Min: %s, Max: %s)' % (
instance_count,
record.scaling.max_instance_count,
)
return 'Auto (Min: %s)' % instance_count
def Transform(self, record: worker_pool_objects.WorkerPool):
"""Transform a worker pool into the output structure of marker classes."""
worker_pool_settings = self._GetWorkerPoolSettings(record)
fmt = cp.Lines([
printer_util.BuildHeader(record),
k8s_util.GetLabels(record.labels),
' ',
instance_split_printer.TransformWorkerPoolInstanceSplit(record),
' ',
worker_pool_settings,
(' ' if worker_pool_settings.WillPrintOutput() else ''),
cp.Labeled([(
printer_util.LastUpdatedMessage(record),
self._RevisionPrinters(record),
)]),
printer_util.FormatReadyMessage(record),
])
return fmt

View File

@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""WorkerPool specific printer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.api_lib.run import worker_pool
from googlecloudsdk.command_lib.run.printers import instance_split_printer
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.command_lib.run.printers import worker_pool_revision_printer
from googlecloudsdk.core.console import console_attr
from googlecloudsdk.core.resource import custom_printer_base as cp
WORKER_POOL_PRINTER_FORMAT = 'workerpool'
class WorkerPoolPrinter(cp.CustomPrinterBase):
"""Prints the run WorkerPool in a custom human-readable format.
Format specific to Cloud Run worker pools. Only available on Cloud Run
commands
that print worker pools.
"""
def _BuildWorkerPoolHeader(self, record):
con = console_attr.GetConsoleAttr()
status = con.Colorize(*record.ReadySymbolAndColor())
try:
place = 'region ' + record.region
except KeyError:
place = 'namespace ' + record.namespace
return con.Emphasize(
'{} {} {} in {}'.format(status, 'WorkerPool', record.name, place)
)
def _GetRevisionHeader(self, record):
header = ''
if record.status is None:
header = 'Unknown revision'
else:
header = 'Revision {}'.format(record.status.latestCreatedRevisionName)
return console_attr.GetConsoleAttr().Emphasize(header)
def _RevisionPrinters(self, record):
"""Adds printers for the revision."""
return cp.Lines([
self._GetRevisionHeader(record),
k8s_util.GetLabels(record.template.labels),
worker_pool_revision_printer.WorkerPoolRevisionPrinter.TransformSpec(
record.template
),
])
def _GetWorkerPoolSettings(self, record):
"""Adds worker pool level values."""
labels = [
cp.Labeled([
('Binary Authorization', k8s_util.GetBinAuthzPolicy(record)),
])
]
scaling_setting = self._GetScalingSetting(record)
if scaling_setting is not None:
scaling_mode_label = cp.Labeled([
('Scaling', scaling_setting),
])
labels.append(scaling_mode_label)
breakglass_value = k8s_util.GetBinAuthzBreakglass(record)
if breakglass_value is not None:
# Show breakglass even if empty, but only if set. There's no skip_none
# option so this the workaround.
breakglass_label = cp.Labeled([
('Breakglass Justification', breakglass_value),
])
breakglass_label.skip_empty = False
labels.append(breakglass_label)
description = k8s_util.GetDescription(record)
if description is not None:
description_label = cp.Labeled([
('Description', description),
])
labels.append(description_label)
labels.append(cp.Labeled([
('Threat Detection', k8s_util.GetThreatDetectionEnabled(record)),
]))
return cp.Section(labels)
def _GetScalingSetting(self, record):
"""Returns the scaling setting for the worker pool."""
scaling_mode = record.annotations.get(
worker_pool.WORKER_POOL_SCALING_MODE_ANNOTATION, ''
)
if scaling_mode == 'manual':
instance_count = record.annotations.get(
worker_pool.MANUAL_INSTANCE_COUNT_ANNOTATION, ''
)
return 'Manual (Instances: %s)' % instance_count
else:
min_instance_count = record.annotations.get(
worker_pool.WORKER_POOL_MIN_SCALE_ANNOTATION, '0'
)
max_instance_count = record.annotations.get(
worker_pool.WORKER_POOL_MAX_SCALE_ANNOTATION, ''
)
if max_instance_count:
return 'Auto (Min: %s, Max: %s)' % (
min_instance_count,
max_instance_count,
)
else:
return 'Auto (Min: %s)' % min_instance_count
def Transform(self, record):
"""Transform a worker pool into the output structure of marker classes."""
worker_pool_settings = self._GetWorkerPoolSettings(record)
fmt = cp.Lines([
self._BuildWorkerPoolHeader(record),
k8s_util.GetLabels(record.labels),
' ',
instance_split_printer.TransformInstanceSplitFields(record),
' ',
worker_pool_settings,
(' ' if worker_pool_settings.WillPrintOutput() else ''),
cp.Labeled([(
k8s_util.LastUpdatedMessage(record),
self._RevisionPrinters(record),
)]),
k8s_util.FormatReadyMessage(record),
])
return fmt

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Worker Pool Revision specific printer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.api_lib.run import container_resource
from googlecloudsdk.api_lib.run import revision
from googlecloudsdk.command_lib.run.printers import container_and_volume_printer_util as container_util
from googlecloudsdk.command_lib.run.printers import k8s_object_printer_util as k8s_util
from googlecloudsdk.core.resource import custom_printer_base as cp
REVISION_PRINTER_FORMAT = 'revision'
CPU_ALWAYS_ALLOCATED_MESSAGE = 'CPU is always allocated'
EXECUTION_ENV_VALS = {'gen1': 'First Generation', 'gen2': 'Second Generation'}
class WorkerPoolRevisionPrinter(cp.CustomPrinterBase):
"""Prints the run Revision in a custom human-readable format.
Format specific to Cloud Run revisions. Only available on Cloud Run commands
that print revisions.
"""
def Transform(self, record):
"""Transform a revision into the output structure of marker classes."""
fmt = cp.Lines([
k8s_util.BuildHeader(record),
k8s_util.GetLabels(record.labels),
' ',
self.TransformSpec(record),
k8s_util.FormatReadyMessage(record),
])
return fmt
@staticmethod
def GetCMEK(record):
cmek_key = record.annotations.get(container_resource.CMEK_KEY_ANNOTATION)
if not cmek_key:
return ''
cmek_name = cmek_key.split('/')[-1]
return cmek_name
@staticmethod
def GetThreatDetectionEnabled(record):
return k8s_util.GetThreatDetectionEnabled(record)
@staticmethod
def TransformSpec(record: revision.Revision) -> cp.Lines:
labels = [('Service account', record.spec.serviceAccountName)]
labels.extend([
(
'SQL connections',
k8s_util.GetCloudSqlInstances(record.annotations),
),
('VPC access', k8s_util.GetVpcNetwork(record.annotations)),
('CMEK', WorkerPoolRevisionPrinter.GetCMEK(record)),
('Volumes', container_util.GetVolumes(record)),
(
'Threat Detection',
WorkerPoolRevisionPrinter.GetThreatDetectionEnabled(record),
),
])
return cp.Lines([container_util.GetContainers(record), cp.Labeled(labels)])