318 lines
13 KiB
Python
318 lines
13 KiB
Python
# -*- 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.
|
|
|
|
"""`gcloud api-gateway api-configs create` command."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import os
|
|
import cloudsdk.google.protobuf.descriptor_pb2 as descriptor
|
|
|
|
from googlecloudsdk.api_lib.api_gateway import api_configs as api_configs_client
|
|
from googlecloudsdk.api_lib.api_gateway import apis as apis_client
|
|
from googlecloudsdk.api_lib.api_gateway import base as apigateway_base
|
|
from googlecloudsdk.api_lib.api_gateway import operations as operations_client
|
|
from googlecloudsdk.api_lib.endpoints import services_util as endpoints
|
|
from googlecloudsdk.calliope import arg_parsers
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.calliope import exceptions as calliope_exceptions
|
|
from googlecloudsdk.command_lib.api_gateway import common_flags
|
|
from googlecloudsdk.command_lib.api_gateway import operations_util
|
|
from googlecloudsdk.command_lib.api_gateway import resource_args
|
|
from googlecloudsdk.command_lib.util.args import labels_util
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core.util import http_encoding
|
|
|
|
MAX_SERVICE_CONFIG_ID_LENGTH = 50
|
|
|
|
|
|
@base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA,
|
|
base.ReleaseTrack.GA)
|
|
@base.DefaultUniverseOnly
|
|
class Create(base.CreateCommand):
|
|
"""Add a new config to an API."""
|
|
|
|
detailed_help = {
|
|
'DESCRIPTION':
|
|
"""\
|
|
{description}
|
|
|
|
NOTE: If the specified API does not exist it will be created.""",
|
|
'EXAMPLES':
|
|
"""\
|
|
To create an API config for the API 'my-api' with an OpenAPI spec, run:
|
|
|
|
$ {command} my-config --api=my-api --openapi-spec=path/to/openapi_spec.yaml
|
|
""",
|
|
}
|
|
|
|
@staticmethod
|
|
def Args(parser):
|
|
base.ASYNC_FLAG.AddToParser(parser)
|
|
common_flags.AddDisplayNameArg(parser)
|
|
labels_util.AddCreateLabelsFlags(parser)
|
|
resource_args.AddApiConfigResourceArg(parser, 'created', positional=True)
|
|
common_flags.AddBackendAuthServiceAccountFlag(parser)
|
|
|
|
group = parser.add_group(mutex=True,
|
|
required=True,
|
|
help='Configuration files for the API.')
|
|
group.add_argument(
|
|
'--openapi-spec',
|
|
type=arg_parsers.ArgList(),
|
|
metavar='FILE',
|
|
help=('The OpenAPI specifications containing service '
|
|
'configuration information, and API specification for the gateway'
|
|
'.'))
|
|
|
|
group.add_argument(
|
|
'--grpc-files',
|
|
type=arg_parsers.ArgList(),
|
|
metavar='FILE',
|
|
help=('Files describing the GRPC service. Google Service Configuration '
|
|
'files in JSON or YAML formats as well as Proto '
|
|
'descriptors should be listed.'))
|
|
|
|
def Run(self, args):
|
|
apis = apis_client.ApiClient()
|
|
api_configs = api_configs_client.ApiConfigClient()
|
|
ops = operations_client.OperationsClient()
|
|
|
|
api_config_ref = args.CONCEPTS.api_config.Parse()
|
|
api_ref = api_config_ref.Parent()
|
|
|
|
# Check to see if Api exists, create if not
|
|
if not apis.DoesExist(api_ref):
|
|
res = apis.Create(api_ref)
|
|
operations_util.PrintOperationResult(
|
|
res.name, ops,
|
|
wait_string='Waiting for API [{}] to be created'.format(
|
|
api_ref.Name()))
|
|
|
|
open_api_docs = []
|
|
svc_configs = []
|
|
grpc_svc_defs = []
|
|
# When we add gRPC support back, we can remove the 'hasattr' call.
|
|
if hasattr(args, 'grpc_files') and args.grpc_files:
|
|
args.grpc_files = [f.strip() for f in args.grpc_files]
|
|
svc_configs, grpc_svc_defs = self.__GrpcMessages(args.grpc_files)
|
|
else:
|
|
args.openapi_spec = [f.strip() for f in args.openapi_spec]
|
|
open_api_docs = self.__OpenApiMessage(args.openapi_spec)
|
|
|
|
# Create ApiConfig object.
|
|
# Only piece affected by async right now
|
|
resp = api_configs.Create(api_config_ref,
|
|
labels=args.labels,
|
|
display_name=args.display_name,
|
|
backend_auth=args.backend_auth_service_account,
|
|
managed_service_configs=svc_configs,
|
|
grpc_service_defs=grpc_svc_defs,
|
|
open_api_docs=open_api_docs)
|
|
|
|
wait = 'Waiting for API Config [{0}] to be created for API [{1}]'.format(
|
|
api_config_ref.Name(), api_ref.Name())
|
|
|
|
return operations_util.PrintOperationResult(
|
|
resp.name,
|
|
ops,
|
|
service=api_configs.service,
|
|
wait_string=wait,
|
|
is_async=args.async_)
|
|
|
|
def __OpenApiMessage(self, open_api_specs):
|
|
"""Parses the Open API scoped configuraiton files into their necessary API Gateway message types.
|
|
|
|
Args:
|
|
open_api_specs: Specs to be used with the API Gateway API Configuration
|
|
|
|
Returns:
|
|
List of ApigatewayApiConfigOpenApiDocument messages
|
|
|
|
Raises:
|
|
BadFileException: If there is something wrong with the files
|
|
"""
|
|
messages = apigateway_base.GetMessagesModule()
|
|
config_files = []
|
|
for config_file in open_api_specs:
|
|
config_contents = endpoints.ReadServiceConfigFile(config_file)
|
|
|
|
config_dict = self.__ValidJsonOrYaml(config_file, config_contents)
|
|
if config_dict:
|
|
if 'swagger' in config_dict or 'openapi' in config_dict:
|
|
# Always use YAML for OpenAPI because JSON is a subset of YAML.
|
|
document = self.__MakeApigatewayApiConfigFileMessage(config_contents,
|
|
config_file)
|
|
config_files.append(messages.ApigatewayApiConfigOpenApiDocument(
|
|
document=document))
|
|
else:
|
|
raise calliope_exceptions.BadFileException(
|
|
'The file {} is not a valid OpenAPI configuration file.'
|
|
.format(config_file))
|
|
else:
|
|
raise calliope_exceptions.BadFileException(
|
|
'OpenAPI files should be of JSON or YAML format')
|
|
return config_files
|
|
|
|
def __GrpcMessages(self, files):
|
|
"""Parses the GRPC scoped configuraiton files into their necessary API Gateway message types.
|
|
|
|
Args:
|
|
files: Files to be sent in as managed service configs and GRPC service
|
|
definitions
|
|
|
|
Returns:
|
|
List of ApigatewayApiConfigFileMessage, list of
|
|
ApigatewayApiConfigGrpcServiceDefinition messages
|
|
|
|
Raises:
|
|
BadFileException: If there is something wrong with the files
|
|
"""
|
|
|
|
grpc_service_definitions = []
|
|
service_configs = []
|
|
for config_file in files:
|
|
config_contents = endpoints.ReadServiceConfigFile(config_file)
|
|
config_dict = self.__ValidJsonOrYaml(config_file, config_contents)
|
|
if config_dict:
|
|
if config_dict.get('type') == 'google.api.Service':
|
|
service_configs.append(
|
|
self.__MakeApigatewayApiConfigFileMessage(config_contents,
|
|
config_file))
|
|
else:
|
|
raise calliope_exceptions.BadFileException(
|
|
'The file {} is not a valid api configuration file. The '
|
|
'configuration type is expected to be of "google.api.Service".'.
|
|
format(config_file))
|
|
elif endpoints.IsProtoDescriptor(config_file):
|
|
grpc_service_definitions.append(
|
|
self.__MakeApigatewayApiConfigGrpcServiceDefinitionMessage(
|
|
config_contents, config_file))
|
|
elif endpoints.IsRawProto(config_file):
|
|
raise calliope_exceptions.BadFileException(
|
|
('[{}] cannot be used as it is an uncompiled proto'
|
|
' file. However, uncompiled proto files can be included for'
|
|
' display purposes when compiled as a source for a passed in proto'
|
|
' descriptor.'
|
|
).format(config_file))
|
|
else:
|
|
raise calliope_exceptions.BadFileException(
|
|
('Could not determine the content type of file [{0}]. Supported '
|
|
'extensions are .descriptor .json .pb .yaml and .yml'
|
|
).format(config_file))
|
|
return service_configs, grpc_service_definitions
|
|
|
|
def __ValidJsonOrYaml(self, file_name, file_contents):
|
|
"""Whether or not this is a valid json or yaml file.
|
|
|
|
Args:
|
|
file_name: Name of the file
|
|
file_contents: data for the file
|
|
|
|
Returns:
|
|
Boolean for whether or not this is a JSON or YAML
|
|
|
|
Raises:
|
|
BadFileException: File appears to be json or yaml but cannot be parsed.
|
|
"""
|
|
if endpoints.FilenameMatchesExtension(file_name,
|
|
['.json', '.yaml', '.yml']):
|
|
config_dict = endpoints.LoadJsonOrYaml(file_contents)
|
|
if config_dict:
|
|
return config_dict
|
|
else:
|
|
raise calliope_exceptions.BadFileException(
|
|
'Could not read JSON or YAML from config file '
|
|
'[{0}].'.format(file_name))
|
|
else:
|
|
return False
|
|
|
|
def __MakeApigatewayApiConfigFileMessage(self, file_contents, filename,
|
|
is_binary=False):
|
|
"""Constructs a ConfigFile message from a config file.
|
|
|
|
Args:
|
|
file_contents: The contents of the config file.
|
|
filename: The path to the config file.
|
|
is_binary: If set to true, the file_contents won't be encoded.
|
|
|
|
Returns:
|
|
The constructed ApigatewayApiConfigFile message.
|
|
"""
|
|
|
|
messages = apigateway_base.GetMessagesModule()
|
|
if not is_binary:
|
|
# File is human-readable text, not binary; needs to be encoded.
|
|
file_contents = http_encoding.Encode(file_contents)
|
|
return messages.ApigatewayApiConfigFile(
|
|
contents=file_contents,
|
|
path=os.path.basename(filename),
|
|
)
|
|
|
|
def __MakeApigatewayApiConfigGrpcServiceDefinitionMessage(self,
|
|
proto_desc_contents,
|
|
proto_desc_file):
|
|
"""Constructs a GrpcServiceDefinition message from a proto descriptor and the provided list of input files.
|
|
|
|
Args:
|
|
proto_desc_contents: The contents of the proto descriptor file.
|
|
proto_desc_file: The path to the proto descriptor file.
|
|
|
|
Returns:
|
|
The constructed ApigatewayApiConfigGrpcServiceDefinition message.
|
|
"""
|
|
|
|
messages = apigateway_base.GetMessagesModule()
|
|
fds = descriptor.FileDescriptorSet.FromString(proto_desc_contents)
|
|
proto_desc_dir = os.path.dirname(proto_desc_file)
|
|
grpc_sources = []
|
|
included_source_paths = []
|
|
not_included_source_paths = []
|
|
|
|
# Iterate over the file descriptors dependency files and attempt to resolve
|
|
# the gRPC source proto files from it.
|
|
for file_descriptor in fds.file:
|
|
source_path = os.path.join(proto_desc_dir, file_descriptor.name)
|
|
if os.path.exists(source_path):
|
|
source_contents = endpoints.ReadServiceConfigFile(source_path)
|
|
file = self.__MakeApigatewayApiConfigFileMessage(source_contents,
|
|
source_path)
|
|
included_source_paths.append(source_path)
|
|
grpc_sources.append(file)
|
|
else:
|
|
not_included_source_paths.append(source_path)
|
|
|
|
if not_included_source_paths:
|
|
log.warning('Proto descriptor\'s source protos [{0}] were not found on'
|
|
' the file system and will not be included in the submitted'
|
|
' GRPC service definition. If you meant to include these'
|
|
' files, ensure the proto compiler was invoked in the same'
|
|
' directory where the proto descriptor [{1}] now resides.'.
|
|
format(', '.join(not_included_source_paths), proto_desc_file))
|
|
|
|
# Log which files are being passed in as to ensure the user is informed of
|
|
# all files being passed into the gRPC service definition.
|
|
if included_source_paths:
|
|
log.info('Added the source protos [{0}] to the GRPC service definition'
|
|
' for the provided proto descriptor [{1}].'.
|
|
format(', '.join(included_source_paths), proto_desc_file))
|
|
|
|
file_descriptor_set = self.__MakeApigatewayApiConfigFileMessage(
|
|
proto_desc_contents, proto_desc_file, True)
|
|
return messages.ApigatewayApiConfigGrpcServiceDefinition(
|
|
fileDescriptorSet=file_descriptor_set, source=grpc_sources)
|