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,31 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The super-group for the meta commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.UniverseCompatible
@base.Hidden
@base.ReleaseTracks(base.ReleaseTrack.GA)
class Meta(base.Group):
"""Cloud meta introspection commands."""
category = base.SDK_TOOLS_CATEGORY

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The super-group for commands to inspect APIs in gcloud."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
class APIs(base.Group):
"""Inspect the APIs registered in gcloud."""

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The super-group for commands to inspect APIs in gcloud."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
class Collections(base.Group):
"""Inspect the API collections registered in gcloud."""

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that describes a resource collection for a given API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta.apis import flags
from googlecloudsdk.command_lib.util.apis import registry
class Describe(base.DescribeCommand):
"""Describe the details of a collection for an API."""
@staticmethod
def Args(parser):
flags.API_VERSION_FLAG.AddToParser(parser)
parser.add_argument(
'collection',
completer=flags.CollectionCompleter,
help='The name of the collection to get the details of.')
def Run(self, args):
return registry.GetAPICollection(args.collection,
api_version=args.api_version)

View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that lists the resource collections for a given API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.meta.apis import flags
from googlecloudsdk.command_lib.util.apis import registry
class Lint(base.ListCommand):
"""Show which collections have non-compliant list API methods."""
@staticmethod
def Args(parser):
base.PAGE_SIZE_FLAG.RemoveFromParser(parser)
base.URI_FLAG.RemoveFromParser(parser)
parser.add_argument(
'--api',
completer=flags.APICompleter,
help='The name of the API to get the collections for.')
flags.API_VERSION_FLAG.AddToParser(parser)
parser.display_info.AddFormat("""\
table(
collection:sort=6,
has_list:sort=1,
resource_arg:sort=2,
flattened:sort=3,
pageable:sort=4,
page_size:sort=5)
""")
def Run(self, args):
if args.api_version and not args.api:
raise exceptions.RequiredArgumentException(
'--api',
'The --api-version flag can only be specified when using the --api '
'flag.')
collections = registry.GetAPICollections(api_name=args.api,
api_version=args.api_version)
results = []
for c in collections:
methods = registry.GetMethods(c.full_name, api_version=c.api_version)
if not methods:
# Synthetic collection
continue
list_methods = [m for m in methods if m.IsList()]
if list_methods:
method = list_methods[0]
results.append({'collection': c.full_name,
'has_list': True,
'resource_arg': bool(method.request_collection),
'flattened': bool(method.ListItemField()),
'pageable': method.HasTokenizedRequest(),
'page_size': bool(method.BatchPageSizeField())})
else:
results.append({'collection': c.full_name, 'has_list': False})
# Filter out anything that is fully within spec.
results = [r for r in results if not (r['has_list'] and
r['resource_arg'] and
r['flattened'] and
r['pageable'] and
r['page_size'])]
return results

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that lists the resource collections for a given API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.meta.apis import flags
from googlecloudsdk.command_lib.util.apis import registry
class List(base.ListCommand):
"""List the resource collections for an API."""
@staticmethod
def Args(parser):
base.PAGE_SIZE_FLAG.RemoveFromParser(parser)
base.URI_FLAG.RemoveFromParser(parser)
parser.add_argument(
'--api',
completer=flags.APICompleter,
help='The name of the API to get the collections for.')
flags.API_VERSION_FLAG.AddToParser(parser)
parser.display_info.AddFormat("""
table(
full_name:sort=1:label=COLLECTION_NAME,
detailed_path
)
""")
def Run(self, args):
if args.api_version and not args.api:
raise exceptions.RequiredArgumentException(
'--api',
'The --api-version flag can only be specified when using the --api '
'flag.')
return registry.GetAPICollections(api_name=args.api,
api_version=args.api_version)

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that describes a registered gcloud API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta.apis import flags
from googlecloudsdk.command_lib.util.apis import registry
class Describe(base.DescribeCommand):
"""Describe the details of an API registered in gcloud."""
@staticmethod
def Args(parser):
flags.API_VERSION_FLAG.AddToParser(parser)
parser.add_argument(
'api_name',
completer=flags.APICompleter,
help='The name of the API to show the details of.')
def Run(self, args):
return registry.GetAPI(args.api_name, api_version=args.api_version)

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The super-group for commands to retrieve APIs definitions."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
class Discovery(base.Group):
"""Inspect the Google Cloud Platform APIs through the discovery service."""

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that describes a API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
class Describe(base.DescribeCommand):
"""Describe the details of an API in discovery service."""
@staticmethod
def Args(parser):
parser.add_argument(
'api',
help='The api_name/api_version to show the details of.')
parser.display_info.AddFormat('json')
def Run(self, args):
client = apis.GetClientInstance('discovery', 'v1')
messages = client.MESSAGES_MODULE
api_name, api_version = args.api.split('/')
request = messages.DiscoveryApisGetRestRequest(
api=api_name, version=api_version)
return client.apis.GetRest(request)

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that lists the registered APIs in gcloud.."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
class List(base.ListCommand):
"""List the APIs available via discovery service."""
@staticmethod
def Args(parser):
base.PAGE_SIZE_FLAG.RemoveFromParser(parser)
parser.display_info.AddUriFunc(lambda x: x.discoveryRestUrl)
parser.display_info.AddFormat("""
table(
name:sort=1,
version:sort=2,
title,
preferred.yesno(yes='*', no=''),
labels.list()
)""")
def Run(self, args):
client = apis.GetClientInstance('discovery', 'v1')
messages = client.MESSAGES_MODULE
request = messages.DiscoveryApisListRequest()
# Cannot use list_pager here since this api method
# does not support page tokens.
return client.apis.List(request).items

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that lists the registered APIs in gcloud.."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.util.apis import registry
class List(base.ListCommand):
"""List the APIs registered in gcloud."""
@staticmethod
def Args(parser):
base.PAGE_SIZE_FLAG.RemoveFromParser(parser)
base.URI_FLAG.RemoveFromParser(parser)
parser.display_info.AddFormat("""
table(
name:sort=1,
version:sort=2,
is_default.yesno(yes='*', no=''),
base_url
)
""")
def Run(self, args):
return registry.GetAllAPIs()

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The group for commands to inspect API messages in gcloud."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
class Messages(base.Group):
"""Inspect the API messages in an API."""

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that describes a message from a given API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.meta.apis import flags
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.command_lib.util.apis import registry
class Describe(base.DescribeCommand):
"""Describe the details of a proto message in an API."""
@staticmethod
def Args(parser):
flags.API_REQUIRED_FLAG.AddToParser(parser)
flags.API_VERSION_FLAG.AddToParser(parser)
parser.add_argument(
'message',
help='The name of the message you want to describe.')
def Run(self, args):
api = registry.GetAPI(args.api, api_version=args.api_version)
try:
message = getattr(api.GetMessagesModule(), args.message)
return arg_utils.GetRecursiveMessageSpec(message)
except AttributeError:
raise exceptions.InvalidArgumentException(
'message', 'Message [{}] does not exist for API [{}]'.format(
args.message, args.api))

View File

@@ -0,0 +1,89 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that generates YAML export schemas for a message in a given API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.meta.apis import flags
from googlecloudsdk.command_lib.util.apis import arg_utils
from googlecloudsdk.command_lib.util.apis import export
from googlecloudsdk.command_lib.util.apis import registry
class GenerateExportSchemas(base.SilentCommand):
"""Generate YAML export schemas for a message in a given API.
*gcloud* commands that have "too many" *create*/*update* command flags may
also provide *export*/*import* commands. *export* lists the current state
of a resource in a YAML *export* format. *import* reads export format data
and either creates a new resource or updates an existing resource.
An export format is an abstract YAML representation of the mutable fields of a
populated protobuf message. Abstraction allows the export format to hide
implementation details of some protobuf constructs like enums and
`additionalProperties`.
One way of describing an export format is with JSON schemas. A schema
documents export format properties for a message in an API, and can also be
used to validate data on import. Validation is important because users can
modify export data before importing it again.
This command generates [JSON schemas](json-schema.org) (in YAML format, go
figure) for a protobuf message in an API. A separate schema files is
generated for each nested message in the resource message.
## CAVEATS
The generated schemas depend on the quality of the protobuf discovery
docs, including proto file comment conventions that are not error checked.
Always manually inspect schemas before using them in a release.
## EXAMPLES
To generate the WorkflowTemplate schemas in the current directory from the
dataproc v1 API:
$ {command} WorkflowTemplate --api=dataproc --api-version=v1
"""
@staticmethod
def Args(parser):
flags.API_REQUIRED_FLAG.AddToParser(parser)
flags.API_VERSION_FLAG.AddToParser(parser)
parser.add_argument(
'message',
help='The name of the message to generate the YAML export schemas for.')
parser.add_argument(
'--directory',
help=('The path name of the directory to create the YAML export '
'schema files in. If not specified then the files are created in '
'the current directory.'))
def Run(self, args):
api = registry.GetAPI(args.api, api_version=args.api_version)
try:
message = getattr(api.GetMessagesModule(), args.message)
except AttributeError:
raise exceptions.InvalidArgumentException(
'message', 'Message [{}] does not exist for API [{} {}]'.format(
args.message, args.api, api.version))
message_spec = arg_utils.GetRecursiveMessageSpec(message)
export.GenerateExportSchemas(
api, args.message, message_spec, args.directory)

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that lists the resource collections for a given API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.protorpclite import messages as _messages
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta.apis import flags
from googlecloudsdk.command_lib.util.apis import registry
class List(base.ListCommand):
"""List the proto messages for an API."""
@staticmethod
def Args(parser):
base.PAGE_SIZE_FLAG.RemoveFromParser(parser)
base.URI_FLAG.RemoveFromParser(parser)
flags.API_REQUIRED_FLAG.AddToParser(parser)
flags.API_VERSION_FLAG.AddToParser(parser)
parser.display_info.AddFormat('table(name)')
def Run(self, args):
api = registry.GetAPI(args.api, api_version=args.api_version)
messages_module = api.GetMessagesModule()
messages = [
m for m in messages_module.__dict__.values()
if issubclass(type(m), type) and issubclass(m, _messages.Message)]
return [{'name': m.__name__} for m in messages]

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The super-group for commands to inspect APIs in gcloud."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
class Methods(base.Group):
"""Inspect the methods for an API collection registered in gcloud."""

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that describes a registered gcloud API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta.apis import flags
from googlecloudsdk.core import properties
ENFORCE_COLLECTION_FLAG = base.Argument(
'--enforce-collection',
action='store_true',
default=True,
help='Fail unless resource belongs to specified collection. '
'This is applicable only if method being called is GET or DELETE '
'and resource identifier is URL.')
class Call(base.Command):
"""Calls an API method with specific parameters."""
@staticmethod
def Args(parser):
flags.API_VERSION_FLAG.AddToParser(parser)
flags.COLLECTION_FLAG.AddToParser(parser)
ENFORCE_COLLECTION_FLAG.AddToParser(parser)
flags.RAW_FLAG.AddToParser(parser)
parser.AddDynamicPositional(
'method',
action=flags.MethodDynamicPositionalAction,
help='The name of the API method to invoke.')
def Run(self, args):
properties.VALUES.core.enable_gri.Set(True)
response = args.method.Call()
return response

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that describes a resource collection for a given API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta.apis import flags
from googlecloudsdk.command_lib.util.apis import registry
class Describe(base.DescribeCommand):
"""Describe the details of a collection for an API."""
@staticmethod
def Args(parser):
flags.API_VERSION_FLAG.AddToParser(parser)
flags.COLLECTION_FLAG.AddToParser(parser)
parser.add_argument(
'method',
completer=flags.MethodCompleter,
help='The name of the method to get the details of.')
def Run(self, args):
return registry.GetMethod(args.collection, args.method,
api_version=args.api_version)

View File

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that lists the resource collections for a given API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import itertools
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta.apis import flags
from googlecloudsdk.command_lib.util.apis import registry
class List(base.ListCommand):
"""List the methods of a resource collection for an API."""
@staticmethod
def Args(parser):
base.PAGE_SIZE_FLAG.RemoveFromParser(parser)
base.URI_FLAG.RemoveFromParser(parser)
collection_flag = base.Argument(
'--collection',
completer=flags.CollectionCompleter,
help='The name of the collection for which to list methods.\n'
'If left blank, returns methods from all collections.')
collection_flag.AddToParser(parser)
flags.API_VERSION_FLAG.AddToParser(parser)
api_flag = base.Argument(
'--api',
completer=flags.APICompleter,
help=('The name of the API to get the methods for. If `--api-version` '
'is also supplied, then returns methods from specified version, '
'otherwise returns methods from all versions of this API.'))
api_flag.AddToParser(parser)
parser.display_info.AddFormat("""
table(
name:sort=1,
detailed_path:optional,
http_method,
request_type,
response_type
)
""")
def Run(self, args):
if not args.collection:
if args.api:
collections = [registry.GetAPICollections(args.api, args.api_version)]
else:
collections = [
registry.GetAPICollections(api.name, api.version)
for api in registry.GetAllAPIs()
]
collections = list(itertools.chain.from_iterable(collections))
methods = [registry.GetMethods(collection.full_name,
api_version=collection.api_version)
for collection in collections]
methods = list(itertools.chain.from_iterable(methods))
return methods
return registry.GetMethods(args.collection, api_version=args.api_version)

View File

@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that regenerates existing or new gcloud API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
import fnmatch
import os
import re
import shutil
import googlecloudsdk
from googlecloudsdk import third_party
from googlecloudsdk.api_lib.regen import generate
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import parser_errors
from googlecloudsdk.command_lib.meta import regen as regen_utils
from googlecloudsdk.core import log
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import files
import ruamel.yaml
import six
from six.moves import map
_API_REGEX = '([a-z0-9_]+)/([a-z0-9_]+)'
@base.UniverseCompatible
class Regen(base.Command):
"""Regenerate given API(s) in gcloud."""
@staticmethod
def Args(parser):
parser.add_argument(
'api',
type=arg_parsers.ArgList(),
help='The APIs to regenerate in api_name/api_version format. '
'These can be filename glob expressions to regenerate multiple '
'apis. For example */* to regegenerate all apis and versions, '
'or */*beta* to only regenerate existing beta apis. '
'Note that if discovery doc is supplied this cannot '
'contain any wildcards.')
parser.add_argument(
'--api-discovery-doc',
help='Path to json file describing the api. If not specified, '
'an existing discovery doc will be used.')
parser.add_argument('--config',
help='Regeneration config yaml filename. '
'If not specified canonical config will be used.')
parser.add_argument(
'--base-dir',
help='Directory where generated code will be written. '
'By default googlecloudsdk/generated_clients/apis.')
def Run(self, args):
config = _LoadConfig(args.config)
root_dir = config['root_dir']
changed_config = False
if args.api_discovery_doc:
if not os.path.isfile(args.api_discovery_doc):
raise regen_utils.DiscoveryDocError(
'File not found {}'.format(args.api_discovery_doc))
if len(args.api) != 1:
raise parser_errors.ArgumentError(
'Can only specify one api when discovery doc is provided.')
match = re.match(_API_REGEX, args.api[0])
if not match:
raise regen_utils.DiscoveryDocError(
'Api name must match {} pattern when discovery doc '
'is specified'.format(_API_REGEX))
api_name, api_version = match.group(1), match.group(2)
if api_name not in config['apis']:
log.warning('No such api %s in config, adding...', api_name)
config['apis'][api_name] = {api_version: {'discovery_doc': ''}}
changed_config = True
elif api_version not in config['apis'][api_name]:
log.warning('No such api version %s in config, adding...', api_version)
config['apis'][api_name][api_version] = {'discovery_doc': ''}
changed_config = True
api_version_config = config['apis'].get(api_name).get(api_version, {})
discovery_doc = api_name + '_' + api_version + '.json'
new_discovery_doc = os.path.realpath(args.api_discovery_doc)
old_discovery_doc = os.path.realpath(
os.path.join(args.base_dir, root_dir, discovery_doc))
if new_discovery_doc != old_discovery_doc:
log.status.Print('Copying in {}'.format(new_discovery_doc))
shutil.copyfile(new_discovery_doc, old_discovery_doc)
if api_version_config['discovery_doc'] != discovery_doc:
changed_config = True
api_version_config['discovery_doc'] = discovery_doc
regenerate_list = [
(match.group(1), match.group(2), api_version_config)
]
else:
regex_patern = '|'.join(map(fnmatch.translate, args.api))
regenerate_list = [
(api_name, api_version, api_config)
for api_name, api_version_config in six.iteritems(config['apis'])
for api_version, api_config in six.iteritems(api_version_config)
if re.match(regex_patern, api_name + '/' + api_version)
]
if not regenerate_list:
raise regen_utils.UnknownApi(
'api [{api_name}] not found in "apis" section of '
'{config_file}. Use [gcloud meta apis list] to see available apis.'
.format(api_name=','.join(args.api),
config_file=args.config))
base_dir = args.base_dir or os.path.dirname(
os.path.dirname(googlecloudsdk.__file__))
for api_name, api_version, api_config in sorted(regenerate_list):
log.status.Print(
'Generating {} {} from {}'
.format(api_name,
api_version,
os.path.join(root_dir, api_config['discovery_doc'])))
discovery_doc = os.path.join(
base_dir, root_dir, api_config['discovery_doc'])
output_dir = os.path.join(base_dir, root_dir)
root_package = root_dir.replace('/', '.')
generate.GenerateApitoolsApi(
discovery_doc, output_dir, root_package, api_name, api_version,
api_config)
generate.GenerateApitoolsResourceModule(
discovery_doc,
output_dir,
api_name,
api_version,
api_config.get('resources', {}),
)
client_output_dir = os.path.join(base_dir, root_dir)
apis_config_map = collections.defaultdict(dict)
for api_name, versions_config in config['apis'].items():
for version, version_config in versions_config.items():
apis_config_map[api_name][version] = generate.ApiConfig.FromData(
data=version_config,
root_dir=root_dir,
discovery_doc_dir=client_output_dir,
)
generate.GenerateApiMap(
os.path.join(client_output_dir, 'apis_map.py'), apis_config_map)
# Now that everything passed, config can be updated if needed.
if changed_config:
log.warning('Updated %s', args.config)
with files.FileWriter(args.config) as stream:
ruamel.yaml.round_trip_dump(config, stream)
def _LoadConfig(config_file_name=None):
"""Loads regen config from given filename."""
config_file_name = config_file_name or os.path.join(
os.path.dirname(encoding.Decode(third_party.__file__)),
'regen_apis_config.yaml')
if not os.path.isfile(config_file_name):
raise regen_utils.ConfigFileError('{} Not found'.format(config_file_name))
with files.FileReader(config_file_name) as stream:
config = ruamel.yaml.round_trip_load(stream)
if not config or 'root_dir' not in config:
raise regen_utils.ConfigFileError(
'{} does not have format of gcloud api config file'
.format(config_file_name))
return config

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The meta cache command group."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.Hidden
@base.ReleaseTracks(base.ReleaseTrack.GA)
class Cache(base.Group):
"""Cloud SDK persistent cache commands."""

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The meta cache completers command group."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.Hidden
@base.ReleaseTracks(base.ReleaseTrack.GA)
class Completers(base.Group):
"""Cloud SDK resource completer cache commands."""

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The meta cache completers list command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta import cache_util
from googlecloudsdk.core.console import progress_tracker
class List(base.ListCommand):
"""List all Cloud SDK command argument completer objects.
Cloud SDK command argument completers are objects that have a module path,
collection name and API version. The module path may be used as the
_MODULE_PATH_ argument to the $ gcloud meta cache completers run command.
"""
@staticmethod
def Args(parser):
parser.display_info.AddFormat("""\
table[box](module_path,
type,
collection,
api_version,
attachments:format="table[box](
command:sort=1,
arguments.list())")
""")
def Run(self, args):
if not args.IsSpecified('sort_by'):
args.sort_by = ['module_path', 'collection', 'api_version']
with progress_tracker.ProgressTracker(
'Collecting attached completers from all command flags and arguments'):
return cache_util.ListAttachedCompleters(self._cli_power_users_only)

View File

@@ -0,0 +1,287 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The meta cache completers run command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import sys
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import parser_extensions
from googlecloudsdk.command_lib.meta import cache_util
from googlecloudsdk.command_lib.util import parameter_info_lib
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.command_lib.util.concepts import presentation_specs
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import module_util
from googlecloudsdk.core.console import console_io
import six
class _FunctionCompleter(object):
"""Convert an argparse function completer to a resource_cache completer."""
def __init__(self, completer):
self._completer = completer
self.parameters = None
def ParameterInfo(self, parsed_args, argument):
del argument
return parsed_args
def Complete(self, prefix, parameter_info):
return self._completer(prefix, parsed_args=parameter_info)
def _GetPresentationSpec(resource_spec_path, **kwargs):
"""Build a presentation spec."""
resource_spec = module_util.ImportModule(resource_spec_path)
if callable(resource_spec):
resource_spec = resource_spec()
flag_name_overrides = kwargs.pop('flag_name_overrides', '')
flag_name_overrides = {
o.split(':')[0]: o.split(':')[1] if ':' in o else ''
for o in flag_name_overrides.split(';')
if o}
prefixes = kwargs.pop('prefixes', False)
return presentation_specs.ResourcePresentationSpec(
kwargs.pop('name', resource_spec.name),
resource_spec,
'help text',
flag_name_overrides=flag_name_overrides,
prefixes=prefixes,
**kwargs)
def _GetCompleter(module_path, cache=None, qualify=None,
resource_spec=None, presentation_kwargs=None, attribute=None,
**kwargs):
"""Returns an instantiated completer for module_path."""
presentation_kwargs = presentation_kwargs or {}
if resource_spec:
presentation_spec = _GetPresentationSpec(resource_spec,
**presentation_kwargs)
completer = module_util.ImportModule(module_path)(
presentation_spec.concept_spec,
attribute)
else:
completer = module_util.ImportModule(module_path)
if not isinstance(completer, type):
return _FunctionCompleter(completer)
try:
return completer(
cache=cache,
qualified_parameter_names=qualify,
**kwargs)
except TypeError:
return _FunctionCompleter(completer())
class AddCompleterResourceFlags(parser_extensions.DynamicPositionalAction):
"""Adds resource argument flags based on the completer."""
def __init__(self, *args, **kwargs):
super(AddCompleterResourceFlags, self).__init__(*args, **kwargs)
self.__argument = None
self.__completer = None
def GenerateArgs(self, namespace, module_path):
args = []
presentation_kwargs = namespace.resource_presentation_kwargs or {}
# Add the args that correspond to the resource arg, but make them
# non-required.
if namespace.resource_spec_path:
spec = _GetPresentationSpec(namespace.resource_spec_path,
**presentation_kwargs)
info = concept_parsers.ConceptParser([spec]).GetInfo(spec.name)
for arg in info.GetAttributeArgs():
if arg.name.startswith('--'):
arg.kwargs['required'] = False
else:
arg.kwargs['nargs'] = '?' if not spec.plural else '*'
args.append(arg)
kwargs = namespace.kwargs or {}
self.__completer = _GetCompleter(
module_path, qualify=namespace.qualify,
resource_spec=namespace.resource_spec_path,
presentation_kwargs=presentation_kwargs,
attribute=namespace.attribute,
**kwargs)
if self.__completer.parameters:
for parameter in self.__completer.parameters:
dest = parameter_info_lib.GetDestFromParam(parameter.name)
if hasattr(namespace, dest):
# Don't add if its already been added.
continue
flag = parameter_info_lib.GetFlagFromDest(dest)
arg = base.Argument(
flag,
dest=dest,
category='RESOURCE COMPLETER',
help='{} `{}` parameter value.'.format(
self.__completer.__class__.__name__, parameter.name))
args.append(arg)
self.__argument = base.Argument(
'resource_to_complete',
nargs='?',
help=('The partial resource name to complete. Omit to enter an '
'interactive loop that reads a partial resource name from the '
'input and lists the possible prefix matches on the output '
'or displays an ERROR message.'))
args.append(self.__argument)
return args
def Completions(self, prefix, parsed_args, **kwargs):
parameter_info = self.__completer.ParameterInfo(
parsed_args, self.__argument)
return self.__completer.Complete(prefix, parameter_info)
class Run(base.Command):
"""Cloud SDK completer module tester.
*{command}* is an ideal way to debug completer modules without interference
from the shell. Shells typically ignore completer errors by disabling all
standard output, standard error and exception messaging. Specify
`--verbosity=INFO` to enable completion and resource cache tracing.
"""
@staticmethod
def Args(parser):
# Add a concept handler that will be stuffed dynamically with information.
concept_parsers.ConceptParser([]).AddToParser(parser)
parser.add_argument(
'--resource-spec-path',
help=('The resource spec path for a resource argument auto-generated '
'completer.'))
parser.add_argument(
'--attribute',
help=('The name of the resource attribute for a resource argument '
'auto-generated completer.'))
parser.add_argument(
'--resource-presentation-kwargs',
type=arg_parsers.ArgDict(
spec={
'name': str,
'flag_name_overrides': str,
'plural': bool,
'prefixes': bool,
'required': bool}),
help=('Dict of kwargs to be passed to the presentation spec for the '
'resource argument for which a completer is being tested, such '
'as name, prefixes, plural, flag name overrides (format as a '
'list of semicolon-separated key:value pairs). Prefixes is False '
'by default. Name is the resource spec name by default.'))
cache_util.AddCacheFlag(parser)
parser.add_argument(
'--qualify',
metavar='NAME',
type=arg_parsers.ArgList(),
help=('A list of resource parameter names that must always be '
'qualified. This is a manual setting for testing. The CLI sets '
'this automatically.'))
parser.add_argument(
'--kwargs',
metavar='NAME=VALUE',
type=arg_parsers.ArgDict(),
help=('Keyword arg dict passed to the completer constructor. For '
'example, use this to set the resource collection and '
'list command for `DeprecatedListCommandCompleter`:\n\n'
' --kwargs=collection=...,foo="..."'))
parser.add_argument(
'--stack-trace',
action='store_true',
default=True,
help=('Enable all exception stack traces, including Cloud SDK core '
'exceptions.'))
parser.AddDynamicPositional(
'module_path',
action=AddCompleterResourceFlags,
help=('The completer module path. Run $ gcloud meta completers list` '
'to list the module paths of the available completers. A '
'completer module may declare additional flags. Specify `--help` '
'after _MODULE_PATH_ for details on the module specific flags.'
'\n\nNOTE: To test resource argument completers, use the '
'module path "googlecloudsdk.command_lib.util.completers:'
'CompleterForAttribute". The flags `--resource-spec-path`, '
'`--attribute`, and (if desired) `--resource-presentation-'
'kwargs` must be provided BEFORE the positional. Unlike with '
'most gcloud commands, the arguments are generated on the fly '
'using the completer you provide, so all the information to '
'create a resource completer needs to be provided up-front. For '
'example:\n\n $ {command} --resource-spec-path MODULE_PATH:'
'SPEC_OBJECT --attribute ATTRIBUTE_NAME --resource-presentation-'
'kwargs flag_name_overrides=ATTRIBUTE1:FLAG1;ATTRIBUTE2:FLAG2 '
'googlecloudsdk.command_lib.util.completers:CompleterForAttribute'
))
def Run(self, args):
"""Returns the results for one completion."""
presentation_kwargs = args.resource_presentation_kwargs or {}
with cache_util.GetCache(args.cache, create=True) as cache:
log.info('cache name {}'.format(cache.name))
if not args.kwargs:
args.kwargs = {}
# Create the ResourceInfo object that is used to hook up the parameter
# info to the argparse namespace for resource argument completers.
if args.resource_spec_path:
spec = _GetPresentationSpec(
args.resource_spec_path,
**presentation_kwargs)
spec.required = False
resource_info = concept_parsers.ConceptParser([spec]).GetInfo(spec.name)
# Since the argument being completed doesn't have the correct
# dest, make sure the handler always gives the same ResourceInfo
# object.
def ResourceInfoMonkeyPatch(*args, **kwargs):
del args, kwargs
return resource_info
args.CONCEPTS.ArgNameToConceptInfo = ResourceInfoMonkeyPatch
completer = _GetCompleter(
args.module_path, cache=cache, qualify=args.qualify,
resource_spec=args.resource_spec_path,
presentation_kwargs=presentation_kwargs,
attribute=args.attribute,
**args.kwargs)
parameter_info = completer.ParameterInfo(
args, args.GetPositionalArgument('resource_to_complete'))
if args.resource_to_complete is not None:
matches = completer.Complete(args.resource_to_complete, parameter_info)
return [matches]
while True:
name = console_io.PromptResponse('COMPLETE> ')
if name is None:
break
try:
completions = completer.Complete(name, parameter_info)
except (Exception, SystemExit) as e: # pylint: disable=broad-except
if args.stack_trace:
exceptions.reraise(Exception(e))
else:
log.error(six.text_type(e))
continue
if completions:
print('\n'.join(completions))
sys.stderr.write('\n')
return None

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The meta cache delete command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta import cache_util
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
class Delete(base.Command):
"""Delete a persistent cache or tables in the cache."""
@staticmethod
def Args(parser):
cache_util.AddCacheFlag(parser)
parser.add_argument(
'tables',
nargs='*',
help=('The table names or name patterns to delete, where `?` matches '
'any character and ```*``` matches any string of zero or more '
'characters. If omitted then the entired cache is deleted.'))
def Run(self, args):
def _RequireConfirmation(name):
"""Prompt for cache deletion and return confirmation."""
console_io.PromptContinue(
message='The entire [{}] cache will be deleted.'.format(name),
cancel_on_no=True,
default=True)
if not args.tables and not args.IsSpecified('cache'):
_RequireConfirmation(args.cache)
cache_util.Delete()
return None
with cache_util.GetCache(args.cache) as cache:
log.info('cache name {}'.format(cache.name))
if args.tables:
names = [name for pattern in args.tables
for name in cache.Select(pattern)]
if not names:
raise cache_util.NoTablesMatched('No tables matched [{}].'.format(
','.join(args.tables)))
console_io.PromptContinue(
message='[{}] will be deleted.'.format(','.join(names)),
default=True,
cancel_on_no=True)
for name in names:
table = cache.Table(name)
table.Delete()
return None
_RequireConfirmation(cache.name)
cache.Delete()
return None

View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The meta cache list command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta import cache_util
from googlecloudsdk.core import log
from googlecloudsdk.core.cache import exceptions as cache_exceptions
class List(base.ListCommand):
"""List the tables or table contents in a persistent cache."""
@staticmethod
def Args(parser):
cache_util.AddCacheFlag(parser)
parser.add_argument(
'tables',
nargs='*',
help=('The table names or name patterns to list, where `?` matches any '
'character and ```*``` matches any string of zero or more '
'characters. If omitted then a table of all tables is '
'displayed.'))
def Run(self, args):
with cache_util.GetCache(args.cache) as cache:
log.info('cache name {}'.format(cache.name))
if args.tables:
names = [name for pattern in args.tables
for name in cache.Select(pattern)]
if not names:
raise cache_util.NoTablesMatched('No tables matched [{}].'.format(
','.join(args.tables)))
if not args.IsSpecified('format'):
args.format = 'json'
results = []
for name in names:
try:
table = cache.Table(name, create=False)
results.append({
'name': table.name,
'data': table.Select(ignore_expiration=False)
})
except cache_exceptions.Error as e:
log.warning(e)
return results
if not args.IsSpecified('format'):
args.format = ('table[box](name, columns:label=COL, keys:label=KEY, '
'timeout, is_expired:label=EXPIRED)')
names = cache.Select()
return [cache.Table(name=name, create=False) for name in sorted(names)]

View File

@@ -0,0 +1,49 @@
# -*- 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.
"""The `gcloud meta check-import` command."""
from googlecloudsdk.calliope import base
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import module_util
@base.UniverseCompatible
@base.Hidden
class CheckImport(base.Command):
"""Check if modules are importable."""
@staticmethod
def Args(parser):
parser.add_argument(
'modules',
metavar='MODULES',
nargs='*',
help='The list of modules to check, separated with spaces',
)
def Run(self, args):
if not args.modules:
raise exceptions.Error('No modules to check.')
errors = []
for module in args.modules:
try:
module_util.ImportModule(module)
log.status.Print(f'Module [{module}] imported successfully.')
except Exception as e: # pylint: disable=broad-except
errors.append(f'Error importing [{module}]: {e}')
if errors:
raise exceptions.MultiError(errors)

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Lists the installed gcloud interactive CLI trees."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import cli_tree
class CliTrees(base.Group):
"""CLI trees manager.
The *{command}* group generates, updates and lists static CLI trees.
A CLI tree is a module or JSON file that describes a command and its
subcommands, flags, arguments, help text and TAB completers.
*$ gcloud interactive* uses CLI trees for typeahead, command line completion,
and as-you-type documentation, *$ gcloud* uses its CLI tree for static
completion, and *$ gcloud search help* uses the gcloud CLI tree to search
help text.
Packaged CLI tree files are installed in the *cli/data* subdirectory of the
*gcloud* installation root directory. These trees are updated by
*$ gcloud components install* and *$ gcloud components update*. Other CLI
trees are generated on demand and cached in the per project *cli* config
directory. Each CLI tree is version-stamped to its command version and
is updated when the command changes.
"""

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Lists the installed gcloud interactive CLI trees."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta import list_cli_trees
class List(base.Command):
"""List the installed gcloud interactive CLI trees.
This command lists all CLI trees found in the Cloud SDK installation and
config directories. Duplicates may be listed; commands that load the trees
search the configuration directory first.
A CLI tree is a module or JSON file that describes a command and its
subcommands, flags, arguments, help text and TAB completers.
*gcloud interactive* uses CLI trees for typeahead, command line completion,
and as-you-type documentation.
Most CLI tree files are cached in the *cli* subdirectory of the *gcloud*
installation root directory. The cache is automatically updated by the
Cloud SDK installers and the *gcloud components* command group.
"""
@staticmethod
def Args(parser):
parser.add_argument(
'--directory',
help='Insert this directory into the list of directories to search.')
parser.display_info.AddFormat(
'table[box](command:sort=1, cli_version:label=CLI, version:label=VER, '
'path, error)')
def Run(self, args):
return list_cli_trees.ListAll(directory=args.directory)

View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Updates non-gcloud CLI command trees in the installation directory."""
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 cli_tree
from googlecloudsdk.command_lib.meta import generate_cli_trees
def _GetCliTreeGeneratorList():
return ', '.join(
sorted(
[cli_tree.DEFAULT_CLI_NAME]
+ list(generate_cli_trees.GENERATORS.keys())
)
)
@base.UniverseCompatible
class Update(base.Command):
"""Updates gcloud CLI command trees in the installation directory.
A CLI tree is a module or JSON file that describes a command and its
subcommands, flags, arguments, help text and TAB completers.
*gcloud interactive* uses CLI trees for typeahead, command line completion,
and as-you-type documentation.
Most CLI tree files are cached in the *cli* subdirectory of the *gcloud*
installation root directory. The cache is automatically updated by the
Cloud SDK installers and the *gcloud components* command group.
These CLIs are currently supported: {generators}.
"""
detailed_help = {'generators': _GetCliTreeGeneratorList}
@staticmethod
def Args(parser):
parser.add_argument(
'--commands',
type=arg_parsers.ArgList(),
metavar='COMMAND',
help='Update only the commands in this list.',
)
parser.add_argument(
'--directory',
help=(
'Update this directory instead of the default installation '
'directory.'
),
)
parser.add_argument(
'--force',
action='store_true',
help=(
'Force existing CLI trees to be out of date. This causes them '
'to be regenerated.'
),
)
parser.add_argument(
'--tarball',
help=(
'For packaging CLI trees. --commands specifies one command '
'that is a relative path in this tarball. The tarball is '
'extracted to a temporary directory and the command path is '
'adjusted to point to the temporary directory.'
),
)
parser.add_argument(
'--skip-completions',
action='store_true',
default=False,
help='Skip updating the static completion CLI tree.',
)
def Run(self, args):
generate_cli_trees.UpdateCliTrees(
cli=self._cli_power_users_only,
commands=args.commands,
directory=args.directory,
tarball=args.tarball,
force=args.force,
verbose=not args.quiet,
skip_completions=args.skip_completions,
)

View File

@@ -0,0 +1,270 @@
# -*- 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.
"""The `gcloud meta daemon` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import http.server
import json
import logging
import os
import socketserver
import sys
import time
from googlecloudsdk import gcloud_main
from googlecloudsdk.calliope import base
from googlecloudsdk.core.util import files
# --- Configuration ---
HOST = '0.0.0.0'
PORT = 8080
PRECOMPUTE_DATA = os.environ.get(
'GCLOUD_DAEMON_PRECOMPUTE_DATA', 'CLI'
) # or SURFACES or COMMANDS
def perform_gcloud_execution(cli_obj, command_list):
"""Executes the gcloud command using the precomputed CLI object."""
start_time = time.time()
pid = os.getpid() # Current process PID
logging.info(
"PID: %s - Starting perform_gcloud_execution with input: '%s'",
pid,
command_list,
)
if cli_obj is None:
logging.error('PID: %s - Precomputed CLI object is None!', pid)
return {'error': 'Internal Server Error: CLI object not available'}
try:
request = cli_obj.Execute(command_list)
response_data = {
'uri': request.uri,
'method': request.method,
'headers': {
k.decode('utf-8'): v.decode('utf-8')
for k, v in request.headers.items()
},
'body': (
request.body.decode('utf-8')
if isinstance(request.body, bytes)
else request.body
),
}
logging.info('PID: %s - Execution successful.', pid)
return response_data
except SystemExit as exc:
logging.exception('PID: %s - SystemExit processing request: %s', pid, exc)
exit_code = exc.code if isinstance(exc.code, int) else 1
return {
'error': 'Command failed with SystemExit: %s' % exc,
'exit_code': exit_code,
}
except Exception as e: # pylint: disable=broad-exception-caught
logging.error('PID: %s - Exception during command execution:', pid)
return {'error': 'Exception during command execution: %s' % str(e)}
finally:
end_time = time.time()
computation_time = end_time - start_time
logging.info(
'PID: %s - Completed perform_gcloud_execution in %.4f seconds',
pid,
computation_time,
)
@base.Hidden
@base.DefaultUniverseOnly
class Daemon(base.Command):
"""(DEBUG MODE) Precomputes gcloud CLI and runs a single-request server."""
detailed_help = {
'DESCRIPTION': """
(DEBUG MODE) Initializes the gcloud CLI environment based on PRECOMPUTE_DATA,
starts an HTTP server in the FOREGROUND, serves exactly one POST
request to the root path ('/'), executes the requested gcloud command
using the precomputed environment, returns the result, and then exits.
The command will BLOCK until the single request is received and processed.
""",
'EXAMPLES': """
To run the foreground daemon precomputing the basic CLI:
$ gcloud meta daemon
(The command will now wait here)
Open another terminal and send a request (e.g., using curl):
$ curl -X POST -H "Content-Type: application/json" -d '{"command_list": ["projects", "list", "--limit=1", "--format=json"]}' http://localhost:8080/
(The original 'gcloud meta daemon' command will process this, print logs, and then exit)
""",
}
def Run(self, args):
# Configure logging for the main foreground process
# pid = os.getpid()
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - PID: %(process)d - %(levelname)s - %(message)s',
)
# --- Precompute CLI in this main process ---
logging.info('Starting precomputation...')
precomputation_start_time = time.time()
try:
# Set SDK properties needed for precomputation/execution
os.environ['CLOUDSDK_CORE_DRY_RUN'] = '1'
os.environ['CLOUDSDK_AUTH_DISABLE_CREDENTIALS'] = '1'
os.environ['CLOUDSDK_CORE_DISABLE_PROMPTS'] = '1'
os.environ['CLOUDSDK_CORE_DISABLE_USAGE_REPORTING'] = '1'
os.environ['CLOUDSDK_COMPONENT_MANAGER_DISABLE_UPDATE_CHECK'] = '1'
os.environ['CLOUDSDK_CORE_DISABLE_FILE_LOGGING'] = '1'
os.environ['CLOUDSDK_CORE_SHOW_STRUCTURED_LOGS'] = 'always'
os.environ['CLOUDSDK_CORE_VERBOSITY'] = 'error'
os.environ['CLOUDSDK_METRICS_ENVIRONMENT'] = 'gaas'
os.environ['CLOUDSDK_CORE_USER_OUTPUT_ENABLED'] = '0'
precomputed_cli = gcloud_main.CreateCLI([])
logging.info('Precomputing basic CLI...')
logging.info('Loading frequent commands')
try:
compute_instances_list_command = [
'compute',
'instances',
'list',
'--project=fake-project',
]
precomputed_cli.Execute(compute_instances_list_command)
except Exception: # pylint: disable=broad-exception-caught
pass
precomputation_end_time = time.time()
logging.info(
'Precomputation complete in %.4f seconds.',
precomputation_end_time - precomputation_start_time,
)
except Exception as e: # pylint: disable=broad-exception-caught
logging.exception('Failed during CLI precomputation:')
sys.exit('Error during CLI precomputation: %s' % e)
# --- Define Handler and Start Server ---
class SingleRequestHandler(http.server.BaseHTTPRequestHandler):
"""Handles ONE incoming POST request and then signals shutdown."""
# pylint: disable=invalid-name
def do_POST(self):
"""Handles the single POST request."""
pid = os.getpid() # Log current process PID
logging.info('PID: %s - Request received at path: %s', pid, self.path)
if self.path == '/':
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
try:
request_data = json.loads(post_data.decode('utf-8'))
command_list = request_data.get('command_list')
if not command_list or not isinstance(command_list, list):
logging.error(
"PID: %s - Missing or invalid 'command_list' (must be a list)"
' in request',
pid,
)
self.send_error(
400,
"Missing or invalid 'command_list' (must be a list) in"
' request',
)
return
except json.JSONDecodeError:
logging.error('PID: %s - Invalid JSON request', pid, exc_info=True)
self.send_error(400, 'Invalid JSON request')
return
logging.info(
'PID: %s - Received compute request with command: %s',
pid,
command_list,
)
# Execute the command using the precomputed CLI
response_data = perform_gcloud_execution(
precomputed_cli, command_list
)
# Send response
status_code = 200 # sandbox always returns 200 for IPC
self.send_response(status_code)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response_data).encode('utf-8'))
logging.info('PID: %s - Response sent to client.', pid)
else:
logging.warning(
'PID: %s - Received request for unknown path: %s', pid, self.path
)
self.send_error(404) # Not Found
# Signal the server's main thread to shut down
logging.info(
'PID: %s - Request handled. Signaling server shutdown.', pid
)
# pylint: disable=redefined-builtin
def log_message(self, format, *args):
"""Log messages using the standard logger."""
# Adjust message slightly for foreground clarity
logging.info('PID: %s - HTTP Server: %s', os.getpid(), format % args)
# --- Start HTTP server directly in this process ---
logging.info('Starting foreground HTTP server...')
try:
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer((HOST, PORT), SingleRequestHandler) as httpd:
# Touch a file to signal readiness
gcloud_daemon_ready_file = '/tmp/gcloud_daemon.ready'
with files.FileWriter(gcloud_daemon_ready_file) as f:
f.write('ready')
logging.info('Created %s', gcloud_daemon_ready_file)
logging.info(
'Server started on http://%s:%s (PRECOMPUTE_DATA=%s)',
HOST,
PORT,
PRECOMPUTE_DATA,
)
# Process one request (blocks until one arrives) and then exits the
# 'with' block, which handles shutdown.
httpd.handle_request()
logging.info('Request handled. Server shutting down.')
httpd.server_close()
except Exception as e: # pylint: disable=broad-exception-caught
# Log any exception during server setup or the loop itself
logging.exception('Unhandled exception occurred in server process:')
sys.exit('Server error: %s' % e)
finally:
logging.info('Server process function exiting.')
logging.shutdown() # Flush and close handlers
logging.info('Daemon command finished normally after serving request.')
sys.exit(0) # Ensure clean exit

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*- #
# Copyright 2016 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.
"""The `gcloud meta debug` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.meta import debug
class Debug(base.Command):
"""Run an interactive debug console with the Cloud SDK libraries loaded.
This command runs an interactive console with the Cloud SDK libraries loaded.
It's useful for:
* Manually testing out an API.
* Exploring available Cloud SDK core libraries.
* Debugging specific problems.
It comes with many utilities pre-loaded in the environment:
* All API clients loaded with one command (`LoadApis()`). Then, for instance,
`appengine` refers to the App Engine API client.
* Many common Cloud SDK imports pre-imported (e.g. core.util.files,
console_io, properties).
Use `dir()` to explore them all.
"""
@staticmethod
def Args(parser):
parser.add_argument(
'--mode', choices=sorted(debug.CONSOLES.keys()), default='python',
help='The debug console mode to run in.')
def Run(self, args):
debug.CONSOLES[args.mode]()

View File

@@ -0,0 +1,135 @@
# A standard vocabulary of command names, so that users will perceive the
# Cloud SDK as an integrated whole rather than as a conglomeration of separate
# tools. A user familiar with one component will be able to predict and
# understand the command names in another component.
# When adding new command consider reusing one of these (if appropriate).
abandon-instances
abort
activate-refresh-token
activate-service-account
add
add-access-config
add-backend
add-health-checks
add-host-rule
add-instances
add-metadata
add-path-matcher
add-rows
add-service
add-tags
addinstance
attach-disk
cancel
cancel-deployment
categories
clone
command
config-ssh
configure
copy
copy-files
create
create-config
create-indexes
delete
delete-access-config
delete-instances
deploy
deprecate
describe
detach-disk
devshell
docker
download
edit
execute
expand
export
gen-repo-info-file
get
get-client-lib
get-health
get-logs
get-named-ports
get-operation
get-resource-limits
get-serial-port-output
git-helper
help
import
info
init
kubectl
lint
list
list-commands
list-gcloud
list-instance-updates
list-instances
list-logs
list-operations
list-projects
list-services
login
patch
pause
print-access-token
print-refresh-token
promote-replica
query
recreate-instances
reinstall
remove
remove-backend
remove-health-checks
remove-host-rule
remove-instances
remove-metadata
remove-path-matcher
remove-service
remove-tags
removeinstance
reset
reset-ssl-config
resize
restart
restore
restore-backup
resume
resume-instances
revoke
rollback
run
set
set-backup
set-default
set-default-service
set-disk-auto-delete
set-managed-by
set-named-ports
set-root-password
set-scheduling
set-target
set-target-pools
set-template
set-usage-bucket
show-rows
snapshot
ssh
start
start-instances
stop
stop-instances
suspend-instances
undelete
unset
update
update-backend
update-template
upload
version
wait
write

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that generates and/or updates help document directoriess."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
import googlecloudsdk.command_lib.meta.generate_command as generate_command
class GenerateCommand(base.Command):
"""Generate YAML file to implement given command.
The command YAML file is generated in the --output-dir directory.
"""
@staticmethod
def Args(parser):
parser.add_argument(
'collection',
metavar='COLLECTION',
help=('The name of the collection to generate commands for.'))
parser.add_argument(
'--output-dir',
metavar='DIRECTORY',
help=('The directory where the generated command YAML files '
'will be written. If not specified then yaml files will '
'not be generated. If no output directory is specified, '
'the new YAML file will be written to stdout.'))
def Run(self, args):
return generate_command.WriteAllYaml(args.collection, args.output_dir)

View File

@@ -0,0 +1,75 @@
# -*- 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.
"""A command that generates and/or updates single resource config commands."""
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.command_lib.meta import generate_config_command
from googlecloudsdk.command_lib.util.resource_map.declarative import resource_name_translator
class GenerateCommand(base.Command):
"""Generate declarative config commands with surface specs and tests."""
@staticmethod
def Args(parser):
parser.add_argument(
'--output-root',
metavar='DIRECTORY',
required=True,
help=('Root of the directory within which to generate config '
'config export commands.'))
parser.add_argument(
'--enable-overwrites',
action='store_true',
help=('When enabled, allows overwriting of existing commands, surface '
'specs, and test files.'))
parser.add_argument(
'--collections',
metavar='COLLECTION',
type=arg_parsers.ArgList(),
help=('List of apitools collections to generate commands for.'))
parser.add_argument(
'--release-tracks',
metavar='TRACK',
type=arg_parsers.ArgList(),
help='List of release tracks to generate commands for. E.g. `ALPHA,BETA,GA`'
)
def Run(self, args):
translator = resource_name_translator.ResourceNameTranslator()
release_tracks = getattr(args, 'release_tracks') or ['ALPHA']
specified_collections = getattr(args, 'collections')
for collection in translator:
render_files = False
resource_data = collection.resource_data
if ('enable_single_resource_autogen' in resource_data and
resource_data.enable_single_resource_autogen):
if specified_collections:
if collection.collection_name in specified_collections:
render_files = True
else:
render_files = True
if render_files:
generate_config_command.WriteConfigYaml(collection.collection_name,
args.output_root,
resource_data,
release_tracks,
args.enable_overwrites)

View File

@@ -0,0 +1,174 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that generates and/or updates help document directoriess."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import walker_util
from googlecloudsdk.command_lib.meta import help_util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_attr
class HelpOutOfDateError(exceptions.Error):
"""Help documents out of date for --test."""
class GenerateHelpDocs(base.Command):
"""Generate and/or update help document directories.
The DevSite docs are generated in the --devsite-dir directory with pathnames
in the reference directory hierarchy. The manpage docs are generated in the
--manpage-dir directory with pathnames in the manN/ directory hierarchy.
"""
@staticmethod
def Args(parser):
parser.add_argument(
'--hidden',
action='store_true',
default=False,
help=('Include documents for hidden commands and groups.'))
parser.add_argument(
'--devsite-dir',
metavar='DIRECTORY',
help=('The directory where the generated DevSite reference document '
'subtree will be written. If not specified then DevSite '
'documents will not be generated.'))
parser.add_argument(
'--help-text-dir',
metavar='DIRECTORY',
help=('The directory where the generated help text reference document '
'subtree will be written. If not specified then help text '
'documents will not be generated. The --hidden flag is implied '
'for --help-text-dir.'))
parser.add_argument(
'--html-dir',
metavar='DIRECTORY',
help=('The directory where the standalone manpage HTML files will be '
'generated. index.html contains manpage tree navigation in the '
'left pane. The active command branch and its immediate children '
'are visible and clickable. Hover to navigate the tree. Run '
'`python -m http.server 8888 &` in DIRECTORY and point '
'your browser at [](http://localhost:8888) to view the manpage '
'tree. If not specified then the HTML manpage site will not be '
'generated.'))
parser.add_argument(
'--linter-dir',
metavar='DIRECTORY',
help=('The directory where the generated documentation linter errors '
'for the help text reference document subtree will be written. '
'If not specified then documentation linter documents will not '
'be generated.'))
parser.add_argument(
'--manpage-dir',
metavar='DIRECTORY',
help=('The directory where the generated manpage document subtree will '
'be written. The manpage hierarchy is flat with all command '
'documents in the manN/ subdirectory. If not specified then '
'manpage documents will not be generated.'))
parser.add_argument(
'--test',
action='store_true',
help=('Show but do not apply --update actions. Exit with non-zero exit '
'status if any help document file must be updated.'))
parser.add_argument(
'--update',
action='store_true',
default=False,
help=('Update destination directories to match the current CLI. '
'Documents for commands not present in the current CLI will be '
'deleted. Use this flag to update the help text golden files '
'after the help_text_test test fails.'))
parser.add_argument(
'--update-help-text-dir',
hidden=True,
metavar='DIRECTORY',
help='Deprecated. Use --update --help-text-dir=DIRECTORY instead.')
parser.add_argument(
'restrict',
metavar='COMMAND/GROUP',
nargs='*',
default=None,
help=("""Restrict document generation to these dotted command paths.
For example:
gcloud.alpha gcloud.beta.test
OR
gcloud.{alpha.,beta.,}compute.instances
"""))
def Run(self, args):
out_of_date = set()
def Generate(kind, generator, directory, encoding='utf-8', hidden=False):
"""Runs generator and optionally updates help docs in directory."""
restrict_dir = [re.sub(r'_', r'-', p) for p in args.restrict]
console_attr.ResetConsoleAttr(encoding)
if not args.update:
generator(
self._cli_power_users_only, directory, restrict=restrict_dir
).Walk(hidden, restrict_dir)
elif help_util.HelpUpdater(
self._cli_power_users_only, directory, generator,
test=args.test, hidden=hidden).Update(restrict_dir):
out_of_date.add(kind)
# Handle deprecated flags -- probably burned in a bunch of eng scripts.
if args.update_help_text_dir:
log.warning('[--update-help-text-dir={directory}] is deprecated. Use '
'this instead: --update --help-text-dir={directory}.'.format(
directory=args.update_help_text_dir))
args.help_text_dir = args.update_help_text_dir
args.update = True
# Generate/update the destination document directories.
if args.devsite_dir:
Generate('DevSite', walker_util.DevSiteGenerator, args.devsite_dir,
hidden=args.hidden)
if args.help_text_dir:
Generate('help text', walker_util.HelpTextGenerator, args.help_text_dir,
'ascii', hidden=True)
if args.html_dir:
Generate('html', walker_util.HtmlGenerator, args.html_dir,
hidden=args.hidden)
if args.manpage_dir:
Generate('man page', walker_util.ManPageGenerator, args.manpage_dir,
hidden=args.hidden)
if args.linter_dir:
Generate('command linter', walker_util.LinterGenerator, args.linter_dir,
hidden=args.hidden)
# Test update fails with an exception if documents are out of date.
if out_of_date and args.test:
names = sorted(out_of_date)
if len(names) > 1:
kinds = ' and '.join([', '.join(names[:-1]), names[-1]])
else:
kinds = names[0]
raise HelpOutOfDateError(
'{} document files must be updated.'.format(kinds))

View File

@@ -0,0 +1,329 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that validates gcloud flags according to Cloud SDK CLI Style."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import io
import os
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import usage_text
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.util import files
import six
class UnknownCheckException(Exception):
"""An exception when unknown lint check is requested."""
class LintException(exceptions.Error):
"""One or more lint errors found."""
class LintError(object):
"""Validation failure.
Attributes:
name: str, The name of the validation that produced this failure.
command: calliope.backend.CommandCommon, The offending command.
msg: str, A message indicating what the problem was.
"""
def __init__(self, name, command, error_message):
self.name = name
self.command = command
self.msg = '[{cmd}]: {msg}'.format(
cmd='.'.join(command.GetPath()), msg=error_message)
class Checker(object):
"""The abstract base class for all the checks.
Attributes:
name: A string, the name of this Checker.
description: string, command line description of this check.
"""
def ForEveryGroup(self, group):
pass
def ForEveryCommand(self, command):
pass
def End(self):
return []
class NameChecker(Checker):
"""Checks if group,command and flags names have underscores or mixed case."""
name = 'NameCheck'
description = 'Verifies all existing flags not to have underscores.'
def __init__(self):
super(NameChecker, self).__init__()
self._issues = []
def _ForEvery(self, cmd_or_group):
"""Run name check for given command or group."""
if '_' in cmd_or_group.cli_name:
self._issues.append(LintError(
name=NameChecker.name,
command=cmd_or_group,
error_message='command name [{0}] has underscores'.format(
cmd_or_group.cli_name)))
if not (cmd_or_group.cli_name.islower() or cmd_or_group.cli_name.isupper()):
self._issues.append(LintError(
name=NameChecker.name,
command=cmd_or_group,
error_message='command name [{0}] mixed case'.format(
cmd_or_group.cli_name)))
for flag in cmd_or_group.GetSpecificFlags():
if not any(f.startswith('--') for f in flag.option_strings):
if len(flag.option_strings) != 1 or flag.option_strings[0] != '-h':
self._issues.append(LintError(
name=NameChecker.name,
command=cmd_or_group,
error_message='flag [{0}] has no long form'.format(
','.join(flag.option_strings))))
for flag_option_string in flag.option_strings:
msg = None
if '_' in flag_option_string:
msg = 'flag [%s] has underscores' % flag_option_string
if (flag_option_string.startswith('--')
and not flag_option_string.islower()):
msg = 'long flag [%s] has upper case characters' % flag_option_string
if msg:
self._issues.append(LintError(
name=NameChecker.name, command=cmd_or_group, error_message=msg))
def ForEveryGroup(self, group):
self._ForEvery(group)
def ForEveryCommand(self, command):
self._ForEvery(command)
def End(self):
return self._issues
class BadListsChecker(Checker):
"""Checks command flags that take lists."""
name = 'BadLists'
description = 'Verifies all flags implement lists properly.'
def __init__(self):
super(BadListsChecker, self).__init__()
self._issues = []
def _ForEvery(self, cmd_or_group):
for flag in cmd_or_group.GetSpecificFlags():
if flag.nargs not in [None, 0, 1]:
self._issues.append(LintError(
name=BadListsChecker.name,
command=cmd_or_group,
error_message=(
'flag [{flg}] has nargs={nargs}'.format(
flg=flag.option_strings[0],
nargs="'{}'".format(six.text_type(flag.nargs))))))
if isinstance(flag.type, arg_parsers.ArgObject):
# No metavar requirements for ArgObject.
return
if isinstance(flag.type, arg_parsers.ArgDict):
if not (flag.metavar or flag.type.spec):
self._issues.append(
LintError(
name=BadListsChecker.name,
command=cmd_or_group,
error_message=(
('dict flag [{flg}] has no metavar and type.spec'
' (at least one needed)'
).format(flg=flag.option_strings[0]))))
elif isinstance(flag.type, arg_parsers.ArgList):
if not flag.metavar:
self._issues.append(LintError(
name=BadListsChecker.name,
command=cmd_or_group,
error_message=(
'list flag [{flg}] has no metavar'.format(
flg=flag.option_strings[0]))))
def ForEveryGroup(self, group):
self._ForEvery(group)
def ForEveryCommand(self, command):
self._ForEvery(command)
def End(self):
return self._issues
def _GetAllowlistedCommandVocabulary():
"""Returns allowlisted set of gcloud commands."""
vocabulary_file = os.path.join(os.path.dirname(__file__),
'gcloud_command_vocabulary.txt')
return set(
line for line in files.ReadFileContents(vocabulary_file).split('\n')
if not line.startswith('#'))
class VocabularyChecker(Checker):
"""Checks that command is the list of allowlisted names."""
name = 'AllowlistedNameCheck'
description = 'Verifies that every command is allowlisted.'
def __init__(self):
super(VocabularyChecker, self).__init__()
self._allowlist = _GetAllowlistedCommandVocabulary()
self._issues = []
def ForEveryGroup(self, group):
pass
def ForEveryCommand(self, command):
if command.cli_name not in self._allowlist:
self._issues.append(LintError(
name=self.name,
command=command,
error_message='command name [{0}] is not allowlisted'.format(
command.cli_name)))
def End(self):
return self._issues
def _WalkGroupTree(group):
"""Visits each group in the CLI group tree.
Args:
group: backend.CommandGroup, root CLI subgroup node.
Yields:
group instance.
"""
yield group
for sub_group in six.itervalues(group.groups):
for value in _WalkGroupTree(sub_group):
yield value
class Linter(object):
"""Lints gcloud commands."""
def __init__(self):
self._checks = []
def AddCheck(self, check):
self._checks.append(check())
def Run(self, group_root):
"""Runs registered checks on all groups and commands."""
for group in _WalkGroupTree(group_root):
for check in self._checks:
check.ForEveryGroup(group)
for command in six.itervalues(group.commands):
for check in self._checks:
check.ForEveryCommand(command)
return [issue for check in self._checks for issue in check.End()]
# List of registered checks, all are run by default.
_DEFAULT_LINT_CHECKS = [
NameChecker,
]
_LINT_CHECKS = [
BadListsChecker,
VocabularyChecker,
]
def _FormatCheckList(check_list):
buf = io.StringIO()
for check in check_list:
usage_text.WrapWithPrefix(
check.name, check.description, 20, 78, ' ', writer=buf)
return buf.getvalue()
class Lint(base.Command):
"""Validate gcloud flags according to Cloud SDK CLI Style."""
@staticmethod
def Args(parser):
parser.add_argument(
'checks',
metavar='CHECKS',
nargs='*',
default=[],
help="""\
A list of checks to apply to gcloud groups and commands.
If omitted will run all available checks.
Available Checks:
""" + _FormatCheckList(_LINT_CHECKS))
def Run(self, args):
# pylint: disable=protected-access
group = self._cli_power_users_only._TopElement()
group.LoadAllSubElements(recursive=True)
return Lint._SetupAndRun(group, args.checks)
@staticmethod
def _SetupAndRun(group, check_list):
"""Builds up linter and executes it for given set of checks."""
linter = Linter()
unknown_checks = []
if not check_list:
for check in _DEFAULT_LINT_CHECKS:
linter.AddCheck(check)
else:
available_checkers = dict(
(checker.name, checker)
for checker in _DEFAULT_LINT_CHECKS + _LINT_CHECKS)
for check in check_list:
if check in available_checkers:
linter.AddCheck(available_checkers[check])
else:
unknown_checks.append(check)
if unknown_checks:
raise UnknownCheckException(
'Unknown lint checks: %s' % ','.join(unknown_checks))
return linter.Run(group)
def Display(self, args, result):
writer = log.out
for issue in result:
writer.Print(issue.msg)
if result:
raise LintException('there were some lint errors.')

View File

@@ -0,0 +1,565 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Command that statically validates gcloud commands for corectness.
To validate a command, run:
```
gcloud meta lint-gcloud-commands --command-string="gcloud compute instances
list"
```
To validate a list of commands in a file:
1) Create a JSON file with the following format:
```
[
{
"command_string": "gcloud compute instances list",
},
{
"command_string": "gcloud compute instances describe my-instance",
}
]
```
2) Then run the command:
```
gcloud meta lint-gcloud-commands --commands-file=commands.json
```
Commands can also be associated with an ID, which will be used to identify the
command in the output. Simply run:
```
gcloud meta lint-gcloud-commands --commands-file=commands.json --serialize
```
This will associated each command with using the index it was found in the file
as the ID. If you want to associate a command with a specific ID, you can do so
by adding the `id` field to the command in the JSON file. For example:
```
[
{
"command_string": "gcloud compute instances list",
"id": 0,
},
{
"command_string": "gcloud compute instances describe my-instance",
"id": 1,
}
]
```
This will output the validation results in the following format:
```
{"0": [{<OUTPUT_1>}], "1": [{<OUTPUT_2>}]}
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import argparse
import copy
import json
import os
import re
import shlex
from typing import collections
from googlecloudsdk import gcloud_main
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as gcloud_exceptions
from googlecloudsdk.command_lib.meta import generate_argument_spec
from googlecloudsdk.core import log
from googlecloudsdk.core import yaml
from googlecloudsdk.core.util import files
import six
_PARSING_OUTPUT_TEMPLATE = {
'command_string': None,
'success': False,
'command_args': None,
'command_string_no_args': None,
'args_structure': {},
'error_message': None,
'error_type': None,
}
_IGNORE_ARGS = ['--help']
class CommandValidationError(Exception):
pass
def _read_commands_from_file(commands_file):
"""Reads commands from a JSON file."""
with files.FileReader(commands_file) as f:
command_file_data = json.load(f)
ref_id = 0
command_strings = {}
needs_id = any(command_data.get('id') for command_data in command_file_data)
for command_data in command_file_data:
command_id = command_data.get('id')
if needs_id and command_id is None:
raise ValueError(
'Not all commands have an ID. Id for command'
f' {command_data["command_string"]} is None.'
)
command_strings[command_data['command_string']] = command_id or ref_id
ref_id += 1
return command_strings
def _separate_command_arguments(command_string: str):
"""Move all flag arguments to back."""
command_string = command_string.split('#')[0]
try:
# Split arguments
if os.name == 'nt':
command_arguments = shlex.split(command_string, posix=False)
else:
command_arguments = shlex.split(command_string)
except Exception: # pylint: disable=broad-except
raise CommandValidationError(
'Command could not be validated due to unforeseen edge case.'
)
# Move any flag arguments to end of command.
flag_args = [arg for arg in command_arguments if arg.startswith('--')]
command_args = [arg for arg in command_arguments if not arg.startswith('--')]
return command_args + flag_args
def _add_equals_to_flags(command):
"""Adds equals signs to gcloud command flags, except for format and help flags."""
pattern = ( # Matches flag name and its value (excluding format and help)
r'(--[a-zA-Z0-9-]+) +([^-][^ ]*)'
)
replacement = r'\1=\2' # Inserts equals sign between flag and
modified_command = re.sub(pattern, replacement, command)
# Remove = from flags without explicit values
modified_command = re.sub(r'(--[a-zA-Z0-9-]+)= ', r'\1 ', modified_command)
return modified_command
def formalize_gcloud_command(command_str):
command_str = _add_equals_to_flags(command_str)
command_str = (
command_str.replace('--project=PROJECT ', '--project=my-project ')
.replace('--project=PROJECT_ID ', '--project=my-project ')
.replace('$PROJECT_ID ', 'my-project ')
.replace('YOUR_PROJECT_ID ', 'my-project ')
)
return command_str
def _extract_gcloud_commands(text):
"""Extracts code snippets from fenced code blocks within a text string.
Args:
text: The text string containing fenced code blocks.
Returns:
A list of extracted code snippets.
"""
text = bytes(text, 'utf-8').decode('unicode_escape')
fenced_pattern = r'```(?:[\w ]+\n)?(.*?)```'
indented_pattern = ( # 3-8 indented spaces as arbitray nums
r'(?: {3-8}|\t)(.*?)(?:\n\S|\n$)'
)
combined_pattern = re.compile(
f'{fenced_pattern}|{indented_pattern}', re.DOTALL
)
code_snippets = []
for match in combined_pattern.finditer(
text
): # use finditer instead of findall
command_str = match.group(1).strip()
if 'gcloud ' not in command_str or not command_str.startswith('gcloud'):
continue
for cmd in command_str.split('gcloud '):
cmd_new_lines = cmd.split('\n')
if len(cmd_new_lines) >= 1 and cmd_new_lines[0].strip():
command_str = formalize_gcloud_command(cmd_new_lines[0].strip())
code_snippets.append(f'gcloud {command_str}')
return code_snippets
def _get_command_node(command_arguments):
"""Returns the command node for the given command arguments."""
cli = gcloud_main.CreateCLI([])
command_arguments = command_arguments[1:]
current_command_node = cli._TopElement() # pylint: disable=protected-access
for argument in command_arguments:
if argument.startswith('--'):
break
child_command_node = current_command_node.LoadSubElement(argument)
if not child_command_node:
break
current_command_node = child_command_node
return current_command_node
def _get_command_no_args(command_node):
"""Returns the command string without any arguments."""
return ' '.join(command_node.ai.command_name)
def _get_command_args_tree(command_node):
"""Returns the command string without any arguments."""
argument_tree = generate_argument_spec.GenerateArgumentSpecifications(
command_node
)
return argument_tree
def _get_positional_metavars(args_tree):
"""Returns a dict of positional metavars."""
positional_args = []
def _process_arg(node):
if 'name' in node and node.get('positional', False):
if node['name']:
positional_args.append(node['name'])
def _traverse_arg_group(node):
for arg in node:
_traverse_tree(arg)
def _traverse_tree(node):
if 'group' in node:
group = node['group']['arguments']
_traverse_arg_group(group)
else:
_process_arg(node)
for node in args_tree:
_traverse_tree(node)
return positional_args
def _normalize_command_args(command_args, args_tree):
"""Normalizes command args for storage."""
positionals_used = set()
arg_name_value = {}
positional_args_in_tree = _get_positional_metavars(args_tree['arguments'])
def _sort_command_args(args):
"""Sorts command arguments.
Arguments starting with '--' are placed at the back, and all arguments are
ordered alphabetically.
Args:
args: The command arguments to sort.
Returns:
The sorted command arguments.
"""
flag_args = sorted([arg for arg in args if arg.startswith('--')])
positional_args = [arg for arg in args if not arg.startswith('--')]
return positional_args + flag_args
command_args = _sort_command_args(command_args)
def _get_next_available_positional_arg():
for positional_metavar in positional_args_in_tree:
if positional_metavar not in positionals_used:
command_value = command_arg
command_arg_name = positional_metavar.upper()
positionals_used.add(positional_metavar)
return command_arg_name, command_value
return None, None
arg_index = 0
for command_arg in command_args:
command_arg_name = command_arg
if command_arg.startswith('--'):
equals_index = command_arg.find('=')
if equals_index != -1:
command_arg_name = command_arg[:equals_index]
command_value = command_arg[equals_index + 1 :]
else:
command_value = ''
else:
# Positional argument
command_arg_name, command_value = _get_next_available_positional_arg()
# Arg should be included in output, regardless of whether it a real
# positional arg or not.
command_arg_name = command_arg_name or command_arg
command_value = command_value or ''
arg_name_value[command_arg_name] = {
'value': command_value,
'index': arg_index,
}
arg_index += 1
return collections.OrderedDict(
sorted(arg_name_value.items(), key=lambda item: item[1]['index'])
)
@base.UniverseCompatible
class GenerateCommand(base.Command):
"""Generate YAML file to implement given command.
The command YAML file is generated in the --output-dir directory.
"""
_INDEXED_VALIDATION_RESULTS = collections.OrderedDict()
_SERIALIZED_VALIDATION_RESULTS = collections.OrderedDict()
_VALIDATION_RESULTS = []
index_results = False
serialize_results = False
def _validate_command(self, command_string, ref_id=0):
"""Validate a single command."""
command_string = formalize_gcloud_command(command_string)
command_arguments = _separate_command_arguments(command_string)
command_success, command_node, flag_arguments = (
self._validate_command_prefix(command_arguments, command_string, ref_id)
)
if not command_success:
return
flag_success = self._validate_command_suffix(
command_node, flag_arguments, command_string, ref_id
)
if not flag_success:
return
self._store_validation_results(True, command_string, ref_id, flag_arguments)
def _validate_commands_from_file(self, commands_file):
"""Validate multiple commands given in a file."""
commands = _read_commands_from_file(commands_file)
for command, ref_id in commands.items():
try:
self._validate_command(command, ref_id)
except Exception as e: # pylint: disable=broad-except
self._store_validation_results(
False,
command,
ref_id,
None,
f'Command could not be validated: {e}',
'CommandValidationError',
)
def _validate_commands_from_text(self, commands_text_file):
"""Validate multiple commands given in a text string."""
with files.FileReader(commands_text_file) as f:
text = f.read()
commands = _extract_gcloud_commands(text)
ref_id = 0
for command in commands:
self._validate_command(command, ref_id)
ref_id += 1
def _validate_command_prefix(self, command_arguments, command_string, ref_id):
"""Validate that the argument string contains a valid command or group."""
cli = gcloud_main.CreateCLI([])
# Remove "gcloud" from command arguments.
command_arguments = command_arguments[1:]
index = 0
current_command_node = cli._TopElement() # pylint: disable=protected-access
for argument in command_arguments:
# If this hits, we've found a command group with a flag passed.
# e.g. gcloud compute --help
if argument.startswith('--'):
return (
True,
current_command_node,
command_arguments[index:],
)
# Attempt to load next section of command path.
current_command_node = current_command_node.LoadSubElement(argument)
# If not a valid section of command path, fail validation.
if not current_command_node:
self._store_validation_results(
False,
command_string,
ref_id,
command_arguments[index:],
"Invalid choice: '{}'".format(argument),
'UnrecognizedCommandError',
)
return False, None, None
index += 1
# If command path is valid and is a command, return the command node.
if not current_command_node.is_group:
return (
True,
current_command_node,
command_arguments[index:],
)
# If we make it here then only a command group has been provided.
remaining_flags = command_arguments[index:]
if not remaining_flags:
self._store_validation_results(
False,
command_string,
ref_id,
command_arguments[index:],
'Command name argument expected',
'UnrecognizedCommandError',
)
return False, None, None
# If we've iterated through the entire list and end up here, something
# unpredicted has happened.
raise CommandValidationError(
'Command could not be validated due to unforeseen edge case.'
)
def _validate_command_suffix(
self, command_node, command_arguments, command_string, ref_id
):
"""Validates that the given flags can be parsed by the argparse parser."""
for ignored_arg in _IGNORE_ARGS:
if ignored_arg in command_arguments:
return True
found_parent = False
if command_arguments:
for command_arg in command_arguments:
if (
'--project' in command_arg
or '--folder' in command_arg
or '--organization' in command_arg
):
found_parent = True
if not command_arguments:
command_arguments = []
if not found_parent:
command_arguments.append('--project=myproject')
try:
command_node._parser.parse_args(command_arguments, raise_error=True) # pylint: disable=protected-access
except (
files.MissingFileError,
gcloud_exceptions.BadFileException,
yaml.FileLoadError,
):
pass
except argparse.ArgumentError as e:
if 'No such file or directory' in str(e):
return True
self._store_validation_results(
False,
command_string,
ref_id,
command_arguments,
six.text_type(e),
type(e).__name__,
)
return False
return True
def _store_validation_results(
self,
success,
command_string,
ref_id,
command_args=None,
error_message=None,
error_type=None,
):
"""Store information related to the command validation."""
validation_output = copy.deepcopy(_PARSING_OUTPUT_TEMPLATE)
validation_output['command_string'] = command_string
try:
command_node = _get_command_node(
_separate_command_arguments(command_string)
)
validation_output['command_string_no_args'] = _get_command_no_args(
command_node
)
validation_output['args_structure'] = _get_command_args_tree(command_node)
except Exception: # pylint: disable=broad-except
validation_output['command_string_no_args'] = command_string
if command_args:
validation_output['command_args'] = _normalize_command_args(
command_args, validation_output['args_structure']
)
validation_output['success'] = success
validation_output['error_message'] = error_message
validation_output['error_type'] = error_type
sorted_validation_output = collections.OrderedDict(
sorted(validation_output.items())
)
if self.serialize_results:
if ref_id not in self._SERIALIZED_VALIDATION_RESULTS:
self._SERIALIZED_VALIDATION_RESULTS[ref_id] = [sorted_validation_output]
else:
self._SERIALIZED_VALIDATION_RESULTS[ref_id].append(
sorted_validation_output
)
if self.index_results:
self._INDEXED_VALIDATION_RESULTS[command_string] = (
sorted_validation_output
)
else:
self._VALIDATION_RESULTS.append(sorted_validation_output)
def _log_validation_results(self):
"""Output collected validation results."""
if self.index_results:
log.out.Print(json.dumps(self._INDEXED_VALIDATION_RESULTS))
elif self.serialize_results:
log.out.Print(json.dumps(self._SERIALIZED_VALIDATION_RESULTS))
else:
log.out.Print(json.dumps(self._VALIDATION_RESULTS))
@staticmethod
def Args(parser):
command_group = parser.add_group(mutex=True)
command_group.add_argument(
'--command-string',
help='Gcloud command to statically validate.',
)
command_group.add_argument(
'--commands-file',
help='JSON file containing list of gcloud commands to validate.',
)
command_group.add_argument(
'--commands-text-file',
help=(
'Raw text containing gcloud command(s) to validate. For example,'
' the commands could be in fenced code blocks or indented code'
' blocks.'
),
)
parser.add_argument(
'--serialize',
action='store_true',
help='Output results in a dictionary serialized by reference id.',
)
def Run(self, args):
if args.serialize:
self.serialize_results = True
if args.IsSpecified('command_string'):
self._validate_command(args.command_string)
elif args.IsSpecified('commands_text_file'):
self._validate_commands_from_text(args.commands_text_file)
else:
self._validate_commands_from_file(args.commands_file)
self._log_validation_results()

View File

@@ -0,0 +1,190 @@
# -*- coding: utf-8 -*- #
# Copyright 2014 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that lists all possible gcloud commands, optionally with flags."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import sys
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import cli_tree
from googlecloudsdk.calliope import walker_util
_LOOKUP_INTERNAL_FLAGS = '_flags_'
_LOOKUP_INTERNAL_NAME = '_name_'
def DisplayFlattenedCommandTree(command, out=None):
"""Displays the commands in the command tree in sorted order on out.
Args:
command: dict, The tree (nested dict) of command/group names.
out: stream, The output stream, sys.stdout if None.
"""
def WalkCommandTree(commands, command, args):
"""Visit each command and group in the CLI command tree.
Each command line is added to the commands list.
Args:
commands: [str], The list of command strings.
command: dict, The tree (nested dict) of command/group names.
args: [str], The subcommand arg prefix.
"""
args_next = args + [command[_LOOKUP_INTERNAL_NAME]]
if commands:
commands.append(' '.join(args_next))
else:
# List the global flags with the root command.
commands.append(' '.join(
args_next + command.get(_LOOKUP_INTERNAL_FLAGS, [])))
if cli_tree.LOOKUP_COMMANDS in command:
for c in command[cli_tree.LOOKUP_COMMANDS]:
name = c.get(_LOOKUP_INTERNAL_NAME, c)
flags = c.get(_LOOKUP_INTERNAL_FLAGS, [])
commands.append(' '.join(args_next + [name] + flags))
if cli_tree.LOOKUP_GROUPS in command:
for g in command[cli_tree.LOOKUP_GROUPS]:
WalkCommandTree(commands, g, args_next)
commands = []
WalkCommandTree(commands, command, [])
if not out:
out = sys.stdout
out.write('\n'.join(sorted(commands)) + '\n')
_COMPLETIONS_PREFIX = '_SC_'
def DisplayCompletions(command, out=None):
"""Displays the static tab completion data on out.
The static completion data is a shell script containing variable definitons
of the form {_COMPLETIONS_PREFIX}{COMMAND.PATH} for each dotted command path.
Args:
command: dict, The tree (nested dict) of command/group names.
out: stream, The output stream, sys.stdout if None.
"""
def ConvertPathToIdentifier(path):
return _COMPLETIONS_PREFIX + '__'.join(path).replace('-', '_')
def WalkCommandTree(command, prefix):
"""Visit each command and group in the CLI command tree.
Args:
command: dict, The tree (nested dict) of command/group names.
prefix: [str], The subcommand arg prefix.
"""
name = command.get(_LOOKUP_INTERNAL_NAME)
args = prefix + [name]
commands = command.get(cli_tree.LOOKUP_COMMANDS, [])
groups = command.get(cli_tree.LOOKUP_GROUPS, [])
names = []
for c in commands + groups:
names.append(c.get(_LOOKUP_INTERNAL_NAME, c))
if names:
flags = command.get(_LOOKUP_INTERNAL_FLAGS, [])
if prefix:
out.write('{identifier}=({args})\n'.format(
identifier=ConvertPathToIdentifier(args),
args=' '.join(names + flags)))
else:
out.write('{identifier}=({args})\n'.format(
identifier=ConvertPathToIdentifier(['-GCLOUD-WIDE-FLAGS-']),
args=' '.join(flags)))
out.write('{identifier}=({args})\n'.format(
identifier=ConvertPathToIdentifier(args),
args=' '.join(names)))
for c in commands:
name = c.get(_LOOKUP_INTERNAL_NAME, c)
flags = c.get(_LOOKUP_INTERNAL_FLAGS, [])
out.write('{identifier}=({args})\n'.format(
identifier=ConvertPathToIdentifier(args + [name]),
args=' '.join(flags)))
for g in groups:
WalkCommandTree(g, args)
if not out:
out = sys.stdout
WalkCommandTree(command, [])
class ListCommands(base.Command):
"""List all possible gcloud commands excluding flags."""
@staticmethod
def Args(parser):
parser.add_argument(
'--completions',
action='store_true',
help=("""\
Write the static TAB completion data on the standard output. The
data is a shell script containing variable definitons of the
form ```""" +
_COMPLETIONS_PREFIX +
'{COMMAND.PATH}``` for each dotted command path.')
)
parser.add_argument(
'--flags',
action='store_true',
help='Include the non-global flags for each command/group.')
parser.add_argument(
'--flag-values',
action='store_true',
help="""\
Include the non-global flags and flag values/types for each
command/group. Flags with fixed choice values will be listed as
--flag=choice1,..., and flags with typed values will be listed
as --flag=:type:.""")
parser.add_argument(
'--hidden',
action='store_true',
help='Include hidden commands and groups.')
parser.add_argument(
'--universe-compatible-commands',
hidden=True,
action='store_true',
help='Exclusively show universe compatible commands.')
parser.add_argument(
'restrict',
metavar='COMMAND/GROUP',
nargs='*',
help=('Restrict the listing to these dotted command paths. '
'For example: gcloud.alpha gcloud.beta.test'))
def Run(self, args):
if args.completions:
args.flags = True
args.flag_values = True
args.hidden = True
args.universe_compatible_commands = False
return walker_util.CommandTreeGenerator(
self._cli_power_users_only,
with_flags=args.flags,
with_flag_values=args.flag_values,
).Walk(args.hidden, args.universe_compatible_commands, args.restrict)
def Display(self, args, result):
if args.completions:
return DisplayCompletions(result)
return DisplayFlattenedCommandTree(result)

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The `gcloud meta list-files-for-upload` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.util import gcloudignore
class ListFilesForUpload(base.Command):
"""List files for upload.
List the files that would be uploaded in a given directory.
Useful for checking the effects of a .gitignore or .gcloudignore file.
"""
@staticmethod
def Args(parser):
parser.add_argument(
'directory', default='.', nargs='?',
help='The directory in which to show what files would be uploaded')
parser.display_info.AddFormat('value(.)')
def Run(self, args):
file_chooser = gcloudignore.GetFileChooserForDir(args.directory,
write_on_disk=False)
file_chooser = file_chooser or gcloudignore.FileChooser([])
return file_chooser.GetIncludedFiles(args.directory, include_dirs=False)

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that reads JSON data and lists it."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
import sys
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.core.util import files
class ListFromJson(base.ListCommand):
"""Read JSON data and list it on the standard output.
*{command}* is a test harness for resource output formatting and filtering.
It behaves like any other `gcloud ... list` command except that the resources
are read from a JSON data file.
The input JSON data is either a single resource object or a list of resource
objects of the same type. The resources are printed on the standard output.
The default output format is *json*.
"""
@staticmethod
def Args(parser):
base.URI_FLAG.RemoveFromParser(parser)
parser.add_argument(
'json_file',
metavar='JSON-FILE',
nargs='?',
default=None,
help=('A file containing JSON data for a single resource or a list of'
' resources of the same type. If omitted then the standard input'
' is read.'))
parser.display_info.AddFormat('json')
parser.display_info.AddCacheUpdater(None) # No resource URIs.
def Run(self, args):
if args.json_file:
try:
resources = json.loads(files.ReadFileContents(args.json_file))
except (files.Error, ValueError) as e:
raise exceptions.BadFileException(
'Cannot read [{}]: {}'.format(args.json_file, e))
else:
try:
resources = json.load(sys.stdin)
except (IOError, ValueError) as e:
raise exceptions.BadFileException(
'Cannot read the standard input: {}'.format(e))
return resources

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that lists the gcloud group and command tree with details."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import cli_tree
from googlecloudsdk.command_lib.static_completion import generate
class ListGCloud(base.Command):
"""List the gcloud CLI command tree with flag, positional and help details."""
@staticmethod
def Args(parser):
parser.add_argument(
'--branch',
metavar='COMMAND_PATH',
help=('The branch of the CLI subtree to generate as a dotted command '
'path. Mainly used to generate test data. For example, for the '
'`gcloud compute instances` branch use "compute.instances".'))
parser.add_argument(
'--completions',
action='store_true',
help=('List the static completion CLI tree. This is a stripped down '
'variant of the CLI tree that only contains the subcommand and '
'flag name dictionaries. The tree is written as a Python '
'source file (~1MiB) that loads fast (~30ms) as a .pyc file.'))
def Run(self, args):
branch = args.branch.split('.') if args.branch else None
if args.completions:
generate.ListCompletionTree(cli=self._cli_power_users_only, branch=branch)
else:
cli_tree.Dump(cli=self._cli_power_users_only, path='-', branch=branch)

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that generates all DevSite and manpage documents."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import io
import sys
from googlecloudsdk.calliope import base
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.document_renderers import render_document
class GenerateHelpDocs(base.Command):
"""Uses gcloud's markdown renderer to render the given markdown file."""
@staticmethod
def Args(parser):
parser.add_argument(
'md_file',
help=('The path to a file containing markdown to render, or `-` to '
'read from stdin.'))
parser.add_argument(
'--style',
default='text',
choices=sorted(render_document.STYLES.keys()),
help='The renderer output format.')
def Run(self, args):
data = console_io.ReadFromFileOrStdin(args.md_file, binary=False)
with io.StringIO(data) as f:
render_document.RenderDocument(args.style, f, sys.stdout)

View File

@@ -0,0 +1,28 @@
# -*- 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.
"""Command group for resource map related commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import cli_tree
@base.Hidden
@base.ReleaseTracks(base.ReleaseTrack.GA)
class ResourceMaps(base.Group):
"""Command group for resource map related commands."""

View File

@@ -0,0 +1,47 @@
# -*- 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.
"""Command used for updating resource maps."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.util.resource_map import resource_map_update_util
from googlecloudsdk.command_lib.util.resource_map.declarative import declarative_map_update_util
class UpdateResourceMap(base.Command):
"""Command used for updating resource maps."""
@staticmethod
def Args(parser):
group = parser.add_group(mutex=True)
group.add_argument(
'--all', action='store_true', help='Update all resource maps.')
individual_maps_group = group.add_group()
individual_maps_group.add_argument(
'--declarative',
action='store_true',
help='Update the declarative resource map.')
individual_maps_group.add_argument(
'--base', action='store_true', help='Update the base resource map.')
def Run(self, args):
if args.base or args.all:
resource_map_update_util.update()
if args.declarative or args.all:
declarative_map_update_util.update()

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The meta resources command group."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.Hidden
@base.ReleaseTracks(base.ReleaseTrack.GA)
class Resources(base.Group):
"""Cloud SDK resource parser test commands."""

View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that generates resource URIs given collection and api version."""
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.core import resources
import six
from six.moves import range
class Parse(base.Command):
"""Cloud SDK resource test URI generator.
*{command}* is an handy way to generate test URIs for the resource parser.
"""
@staticmethod
def Args(parser):
parser.add_argument(
'--collection',
metavar='NAME',
required=True,
help='The resource collection name of the resource to parse.')
parser.add_argument(
'--api-version',
metavar='VERSION',
help=('The resource collection API version. The collection default '
'is used if not specified.'))
parser.add_argument(
'--count',
default=1,
type=arg_parsers.BoundedInt(lower_bound=1),
help='The number of test resource URIs to generate.')
def Run(self, args):
"""Returns the list of generated resources."""
collection_info = resources.REGISTRY.GetCollectionInfo(
args.collection, api_version=args.api_version)
templates = {}
params = collection_info.GetParams('')
if not params:
# There are some collections that don't operate on resources but are
# manually created in regen_apis_config.yaml. If there are no template
# params, don't try to generate resources for them.
return []
for param in params:
templates[param] = 'my-' + param.lower() + '-{}'
uris = []
for i in range(1, args.count + 1):
params = {}
for param, template in six.iteritems(templates):
params[param] = template.format(i)
uri = resources.Resource(
None, collection_info, '', params, None).SelfLink()
uris.append(uri)
return uris

View File

@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that parses resources given collection and api version."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import sys
from googlecloudsdk.calliope import base
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import resources
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.resource import resource_printer
import six
class Parse(base.ListCommand):
"""Cloud SDK resource parser module tester.
*{command}* is an handy way to debug the resource parser from the command
line.
"""
@staticmethod
def Args(parser):
parser.add_argument(
'--api-version',
metavar='VERSION',
help=('The resource collection API version. The collection default '
'is used if not specified.'))
parser.add_argument(
'--collection',
metavar='NAME',
help='The resource collection name of the resource to parse.')
parser.add_argument(
'--stack-trace',
action='store_true',
default=True,
help=('Enable all exception stack traces, including Cloud SDK core '
'exceptions.'))
parser.add_argument(
'resources_to_parse',
nargs='*',
help=('The list of resource URLs to parse. If not specified then '
'*{command}* enters an interactive loop, prompting for URLs to '
'parse.'))
def Run(self, args):
"""Returns the parsed parameters for one resource."""
if args.api_version:
api_name = args.collection.split('.')[0]
resources.REGISTRY.RegisterApiByName(
api_name, api_version=args.api_version)
if args.resources_to_parse:
parsed_resources = []
for uri in args.resources_to_parse:
try:
resource = resources.REGISTRY.Parse(uri, collection=args.collection)
except (Exception, SystemExit) as e: # pylint: disable=broad-except
if args.stack_trace:
exceptions.reraise(e)
log.error(six.text_type(e))
parsed_resources.append({
'error': six.text_type(e),
'uri': uri,
})
continue
collection_info = resource.GetCollectionInfo()
parsed_resources.append({
'api_name': collection_info.api_name,
'api_version': collection_info.api_version,
'collection': collection_info.full_name,
'params': resource.AsDict(),
'uri': resource.SelfLink(),
})
return parsed_resources
while True:
uri = console_io.PromptResponse('PARSE> ')
if uri is None:
break
if not uri:
continue
try:
params = resources.REGISTRY.Parse(
uri,
collection=args.collection).AsDict()
except (Exception, SystemExit) as e: # pylint: disable=broad-except
if args.stack_trace:
exceptions.reraise(e)
log.error(six.text_type(e))
continue
resource_printer.Print(params, 'json')
sys.stderr.write('\n')
return None
def Epilog(self, items_were_listed=False):
del items_were_listed

View File

@@ -0,0 +1,224 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The `gcloud meta test` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import os
import signal
import sys
import time
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import parser_completer
from googlecloudsdk.calliope import parser_errors
from googlecloudsdk.command_lib.compute import completers
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import execution_utils
from googlecloudsdk.core import module_util
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.console import progress_tracker
@base.UniverseCompatible
class Test(base.Command):
"""Run miscellaneous gcloud command and CLI test scenarios.
This command sets up scenarios for testing the gcloud command and CLI.
"""
@staticmethod
def Args(parser):
parser.add_argument(
'name',
nargs='*',
completer=completers.TestCompleter,
help='command_lib.compute.TestCompleter instance name test.')
scenarios = parser.add_group(mutex=True, required=True)
scenarios.add_argument(
'--arg-dict',
type=arg_parsers.ArgDict(),
metavar='ATTRIBUTES',
help='ArgDict flag value test.')
scenarios.add_argument(
'--arg-list',
type=arg_parsers.ArgList(),
metavar='ITEMS',
help='ArgList flag value test.')
scenarios.add_argument(
'--argumenterror-outside-argparse',
action='store_true',
help=('Trigger a calliope.parser_errors.ArgumentError exception '
'outside of argparse.'))
scenarios.add_argument(
'--core-exception',
action='store_true',
help='Trigger a core exception.')
scenarios.add_argument(
'--exec-file',
metavar='SCRIPT_FILE',
help='Runs `bash SCRIPT_FILE`.')
scenarios.add_argument(
'--interrupt',
action='store_true',
help='Kill the command with SIGINT.')
scenarios.add_argument(
'--is-interactive',
action='store_true',
help=('Call console_io.IsInteractive(heuristic=True) and exit 0 '
'if the return value is True, 1 if False.'))
scenarios.add_argument(
'--prompt-completer',
metavar='MODULE_PATH',
help=('Call console_io.PromptResponse() with a MODULE_PATH completer '
'and print the response on the standard output.'))
scenarios.add_argument(
'--progress-tracker',
metavar='SECONDS',
type=float,
default=0.0,
help='Run the progress tracker for SECONDS seconds and exit.')
scenarios.add_argument(
'--sleep',
metavar='SECONDS',
type=float,
default=0.0,
help='Sleep for SECONDS seconds and exit.')
scenarios.add_argument(
'--uncaught-exception',
action='store_true',
help='Trigger an exception that is not caught.')
scenarios.add_argument(
'--staged-progress-tracker',
action='store_true',
help='Run example staged progress tracker.')
scenarios.add_argument(
'--feature-flag',
action='store_true',
help='Print the value of a feature flag.')
def _RunArgDict(self, args):
return args.arg_dict
def _RunArgList(self, args):
return args.arg_list
def _RunArgumenterrorOutsideArgparse(self, args):
raise parser_errors.RequiredError(argument='--some-flag')
def _RunCoreException(self, args):
raise exceptions.Error('Some core exception.')
def _RunExecFile(self, args):
# We may want to add a timeout, though that will complicate the logic a bit
execution_utils.Exec(['bash', args.exec_file])
def _RunIsInteractive(self, args):
sys.exit(int(not console_io.IsInteractive(heuristic=True)))
def _RunInterrupt(self, args):
try:
# Windows hackery to simulate ^C and wait for it to register.
# NOTICE: This only works if this command is run from the console.
os.kill(os.getpid(), signal.CTRL_C_EVENT)
time.sleep(1)
except AttributeError:
# Back to normal where ^C is SIGINT and it works immediately.
os.kill(os.getpid(), signal.SIGINT)
raise exceptions.Error('SIGINT delivery failed.')
def _RunPromptCompleter(self, args):
completer_class = module_util.ImportModule(args.prompt_completer)
choices = parser_completer.ArgumentCompleter(completer_class, args)
response = console_io.PromptResponse('Complete this: ', choices=choices)
print(response)
def _RunProgressTracker(self, args):
start_time = time.time()
def message_callback():
remaining_time = args.progress_tracker - (time.time() - start_time)
return '{0:.1f}s remaining'.format(remaining_time)
with progress_tracker.ProgressTracker(
message='This is a progress tracker.',
detail_message_callback=message_callback):
time.sleep(args.progress_tracker)
def _RunSleep(self, args):
time.sleep(args.sleep)
def _RunUncaughtException(self, args):
raise ValueError('Catch me if you can.')
def _RunStagedProgressTracker(self, args):
get_bread = progress_tracker.Stage('Getting bread...', key='bread')
get_pb_and_j = progress_tracker.Stage('Getting peanut butter...', key='pb')
make_sandwich = progress_tracker.Stage('Making sandwich...', key='make')
stages = [get_bread, get_pb_and_j, make_sandwich]
with progress_tracker.StagedProgressTracker(
'Making sandwich...',
stages,
success_message='Time to eat!',
failure_message='Time to order delivery..!',
tracker_id='meta.make_sandwich') as tracker:
tracker.StartStage('bread')
time.sleep(0.5)
tracker.UpdateStage('bread', 'Looking for bread in the pantry')
time.sleep(0.5)
tracker.CompleteStage('bread', 'Got some whole wheat bread!')
tracker.StartStage('pb')
time.sleep(1)
tracker.CompleteStage('pb')
tracker.StartStage('make')
time.sleep(1)
tracker.CompleteStage('make')
def Run(self, args):
if args.arg_dict:
r = self._RunArgDict(args)
elif args.arg_list:
r = self._RunArgList(args)
elif args.argumenterror_outside_argparse:
r = self._RunArgumenterrorOutsideArgparse(args)
elif args.core_exception:
self._RunCoreException(args)
r = None
elif args.exec_file:
self._RunExecFile(args)
r = None
elif args.interrupt:
self._RunInterrupt(args)
r = None
elif args.is_interactive:
self._RunIsInteractive(args)
r = None
elif args.prompt_completer:
self._RunPromptCompleter(args)
r = None
elif args.progress_tracker:
self._RunProgressTracker(args)
r = None
elif args.sleep:
self._RunSleep(args)
r = None
elif args.uncaught_exception:
r = self._RunUncaughtException(args)
elif args.staged_progress_tracker:
self._RunStagedProgressTracker(args)
r = None
return r

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A command that validates YAML data against a JSON Schema."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
from googlecloudsdk.core import yaml
from googlecloudsdk.core import yaml_validator
from googlecloudsdk.core.console import console_io
class ValidateYAML(base.Command):
"""Validate a YAML file against a JSON Schema.
{command} validates YAML / JSON files against
[JSON Schemas](https://json-schema.org/).
"""
@staticmethod
def Args(parser):
parser.add_argument(
'schema_file',
help='The path to a file containing the JSON Schema.')
parser.add_argument(
'yaml_file',
help=('The path to a file containing YAML / JSON data. Use `-` for '
'the standard input.'))
def Run(self, args):
contents = console_io.ReadFromFileOrStdin(args.yaml_file, binary=False)
parsed_yaml = yaml.load(contents)
yaml_validator.Validator(args.schema_file).Validate(parsed_yaml)