File: //snap/google-cloud-cli/current/lib/surface/api_gateway/api_configs/create.py
# -*- 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)