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,14 @@
# -*- 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.

View File

@@ -0,0 +1,740 @@
# -*- 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.
"""Shared resource arguments and flags."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import parser_arguments
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.calliope.concepts import multitype
from googlecloudsdk.command_lib.secrets import completers as secrets_completers
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.command_lib.util.concepts import presentation_specs
from googlecloudsdk.core import resources
# Args
def AddDataFile(parser, positional=False, **kwargs):
parser.add_argument(
_ArgOrFlag('data-file', positional),
metavar='PATH',
help=('File path from which to read secret data. Set this to "-" to read '
'the secret data from stdin.'),
**kwargs)
def AddOutFile(parser, positional=False, **kwargs):
parser.add_argument(
_ArgOrFlag('out-file', positional),
metavar='OUT-FILE-PATH',
help=('File path to which secret data is written. If this flag is not '
'provided secret data will be written to stdout in UTF-8 format.'),
**kwargs)
def AddProject(parser, positional=False, **kwargs):
concept_parsers.ConceptParser.ForResource(
name=_ArgOrFlag('project', positional),
resource_spec=GetProjectResourceSpec(),
group_help='The project ID.',
**kwargs).AddToParser(parser)
def AddLocation(parser, purpose, positional=False, **kwargs):
concept_parsers.ConceptParser.ForResource(
name=_ArgOrFlag('location', positional),
resource_spec=GetLocationResourceSpec(),
group_help='The location {}.'.format(purpose),
**kwargs).AddToParser(parser)
def AddReplicationPolicyFile(parser, positional=False, **kwargs):
parser.add_argument(
_ArgOrFlag('replication-policy-file', positional),
metavar='REPLICATION-POLICY-FILE',
help=(
'JSON or YAML file to use to read the replication policy. The file '
'must conform to '
'https://cloud.google.com/secret-manager/docs/reference/rest/v1/projects.secrets#replication.'
'Set this to "-" to read from stdin.'),
**kwargs)
def AddKmsKeyName(parser, positional=False, **kwargs):
parser.add_argument(
_ArgOrFlag('kms-key-name', positional),
metavar='KMS-KEY-NAME',
help=('Global KMS key with which to encrypt and decrypt the secret. Only '
'valid for secrets with an automatic replication policy.'),
**kwargs)
def AddSetKmsKeyName(parser, positional=False, **kwargs):
parser.add_argument(
_ArgOrFlag('set-kms-key', positional),
metavar='SET-KMS-KEY',
help=(
'New KMS key with which to encrypt and decrypt future secret versions.'
),
**kwargs)
def AddRemoveCmek(parser, positional=False, **kwargs):
parser.add_argument(
_ArgOrFlag('remove-cmek', positional),
action='store_true',
help=(
'Remove customer managed encryption key so that future versions will '
'be encrypted by a Google managed encryption key.'),
**kwargs)
def AddReplicaLocation(parser, positional=False, **kwargs):
parser.add_argument(
_ArgOrFlag('location', positional),
metavar='REPLICA-LOCATION',
help=('Location of replica to update. For secrets with automatic '
'replication policies, this can be omitted.'),
**kwargs)
def AddSecret(parser, purpose='', positional=False, help_text=None, **kwargs):
"""Add secret resource argument to the parser.
Args:
parser: The parser to add the argument to.
purpose: The purpose of the secret, used to generate the help text.
positional: Whether the argument is positional.
help_text: The help text to use for the argument.
**kwargs: Extra arguments.
"""
if help_text is None: # NOMUTANTS--no good way to test
help_text = 'The secret {}.'.format(purpose)
concept_parsers.ConceptParser.ForResource(
name=_ArgOrFlag('secret', positional),
resource_spec=GetSecretResourceSpec(),
group_help=help_text,
**kwargs,
).AddToParser(parser)
def AddVersion(parser, purpose, positional=False, **kwargs):
concept_parsers.ConceptParser.ForResource(
name=_ArgOrFlag('version', positional),
resource_spec=GetVersionResourceSpec(),
group_help=('Numeric secret version {}.').format(purpose),
**kwargs).AddToParser(parser)
def AddVersionOrAlias(parser, purpose, positional=False, **kwargs):
concept_parsers.ConceptParser.ForResource(
name=_ArgOrFlag('version', positional),
resource_spec=GetVersionResourceSpec(),
group_help=(
'Numeric secret version {} or a configured alias (including \'latest\' to use the latest version).'
).format(purpose),
**kwargs).AddToParser(parser)
def AddTopics(parser, positional=False, **kwargs):
parser.add_argument(
_ArgOrFlag('topics', positional),
metavar='TOPICS',
type=arg_parsers.ArgList(),
action=arg_parsers.UpdateAction,
help=('List of Pub/Sub topics to configure on the secret.'),
**kwargs)
def AddUpdateTopicsGroup(parser):
"""Add flags for specifying topics on secret updates."""
group = parser.add_group(mutex=True, help='Topics.')
group.add_argument(
_ArgOrFlag('add-topics', False),
metavar='ADD-TOPICS',
type=arg_parsers.ArgList(),
action=arg_parsers.UpdateAction,
help=('List of Pub/Sub topics to add to the secret.'))
group.add_argument(
_ArgOrFlag('remove-topics', False),
metavar='REMOVE-TOPICS',
type=arg_parsers.ArgList(),
action=arg_parsers.UpdateAction,
help=('List of Pub/Sub topics to remove from the secret.'))
group.add_argument(
_ArgOrFlag('clear-topics', False),
action='store_true',
help=('Clear all Pub/Sub topics from the secret.'))
def AddUpdateReplicationGroup(parser):
"""Add flags for specifying replication policy updates."""
group = parser.add_group(mutex=True, help='Replication update.')
group.add_argument(
_ArgOrFlag('remove-cmek', False),
action='store_true',
help=(
'Remove customer managed encryption key so that future versions will '
'be encrypted by a Google managed encryption key.'))
subgroup = group.add_group(help='CMEK Update.')
subgroup.add_argument(
_ArgOrFlag('set-kms-key', False),
metavar='SET-KMS-KEY',
help=(
'New KMS key with which to encrypt and decrypt future secret versions.'
))
subgroup.add_argument(
_ArgOrFlag('location', False),
metavar='REPLICA-LOCATION',
help=('Location of replica to update. For secrets with automatic '
'replication policies, this can be omitted.'))
def AddCreateReplicationPolicyGroup(parser):
"""Add flags for specifying replication policy on secret creation."""
group = parser.add_group(mutex=True, help='Replication policy.')
group.add_argument(
_ArgOrFlag('replication-policy-file', False),
metavar='REPLICATION-POLICY-FILE',
help=(
'JSON or YAML file to use to read the replication policy. The file '
'must conform to '
'https://cloud.google.com/secret-manager/docs/reference/rest/v1/projects.secrets#replication.'
'Set this to "-" to read from stdin.'))
subgroup = group.add_group(help='Inline replication arguments.')
subgroup.add_argument(
_ArgOrFlag('replication-policy', False),
metavar='POLICY',
help=('The type of replication policy to apply to this secret. Allowed '
'values are "automatic" and "user-managed". If user-managed then '
'--locations must also be provided.'))
subgroup.add_argument(
_ArgOrFlag('kms-key-name', False),
metavar='KMS-KEY-NAME',
help=('Global KMS key with which to encrypt and decrypt the secret. Only '
'valid for secrets with an automatic replication policy.'))
subgroup.add_argument(
_ArgOrFlag('locations', False),
action=arg_parsers.UpdateAction,
metavar='LOCATION',
type=arg_parsers.ArgList(),
help=('Comma-separated list of locations in which the secret should be '
'replicated.'))
def AddCreateVersionDestroyTTL(parser, positional=False, **kwargs):
"""Add flags for specifying version destroy ttl on secret creates."""
parser.add_argument(
_ArgOrFlag('version-destroy-ttl', positional),
metavar='VERSION-DESTROY-TTL',
type=arg_parsers.Duration(),
help=(
'Secret Version Time To Live (TTL) after destruction request. '
'For secret with TTL>0, version destruction does not happen '
'immediately on calling destroy; instead, the version goes to a '
'disabled state and destruction happens after the TTL expires. '
'See `$ gcloud topic datetimes` for information on duration formats.'
),
**kwargs,
)
def AddUpdateVersionDestroyTTL(parser, positional=False, **kwargs):
"""Add flags for specifying version destroy ttl on secret updates."""
group = parser.add_group(mutex=True, help='Version destroy ttl.')
group.add_argument(
_ArgOrFlag('version-destroy-ttl', positional),
metavar='VERSION-DESTROY-TTL',
type=arg_parsers.Duration(),
help=(
'Secret Version TTL after destruction request. '
'For secret with TTL>0, version destruction does not happen '
'immediately on calling destroy; instead, the version goes to a '
'disabled state and destruction happens after the TTL expires. '
'See `$ gcloud topic datetimes` for information on duration formats.'
),
**kwargs,
)
group.add_argument(
_ArgOrFlag('remove-version-destroy-ttl', False),
action='store_true',
help='If set, removes the version destroy TTL from the secret.',
**kwargs,
)
def AddUpdateRegionalKmsKey(
parser: parser_arguments.ArgumentInterceptor,
positional: bool = False,
**kwargs
)-> None:
"""Add flags for specifying regional cmek on secret updates.
Args:
parser: Given argument parser.
positional : Whether the argument is positional.
**kwargs: Extra arguments.
"""
group = parser.add_group(mutex=True, help='regional kms key.')
group.add_argument(
_ArgOrFlag('regional-kms-key-name', positional),
metavar='REGIONAL-KMS-KEY-NAME',
help='regional kms key name for regional secret.',
**kwargs,
)
group.add_argument(
_ArgOrFlag('remove-regional-kms-key-name', False),
action='store_true',
help='If set, removes the regional kms key.',
**kwargs,
)
def AddCreateExpirationGroup(parser):
"""Add flags for specifying expiration on secret creates."""
group = parser.add_group(mutex=True, help='Expiration.')
group.add_argument(
_ArgOrFlag('expire-time', False),
metavar='EXPIRE-TIME',
help=('Timestamp at which to automatically delete the secret.'))
group.add_argument(
_ArgOrFlag('ttl', False),
metavar='TTL',
help=(
'Duration of time (in seconds) from the running of the command until '
'the secret is automatically deleted.'))
def AddUpdateExpirationGroup(parser):
"""Add flags for specifying expiration on secret updates.."""
group = parser.add_group(mutex=True, help='Expiration.')
group.add_argument(
_ArgOrFlag('expire-time', False),
metavar='EXPIRE-TIME',
help=('Timestamp at which to automatically delete the secret.'))
group.add_argument(
_ArgOrFlag('ttl', False),
metavar='TTL',
help=(
'Duration of time (in seconds) from the running of the command until '
'the secret is automatically deleted.'))
group.add_argument(
_ArgOrFlag('remove-expiration', False),
action='store_true',
help=(
'If set, removes scheduled expiration from secret (if it had one).'))
def AddCreateRotationGroup(parser):
"""Add flags for specifying rotation on secret creates."""
group = parser.add_group(mutex=False, help='Rotation.')
group.add_argument(
_ArgOrFlag('next-rotation-time', False),
help=('Timestamp at which to send rotation notification.'))
group.add_argument(
_ArgOrFlag('rotation-period', False),
help=('Duration of time (in seconds) between rotation notifications.'))
def AddUpdateRotationGroup(parser):
"""Add flags for specifying rotation on secret updates.."""
group = parser.add_group(mutex=False, help='Rotation.')
group.add_argument(
_ArgOrFlag('next-rotation-time', False),
help=('Timestamp at which to send rotation notification.'))
group.add_argument(
_ArgOrFlag('remove-next-rotation-time', False),
action='store_true',
help=('Remove timestamp at which to send rotation notification.'))
group.add_argument(
_ArgOrFlag('rotation-period', False),
help=('Duration of time (in seconds) between rotation notifications.'))
group.add_argument(
_ArgOrFlag('remove-rotation-period', False),
action='store_true',
help=(
'If set, removes the rotation period, cancelling all rotations '
'except for the next one.'
))
group.add_argument(
_ArgOrFlag('remove-rotation-schedule', False),
action='store_true',
help=('If set, removes rotation policy from a secret.'))
def AddSecretEtag(parser, action):
"""Add flag for specifying the current secret etag."""
parser.add_argument(
_ArgOrFlag('etag', False),
metavar='ETAG',
help=(
'Current entity tag (ETag) of the secret. If specified, the secret is'
' {action} only if the ETag provided matches the current secret\'s '
'ETag.'
).format(action=action))
def AddVersionEtag(parser, action):
"""Add flag for specifying the current secret version etag."""
parser.add_argument(
_ArgOrFlag('etag', False),
metavar='ETAG',
help=(
'Current entity tag (ETag) of the secret version. If specified, the '
'version is {action} only if the ETag provided matches the current '
'version\'s ETag.'
).format(action=action))
def AddRegionalKmsKeyName(parser, positional=False, **kwargs):
"""Add flag for specifying the regional KMS key name."""
parser.add_argument(
_ArgOrFlag('regional-kms-key-name', positional),
metavar='KMS-KEY-NAME',
help=(
'Regional KMS key with which to encrypt and decrypt the secret. Only '
'valid for regional secrets.'
),
**kwargs,
)
def _ArgOrFlag(name, positional):
"""Returns the argument name in resource argument format or flag format.
Args:
name (str): name of the argument
positional (bool): whether the argument is positional
Returns:
arg (str): the argument or flag
"""
if positional:
return name.upper().replace('-', '_')
return '--{}'.format(name)
def AddGlobalOrRegionalSecret(parser, purpose='create a secret', **kwargs):
"""Adds a secret resource.
Secret resource can be global secret or regional secret. If command has
"--location" then regional secret will be created or else global secret will
be created.
Regionl secret - projects/<project>/locations/<location>/secrets/<secret>
Global secret - projects/<project>/secrets/<secret>
Args:
parser: given argument parser
purpose: help text
**kwargs: extra arguments
"""
secret_or_region_secret_spec = multitype.MultitypeResourceSpec(
'global or regional secret',
GetSecretResourceSpec(),
GetRegionalSecretResourceSpec(),
allow_inactive=True,
**kwargs,
)
concept_parsers.ConceptParser([
presentation_specs.MultitypeResourcePresentationSpec(
'secret',
secret_or_region_secret_spec,
purpose,
required=True,
hidden=True,
)
]).AddToParser(parser)
def AddGlobalOrRegionalVersion(parser, purpose='create a version', **kwargs):
"""Adds a version resource.
Args:
parser: given argument parser
purpose: help text
**kwargs: extra arguments
"""
global_or_region_version_spec = multitype.MultitypeResourceSpec(
'global or regional secret version',
GetVersionResourceSpec(),
GetRegionalVersionResourceSpec(),
allow_inactive=True,
**kwargs,
)
concept_parsers.ConceptParser([
presentation_specs.MultitypeResourcePresentationSpec(
'version',
global_or_region_version_spec,
purpose,
required=True,
hidden=True,
)
]).AddToParser(parser)
def AddGlobalOrRegionalVersionOrAlias(
parser, purpose='create a version alias', **kwargs
):
"""Adds a version resource or alias.
Args:
parser: given argument parser
purpose: help text
**kwargs: extra arguments
"""
global_or_region_version_spec = multitype.MultitypeResourceSpec(
'global or regional secret version',
GetVersionResourceSpec(),
GetRegionalVersionResourceSpec(),
allow_inactive=True,
**kwargs,
)
concept_parsers.ConceptParser([
presentation_specs.MultitypeResourcePresentationSpec(
'version',
global_or_region_version_spec,
purpose,
required=True,
hidden=True,
)
]).AddToParser(parser)
### Attribute configurations
def GetProjectAttributeConfig():
return concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG
def GetLocationAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='location',
help_text='The location of the {resource}.',
completion_request_params={'fieldMask': 'name'},
completion_id_field='name')
def GetLocationResourceAttributeConfig():
"""Returns the attribute config for location resource."""
return concepts.ResourceParameterAttributeConfig(
name='location',
help_text=(
'[EXPERIMENTAL] The location of the {resource}.'
),
completion_request_params={'fieldMask': 'name'},
completion_id_field='name',
)
def GetSecretAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='secret',
help_text='The secret of the {resource}.',
completer=secrets_completers.SecretsCompleter)
def GetRegionalSecretAttributeConfig():
"""Returns the attribute config for regional secret."""
return concepts.ResourceParameterAttributeConfig(
name='secret',
help_text='The secret of the {resource}.',
completer=secrets_completers.SecretsCompleter,
)
def GetVersionAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='version',
help_text='The version of the {resource}.',
completion_request_params={'fieldMask': 'name'},
completion_id_field='name')
def GetRegionalVersionAttributeConfig():
"""Returns the attribute config for regional secret version."""
return concepts.ResourceParameterAttributeConfig(
name='version',
help_text='The version of the {resource}.',
completion_request_params={'fieldMask': 'name'},
completion_id_field='name',
)
# Resource specs
def GetProjectResourceSpec():
return concepts.ResourceSpec(
resource_collection='secretmanager.projects',
resource_name='project',
plural_name='projects',
disable_auto_completers=False,
projectsId=GetProjectAttributeConfig())
def GetLocationResourceSpec():
return concepts.ResourceSpec(
resource_collection='secretmanager.projects.locations',
resource_name='location',
plural_name='locations',
disable_auto_completers=False,
locationsId=GetLocationAttributeConfig(),
projectsId=GetProjectAttributeConfig())
def GetSecretResourceSpec():
return concepts.ResourceSpec(
resource_collection='secretmanager.projects.secrets',
resource_name='secret',
plural_name='secrets',
disable_auto_completers=False,
secretsId=GetSecretAttributeConfig(),
projectsId=GetProjectAttributeConfig())
def GetVersionResourceSpec():
return concepts.ResourceSpec(
'secretmanager.projects.secrets.versions',
resource_name='version',
plural_name='version',
disable_auto_completers=False,
versionsId=GetVersionAttributeConfig(),
secretsId=GetSecretAttributeConfig(),
projectsId=GetProjectAttributeConfig())
def GetRegionalSecretResourceSpec():
"""Returns the resource spec for regional secret."""
return concepts.ResourceSpec(
resource_collection='secretmanager.projects.locations.secrets',
resource_name='regional secret',
plural_name='secrets',
disable_auto_completers=False,
secretsId=GetRegionalSecretAttributeConfig(),
projectsId=GetProjectAttributeConfig(),
locationsId=GetLocationResourceAttributeConfig(),
)
def GetRegionalVersionResourceSpec():
"""Returns the resource spec for regional secret version."""
return concepts.ResourceSpec(
resource_collection='secretmanager.projects.locations.secrets.versions',
resource_name='regional version',
plural_name='version',
disable_auto_completers=False,
versionsId=GetRegionalVersionAttributeConfig(),
secretsId=GetRegionalSecretAttributeConfig(),
projectsId=GetProjectAttributeConfig(),
locationsId=GetLocationResourceAttributeConfig(),
)
# Resource parsers
def ParseSecretRef(ref):
return resources.REGISTRY.Parse(
ref, collection='secretmanager.projects.secrets'
)
def ParseVersionRef(ref):
return resources.REGISTRY.Parse(
ref, collection='secretmanager.projects.secrets.versions'
)
def ParseRegionalVersionRef(ref):
"""Parses regional section version into 'secretmanager.projects.locations.secrets.versions' format .
Args:
ref: resource name of regional secret version.
Returns:
Parsed secret version.
"""
return resources.REGISTRY.Parse(
ref, collection='secretmanager.projects.locations.secrets.versions'
)
def MakeGetUriFunc(collection: str, api_version: str = 'v1'):
"""Returns a function which turns a resource into a uri.
Example:
class List(base.ListCommand):
def GetUriFunc(self):
return MakeGetUriFunc(self)
Args:
collection: A command instance.
api_version: api_version to be displayed.
Returns:
A function which can be returned in GetUriFunc.
"""
def _GetUri(resource):
registry = resources.REGISTRY.Clone()
registry.RegisterApiByName('secretmanager', api_version)
parsed = registry.Parse(resource.name, collection=collection)
return parsed.SelfLink()
return _GetUri
def GetTagsArg():
"""Makes the base.Argument for --tags flag."""
help_parts = [
'List of tags KEY=VALUE pairs to bind.',
'Each item must be expressed as',
'`<tag-key-namespaced-name>=<tag-value-short-name>`.\n',
'Example: `123/environment=production,123/costCenter=marketing`\n',
]
return base.Argument(
'--tags',
metavar='KEY=VALUE',
type=arg_parsers.ArgDict(),
action=arg_parsers.UpdateAction,
help='\n'.join(help_parts),
)
def GetTagsFromArgs(args, tags_message, tags_arg_name='tags'):
"""Makes the tags message object."""
tags = getattr(args, tags_arg_name)
if not tags:
return None
# Sorted for test stability
return tags_message(additionalProperties=[
tags_message.AdditionalProperty(key=key, value=value)
for key, value in sorted(tags.items())])

View File

@@ -0,0 +1,51 @@
# -*- 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.
"""Completers to help with tab-completing."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.command_lib.resource_manager import completers as resource_manager_completers
from googlecloudsdk.command_lib.util import completers
from googlecloudsdk.command_lib.util import parameter_info_lib
class SecretsCompleter(completers.ListCommandCompleter):
"""A completer for the secret resource argument."""
def __init__(self,
collection='secretmanager.projects.secrets',
list_command='secrets list --uri',
**kwargs):
super(SecretsCompleter, self).__init__(
collection=collection, list_command=list_command, **kwargs)
def ParameterInfo(self, parsed_args, argument):
return parameter_info_lib.ParameterInfoByConvention(
parsed_args,
argument,
self.collection,
updaters={
'projectsId': (resource_manager_completers.ProjectCompleter, True)
},
)
class BetaSecretsCompleter(SecretsCompleter):
def __init__(self, **kwargs):
super(BetaSecretsCompleter, self).__init__(
list_command='beta secrets list --uri', **kwargs)

View File

@@ -0,0 +1,34 @@
# -*- 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.
"""This module holds exceptions for secrets sruface."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.core import exceptions
class SecretError(exceptions.Error):
"""Base class for secrets failures."""
class MisconfiguredReplicationError(SecretError):
"""Replication was misconfigured."""
class MisconfiguredEncryptionError(SecretError):
"""Misconfigured encryption."""

View File

@@ -0,0 +1,242 @@
# -*- 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.
"""Commonly used display formats."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import parser_arguments
from googlecloudsdk.calliope import parser_extensions
from googlecloudsdk.command_lib.secrets import args as secrets_args
_LOCATION_TABLE = """
table(
name.basename():label=NAME,
displayName:label=LOCATION
)
"""
_SECRET_DATA = """
value[terminator="",private](
payload.data.decode(base64).decode(utf8)
)
"""
_SECRET_TABLE = """
table(
name.basename():label=NAME,
createTime.date():label=CREATED,
policy_transform():label=REPLICATION_POLICY,
locations_transform():label=LOCATIONS
)
"""
_REGIONAL_SECRET_TABLE = """
table(
name.basename():label=NAME,
createTime.date():label=CREATED
)
"""
_VERSION_TABLE = """
table(
name.basename():label=NAME,
state.enum(secrets.StateVersionJobState).color('destroyed', 'disabled', 'enabled', 'unknown'):label=STATE,
createTime.date():label=CREATED,
destroyTime.date(undefined='-'):label=DESTROYED
)
"""
_VERSION_STATE_TRANSFORMS = {
'secrets.StateVersionJobState::enum': {
'STATE_UNSPECIFIED': 'unknown',
'ENABLED': 'enabled',
'DISABLED': 'disabled',
'DESTROYED': 'destroyed',
}
}
def _TransformReplicationPolicy(r):
if 'replication' not in r:
return 'ERROR'
if 'automatic' in r['replication']:
return 'automatic'
if 'userManaged' in r['replication']:
return 'user_managed'
return 'ERROR'
def _TransformLocations(r):
if 'replication' not in r:
return 'ERROR'
if 'automatic' in r['replication']:
return '-'
if 'userManaged' in r['replication'] and 'replicas' in r['replication'][
'userManaged']:
locations = []
for replica in r['replication']['userManaged']['replicas']:
locations.append(replica['location'])
return ','.join(locations)
return 'ERROR'
_SECRET_TRANSFORMS = {
'policy_transform': _TransformReplicationPolicy,
'locations_transform': _TransformLocations,
}
def UseLocationTable(
parser: parser_arguments.ArgumentInterceptor, api_version: str = 'v1'
):
"""Table format to display locations.
Args:
parser: arguments interceptor
api_version: api version to be included in resource name
"""
parser.display_info.AddFormat(_LOCATION_TABLE)
parser.display_info.AddUriFunc(
secrets_args.MakeGetUriFunc(
'secretmanager.projects.locations', api_version=api_version
)
)
def UseSecretTable(parser: parser_arguments.ArgumentInterceptor):
"""Table format to display secrets.
Args:
parser: arguments interceptor
"""
parser.display_info.AddFormat(_SECRET_TABLE)
parser.display_info.AddTransforms(_SECRET_TRANSFORMS)
parser.display_info.AddUriFunc(
lambda r: secrets_args.ParseSecretRef(r.name).SelfLink()
)
def SecretTableUsingArgument(
args: parser_extensions.Namespace, api_version: str = 'v1'
):
"""Table format to display global secrets.
Args:
args: arguments interceptor
api_version: api version to be included in resource name
"""
args.GetDisplayInfo().AddFormat(_SECRET_TABLE)
args.GetDisplayInfo().AddTransforms(_SECRET_TRANSFORMS)
args.GetDisplayInfo().AddUriFunc(
secrets_args.MakeGetUriFunc(
'secretmanager.projects.secrets', api_version=api_version
)
)
def RegionalSecretTableUsingArgument(
args: parser_extensions.Namespace, api_version: str = 'v1'
):
"""Table format to display regional secrets.
Args:
args: arguments interceptor
api_version: api version to be included in resource name
"""
args.GetDisplayInfo().AddFormat(_REGIONAL_SECRET_TABLE)
args.GetDisplayInfo().AddTransforms(_SECRET_TRANSFORMS)
args.GetDisplayInfo().AddUriFunc(
secrets_args.MakeGetUriFunc(
'secretmanager.projects.locations.secrets', api_version=api_version
)
)
def UseSecretData(parser):
parser.display_info.AddFormat(_SECRET_DATA)
def UseVersionTable(
parser: parser_arguments.ArgumentInterceptor, api_version='v1'
):
"""Table format to display secret versions.
Args:
parser: arguments interceptor
api_version: api version to be included in resource name
"""
parser.display_info.AddFormat(_VERSION_TABLE)
parser.display_info.AddTransforms(_VERSION_STATE_TRANSFORMS)
secrets_args.MakeGetUriFunc(
'secretmanager.projects.locations.secrets.versions',
api_version=api_version,
)
def UseRegionalVersionTable(
parser: parser_arguments.ArgumentInterceptor, api_version='v1'
):
"""Table format to display regional secret versions.
Args:
parser: arguments interceptor
api_version: api version to be included in resource name
"""
parser.display_info.AddFormat(_VERSION_TABLE)
parser.display_info.AddTransforms(_VERSION_STATE_TRANSFORMS)
secrets_args.MakeGetUriFunc(
'secretmanager.projects.locations.secrets.versions',
api_version=api_version,
)
def SecretVersionTableUsingArgument(
args: parser_extensions.Namespace, api_version: str = 'v1'
):
"""Table format to display global secret version.
Args:
args: arguments interceptor
api_version: api version to be included in resource name
"""
args.GetDisplayInfo().AddFormat(_VERSION_TABLE)
args.GetDisplayInfo().AddTransforms(_VERSION_STATE_TRANSFORMS)
args.GetDisplayInfo().AddUriFunc(
secrets_args.MakeGetUriFunc(
'secretmanager.projects.secrets.versions', api_version=api_version
)
)
def RegionalSecretVersionTableUsingArgument(
args: parser_extensions.Namespace, api_version: str = 'v1'
):
"""Table format to display regional secrets.
Args:
args: arguments interceptor
api_version: api version to be included in resource name
"""
args.GetDisplayInfo().AddFormat(_VERSION_TABLE)
args.GetDisplayInfo().AddTransforms(_VERSION_STATE_TRANSFORMS)
args.GetDisplayInfo().AddUriFunc(
secrets_args.MakeGetUriFunc(
'secretmanager.projects.locations.secrets.versions',
api_version=api_version,
)
)

View File

@@ -0,0 +1,87 @@
# -*- 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.
"""Common loggers."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.core import log
class Logger(object):
"""Base for all loggers."""
def __init__(self, log_=None):
self.log = log_ or log
def Print(self, *msg):
self.log.status.Print(*msg)
class Secrets(Logger):
"""Logger for secrets."""
def _Print(self, action, secret_ref):
self.Print('{action} secret [{secret}].'.format(
action=action, secret=secret_ref.Name()))
def Created(self, secret_ref):
self._Print('Created', secret_ref)
def Deleted(self, secret_ref):
self._Print('Deleted', secret_ref)
def Updated(self, secret_ref):
self._Print('Updated', secret_ref)
def UpdatedReplication(self, secret_ref):
self._Print('Updated replication for', secret_ref)
class Versions(Logger):
"""Logger for versions."""
_SCHEDULED_DESTROY_MESSAGE = (
'Disabled version [{version}] of the secret [{secret}] and scheduled for'
' destruction at [{time}].'
)
def _Print(self, action, version_ref):
self.Print('{action} version [{version}] of the secret [{secret}].'.format(
action=action,
version=version_ref.Name(),
secret=version_ref.Parent().Name()))
def Created(self, version_ref):
self._Print('Created', version_ref)
def Destroyed(self, version_ref):
self._Print('Destroyed', version_ref)
def ScheduledDestroy(self, scheduled_destroy_time, version_ref):
self.Print(
self._SCHEDULED_DESTROY_MESSAGE.format(
version=version_ref.Name(),
secret=version_ref.Parent().Name(),
time=scheduled_destroy_time,
)
)
def Disabled(self, version_ref):
self._Print('Disabled', version_ref)
def Enabled(self, version_ref):
self._Print('Enabled', version_ref)

View File

@@ -0,0 +1,99 @@
project:
name: project
collection: secretmanager.projects
attributes:
- &project
parameter_name: projectsId
attribute_name: project
help: The project ID.
property: core/project
disable_auto_completers: false
location:
name: location
collection: secretmanager.projects.locations
attributes:
- &location
parameter_name: locationsId
attribute_name: location
help: The location of the secret.
disable_auto_completers: false
region_project:
name: project
collection: secretmanager.projects.locations
attributes:
- *location
- &region_project
parameter_name: projectsId
attribute_name: project
help: A location
disable_auto_completers: false
secret:
name: secret
collection: secretmanager.projects.secrets
attributes:
- *project
- &secret
parameter_name: secretsId
attribute_name: secret
help: The ID of the secret.
completer: googlecloudsdk.command_lib.secrets.completers:SecretsCompleter
disable_auto_completers: false
regional_secret:
name: secret
collection: secretmanager.projects.locations.secrets
attributes:
- *project
- *location
- &regional_secret
parameter_name: secretsId
attribute_name: regional_secret
help: The ID of the secret.
completer: googlecloudsdk.command_lib.secrets.completers:SecretsCompleter
disable_auto_completers: false
version:
name: version
collection: secretmanager.projects.secrets.versions
attributes:
- *project
- *secret
- &version
parameter_name: versionsId
attribute_name: version
help: The numeric version.
disable_auto_completers: false
regional_version:
name: regional_version
collection: secretmanager.projects.locations.secrets.versions
attributes:
- *project
- *location
- *secret
- &regional_version
parameter_name: versionsId
attribute_name: regional_version
help: The numeric version.
disable_auto_completers: false
multitypeResources:
secret-or-regional-secret:
name: secret-or-regional-secret
resources:
-*secret
-*regional_secret
version-or-regional_version:
name: version-or-regional_version
resources:
-*version
-*regional_version
project-or-project-with-location:
name: project-or-project-with-location
resources:
-*project
-*region_project

View File

@@ -0,0 +1,304 @@
# -*- 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.
"""Common utilities and shared helpers for secrets."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.core import yaml
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.util import files
DEFAULT_MAX_BYTES = 65536
def ReadFileOrStdin(path, max_bytes=None, is_binary=True):
"""Read data from the given file path or from stdin.
This is similar to the cloudsdk built in ReadFromFileOrStdin, except that it
limits the total size of the file and it returns None if given a None path.
This makes the API in command surfaces a bit cleaner.
Args:
path (str): path to the file on disk or "-" for stdin
max_bytes (int): maximum number of bytes
is_binary (bool): if true, data will be read as binary
Returns:
result (str): result of reading the file
"""
if not path:
return None
max_bytes = max_bytes or DEFAULT_MAX_BYTES
try:
data = console_io.ReadFromFileOrStdin(path, binary=is_binary)
if len(data) > max_bytes:
raise exceptions.BadFileException(
'The file [{path}] is larger than the maximum size of {max_bytes} '
'bytes.'.format(path=path, max_bytes=max_bytes))
return data
except files.Error as e:
raise exceptions.BadFileException(
'Failed to read file [{path}]: {e}'.format(path=path, e=e))
def WriteBinaryFile(path, content):
"""Writes the given binary contents to the file at given path.
Args:
path (str): The file path to write to.
content (str): The byte string to write.
Raises:
Error: If the file cannot be written.
"""
if not path:
return None
try:
files.WriteBinaryFileContents(path, content, private=True)
except files.Error as e:
raise exceptions.BadFileException(
'Unable to write file [{path}]: {e}'.format(path=path, e=e))
def _ParseUserManagedPolicy(user_managed_policy):
""""Reads user managed replication policy file and returns its data.
Args:
user_managed_policy (str): The json user managed message
Returns:
result (str): "user-managed"
locations (list): Locations that are part of the user-managed replication
keys (list): list of kms keys to be used for each replica.
"""
if 'replicas' not in user_managed_policy or not user_managed_policy[
'replicas']:
raise exceptions.BadFileException(
'Failed to find any replicas in user_managed policy.')
keys = []
locations = []
for replica in user_managed_policy['replicas']:
if 'location' not in replica:
raise exceptions.BadFileException(
'Failed to find a location in all replicas.')
locations.append(replica['location'])
if 'customerManagedEncryption' in replica:
if 'kmsKeyName' in replica['customerManagedEncryption']:
keys.append(replica['customerManagedEncryption']['kmsKeyName'])
else:
raise exceptions.BadFileException(
'Failed to find a kmsKeyName in customerManagedEncryption for '
'replica at least one replica.')
if keys and len(keys) != len(locations):
raise exceptions.BadFileException(
'Only some replicas have customerManagedEncryption. Please either '
'add the missing field to the remaining replicas or remove it from '
'all replicas.')
return 'user-managed', locations, keys
def _ParseAutomaticPolicy(automatic_policy):
""""Reads automatic replication policy file and returns its data.
Args:
automatic_policy (str): The json user managed message
Returns:
result (str): "automatic"
locations (list): empty list
keys (list): 0 or 1 KMS keys depending on whether the policy has CMEK
"""
if not automatic_policy:
return 'automatic', [], []
if 'customerManagedEncryption' not in automatic_policy:
raise exceptions.BadFileException(
'Failed to parse replication policy. Expected automatic to contain '
'either nothing or customerManagedEncryption.')
cmek = automatic_policy['customerManagedEncryption']
if 'kmsKeyName' not in cmek:
raise exceptions.BadFileException(
'Failed to find a kmsKeyName in customerManagedEncryption.')
return 'automatic', [], [cmek['kmsKeyName']]
def _ParseReplicationDict(replication_policy):
"""Reads replication policy dictionary and returns its data."""
if 'userManaged' in replication_policy:
return _ParseUserManagedPolicy(replication_policy['userManaged'])
if 'automatic' in replication_policy:
return _ParseAutomaticPolicy(replication_policy['automatic'])
raise exceptions.BadFileException(
'Expected to find either "userManaged" or "automatic" in replication, '
'but found neither.')
def ParseReplicationFileContents(file_contents):
"""Reads replication policy file contents and returns its data.
Reads the contents of a json or yaml replication policy file which conforms to
https://cloud.google.com/secret-manager/docs/reference/rest/v1/projects.secrets#replication
and returns data needed to create a Secret with that policy. If the file
doesn't conform to the expected format, a BadFileException is raised.
For Secrets with an automtic policy, locations is empty and keys has
either 0 or 1 entry depending on whether the policy includes CMEK. For Secrets
with a user managed policy, the number of keys returns is either 0 or is equal
to the number of locations returned with the Nth key corresponding to the Nth
location.
Args:
file_contents (str): The unvalidated contents fo the replication file.
Returns:
result (str): Either "user-managed" or "automatic".
locations (list): Locations that are part of the user-managed replication
keys (list): list of kms keys to be used for each replica.
"""
try:
replication_policy = json.loads(file_contents)
return _ParseReplicationDict(replication_policy)
except ValueError:
# Assume that this is yaml.
pass
try:
replication_policy = yaml.load(file_contents)
return _ParseReplicationDict(replication_policy)
except yaml.YAMLParseError:
raise exceptions.BadFileException(
'Failed to parse replication policy file as json or yaml.')
def ApplyTopicsUpdate(args, original_topics):
"""Applies updates to the list of topics on a secret.
Preserves the original order of topics.
Args:
args (argparse.Namespace): The collection of user-provided arguments.
original_topics (list): Topics configured on the secret prior to update.
Returns:
result (list): List of strings of topic names after update.
"""
if args.IsSpecified('clear_topics'):
return []
topics_set = set()
for topic in original_topics:
topics_set.add(topic.name)
if args.IsSpecified('remove_topics'):
for topic_name in args.remove_topics:
topics_set.discard(topic_name)
new_topics = []
for topic in original_topics:
if topic.name in topics_set:
new_topics.append(topic.name)
return new_topics
if args.IsSpecified('add_topics'):
new_topics = []
for topic in original_topics:
new_topics.append(topic.name)
for topic_name in args.add_topics:
if topic_name not in topics_set:
new_topics.append(topic_name)
return new_topics
def ApplyAliasUpdate(args, original_version_aliases):
"""Applies updates to the list of version-aliases on a secret.
Makes no alterations to the original version aliases
Args:
args (argparse.Namespace): The collection of user-provided arguments.
original_version_aliases (list): version-aliases configured on the secret
prior to update.
Returns:
result (dict): dict of version_aliases pairs after update.
"""
if args.IsSpecified('clear_version_aliases'):
return {}
version_aliases_dict = dict()
version_aliases_dict.update(
{pair.key: pair.value for pair in original_version_aliases})
if args.IsSpecified('remove_version_aliases'):
for alias in args.remove_version_aliases:
del version_aliases_dict[alias]
new_version_aliases = dict()
for version_alias_pair in original_version_aliases:
if version_alias_pair.key in version_aliases_dict:
new_version_aliases[version_alias_pair.key] = version_alias_pair.value
return new_version_aliases
elif args.IsSpecified('update_version_aliases'):
new_version_aliases = dict()
version_aliases_dict.update(
{pair.key: pair.value for pair in original_version_aliases})
new_version_aliases.update({
alias: version
for (alias, version) in args.update_version_aliases.items()
})
return new_version_aliases
def ApplyAnnotationsUpdate(args, original_annotations):
"""Applies updates to the list of annotations on a secret.
Makes no alterations to the original annotations
Args:
args (argparse.Namespace): The collection of user-provided arguments.
original_annotations (list): annotations configured on the secret prior to
update.
Returns:
result (dict): dict of annotations pairs after update.
"""
if args.IsSpecified('clear_annotations'):
return {}
annotations_dict = dict()
annotations_dict.update(
{pair.key: pair.value for pair in original_annotations})
if args.IsSpecified('remove_annotations'):
for annotation in args.remove_annotations:
del annotations_dict[annotation]
new_annotations = dict()
for annotation_pair in original_annotations:
if annotation_pair.key in annotations_dict:
new_annotations[annotation_pair.key] = annotation_pair.value
return new_annotations
elif args.IsSpecified('update_annotations'):
new_annotations = dict()
annotations_dict.update(
{pair.key: pair.value for pair in original_annotations})
new_annotations.update({
annotation: metadata
for (annotation, metadata) in args.update_annotations.items()
})
return new_annotations