diff --git a/contrib/endpoints/include/api_manager/method.h b/contrib/endpoints/include/api_manager/method.h index 6b7c01072b80..1680b6950ffe 100644 --- a/contrib/endpoints/include/api_manager/method.h +++ b/contrib/endpoints/include/api_manager/method.h @@ -89,6 +89,10 @@ class MethodInfo { // Get the names of url system parameters virtual const std::set &system_query_parameter_names() const = 0; + + // Get quota metric cost vector + virtual const std::vector> &metric_cost_vector() + const = 0; }; } // namespace api_manager diff --git a/contrib/endpoints/repositories.bzl b/contrib/endpoints/repositories.bzl index bae14e92d61c..19ab3bf402a5 100644 --- a/contrib/endpoints/repositories.bzl +++ b/contrib/endpoints/repositories.bzl @@ -211,12 +211,9 @@ def googleapis_repositories(protobuf_repo="@protobuf_git//", bind=True): # ################################################################################ # - licenses(["notice"]) -load("{}:protobuf.bzl", "cc_proto_library") - -exports_files(glob(["google/**"])) +load("@protobuf_git//:protobuf.bzl", "cc_proto_library") cc_proto_library( name = "servicecontrol", @@ -259,9 +256,13 @@ cc_proto_library( "google/api/log.proto", "google/api/logging.proto", "google/api/metric.proto", + "google/api/experimental/experimental.proto", + "google/api/experimental/authorization_config.proto", "google/api/monitored_resource.proto", "google/api/monitoring.proto", + "google/api/quota.proto", "google/api/service.proto", + "google/api/source_info.proto", "google/api/system_parameter.proto", "google/api/usage.proto", ], @@ -290,10 +291,9 @@ cc_proto_library( ) """.format(protobuf_repo) - native.new_git_repository( name = "googleapis_git", - commit = "db1d4547dc56a798915e0eb2c795585385922165", + commit = "2fe0050bd2a6d4c6ba798c0311f0b149b8997314", remote = "https://github.com/googleapis/googleapis.git", build_file_content = BUILD, ) @@ -324,7 +324,7 @@ def servicecontrol_client_repositories(bind=True): native.git_repository( name = "servicecontrol_client_git", - commit = "d739d755365c6a13d0b4164506fd593f53932f5d", + commit = "3d1a30d9221e700542eeaaf20eab69faddb63894", remote = "https://github.com/cloudendpoints/service-control-client-cxx.git", ) @@ -333,3 +333,11 @@ def servicecontrol_client_repositories(bind=True): name = "servicecontrol_client", actual = "@servicecontrol_client_git//:service_control_client_lib", ) + native.bind( + name = "quotacontrol", + actual = "@servicecontrol_client_git//proto:quotacontrol", + ) + native.bind( + name = "quotacontrol_genproto", + actual = "@servicecontrol_client_git//proto:quotacontrol_genproto", + ) diff --git a/contrib/endpoints/src/api_manager/BUILD b/contrib/endpoints/src/api_manager/BUILD index e3d54b46eaca..76416aea90e9 100644 --- a/contrib/endpoints/src/api_manager/BUILD +++ b/contrib/endpoints/src/api_manager/BUILD @@ -81,6 +81,8 @@ cc_library( "method_impl.cc", "path_matcher.cc", "path_matcher_node.cc", + "quota_control.cc", + "quota_control.h", "request_handler.cc", ], linkopts = select({ diff --git a/contrib/endpoints/src/api_manager/auth/service_account_token.h b/contrib/endpoints/src/api_manager/auth/service_account_token.h index 211377449f4a..eca3e148f68c 100644 --- a/contrib/endpoints/src/api_manager/auth/service_account_token.h +++ b/contrib/endpoints/src/api_manager/auth/service_account_token.h @@ -64,6 +64,7 @@ class ServiceAccountToken { enum JWT_TOKEN_TYPE { JWT_TOKEN_FOR_SERVICE_CONTROL = 0, JWT_TOKEN_FOR_CLOUD_TRACING, + JWT_TOKEN_FOR_QUOTA_CONTROL, JWT_TOKEN_TYPE_MAX, }; // Set audience. Only calcualtes JWT token with specified audience. diff --git a/contrib/endpoints/src/api_manager/check_workflow.cc b/contrib/endpoints/src/api_manager/check_workflow.cc index 8335d7791425..27b9fba60daf 100644 --- a/contrib/endpoints/src/api_manager/check_workflow.cc +++ b/contrib/endpoints/src/api_manager/check_workflow.cc @@ -18,6 +18,7 @@ #include "contrib/endpoints/src/api_manager/check_auth.h" #include "contrib/endpoints/src/api_manager/check_service_control.h" #include "contrib/endpoints/src/api_manager/fetch_metadata.h" +#include "contrib/endpoints/src/api_manager/quota_control.h" using ::google::api_manager::utils::Status; @@ -33,6 +34,8 @@ void CheckWorkflow::RegisterAll() { Register(CheckAuth); // Checks service control. Register(CheckServiceControl); + // Quota control + Register(QuotaControl); } void CheckWorkflow::Register(CheckHandler handler) { diff --git a/contrib/endpoints/src/api_manager/config.cc b/contrib/endpoints/src/api_manager/config.cc index b0a1b85d1190..bcc0e3babee7 100644 --- a/contrib/endpoints/src/api_manager/config.cc +++ b/contrib/endpoints/src/api_manager/config.cc @@ -113,6 +113,23 @@ MethodInfoImpl *Config::GetOrCreateMethodInfoImpl(const string &name, return i->second.get(); } +bool Config::LoadQuotaRule(ApiManagerEnvInterface *env) { + for (const auto &rule : service_.quota().metric_rules()) { + auto method = utils::FindOrNull(method_map_, rule.selector()); + if (method) { + for (auto &metric_cost : rule.metric_costs()) { + (*method)->add_metric_cost(metric_cost.first, metric_cost.second); + } + } else { + env->LogError("Metric rule with selector " + rule.selector() + + "is mismatched."); + return false; + } + } + + return true; +} + bool Config::LoadHttpMethods(ApiManagerEnvInterface *env, PathMatcherBuilder *pmb) { std::set all_urls, urls_with_options; @@ -443,6 +460,9 @@ std::unique_ptr Config::Create(ApiManagerEnvInterface *env, if (!config->LoadBackends(env)) { return nullptr; } + if (!config->LoadQuotaRule(env)) { + return nullptr; + } return config; } diff --git a/contrib/endpoints/src/api_manager/config.h b/contrib/endpoints/src/api_manager/config.h index 9a56d16d7450..bdff7e088265 100644 --- a/contrib/endpoints/src/api_manager/config.h +++ b/contrib/endpoints/src/api_manager/config.h @@ -25,6 +25,7 @@ #include "contrib/endpoints/src/api_manager/method_impl.h" #include "contrib/endpoints/src/api_manager/path_matcher.h" #include "contrib/endpoints/src/api_manager/proto/server_config.pb.h" +#include "google/api/quota.pb.h" #include "google/api/service.pb.h" namespace google { @@ -111,6 +112,8 @@ class Config { // Load SystemParameters info to MethodInfo. bool LoadSystemParameters(ApiManagerEnvInterface *env); + bool LoadQuotaRule(ApiManagerEnvInterface *env); + // Gets the MethodInfoImpl creating it if necessary MethodInfoImpl *GetOrCreateMethodInfoImpl(const std::string &name, const std::string &api_name, diff --git a/contrib/endpoints/src/api_manager/config_test.cc b/contrib/endpoints/src/api_manager/config_test.cc index ace0d2afc493..b133df422269 100644 --- a/contrib/endpoints/src/api_manager/config_test.cc +++ b/contrib/endpoints/src/api_manager/config_test.cc @@ -870,6 +870,90 @@ TEST(Config, TestCorsDisabled) { ASSERT_EQ(nullptr, method1); } +TEST(Config, TestInvalidMetricRules) { + MockApiManagerEnvironmentWithLog env; + // There is no http.rule or api.method to match the selector. + static const char config_text[] = R"( +name: "Service.Name" +quota { + metric_rules { + selector: "GetShelves" + metric_costs { + key: "test.googleapis.com/operation/read_book" + value: 100 + } + } +} +)"; + + std::unique_ptr config = Config::Create(&env, config_text, ""); + EXPECT_EQ(nullptr, config); +} + +TEST(Config, TestMetricRules) { + MockApiManagerEnvironmentWithLog env; + static const char config_text[] = R"( +name: "Service.Name" +http { + rules { + selector: "DeleteShelf" + delete: "/shelves" + } + rules { + selector: "GetShelves" + get: "/shelves" + } +} +quota { + metric_rules { + selector: "GetShelves" + metric_costs { + key: "test.googleapis.com/operation/get_shelves" + value: 100 + } + metric_costs { + key: "test.googleapis.com/operation/request" + value: 10 + } + } + metric_rules { + selector: "DeleteShelf" + metric_costs { + key: "test.googleapis.com/operation/delete_shelves" + value: 200 + } + } +} +)"; + + std::unique_ptr config = Config::Create(&env, config_text, ""); + ASSERT_TRUE(config); + + const MethodInfo *method1 = config->GetMethodInfo("GET", "/shelves"); + ASSERT_NE(nullptr, method1); + + std::vector> metric_cost_vector = + method1->metric_cost_vector(); + std::sort(metric_cost_vector.begin(), metric_cost_vector.end()); + ASSERT_EQ(2, metric_cost_vector.size()); + ASSERT_EQ("test.googleapis.com/operation/get_shelves", + metric_cost_vector[0].first); + ASSERT_EQ(100, metric_cost_vector[0].second); + + ASSERT_EQ("test.googleapis.com/operation/request", + metric_cost_vector[1].first); + ASSERT_EQ(10, metric_cost_vector[1].second); + + const MethodInfo *method2 = config->GetMethodInfo("DELETE", "/shelves"); + ASSERT_NE(nullptr, method1); + + metric_cost_vector = method2->metric_cost_vector(); + ASSERT_EQ(1, metric_cost_vector.size()); + ASSERT_EQ("test.googleapis.com/operation/delete_shelves", + metric_cost_vector[0].first); + ASSERT_EQ(200, metric_cost_vector[0].second); +} + } // namespace } // namespace api_manager diff --git a/contrib/endpoints/src/api_manager/context/request_context.cc b/contrib/endpoints/src/api_manager/context/request_context.cc index 24508a4a981c..2c0a2985927c 100644 --- a/contrib/endpoints/src/api_manager/context/request_context.cc +++ b/contrib/endpoints/src/api_manager/context/request_context.cc @@ -241,6 +241,16 @@ void RequestContext::FillCheckRequestInfo( request_->FindHeader(kXIosBundleId, &info->ios_bundle_id); } +void RequestContext::FillAllocateQuotaRequestInfo( + service_control::QuotaRequestInfo *info) { + FillOperationInfo(info); + + info->client_ip = request_->GetClientIP(); + info->method_name = this->method_call_.method_info->name(); + info->metric_cost_vector = + &this->method_call_.method_info->metric_cost_vector(); +} + void RequestContext::FillReportRequestInfo( Response *response, service_control::ReportRequestInfo *info) { FillOperationInfo(info); diff --git a/contrib/endpoints/src/api_manager/context/request_context.h b/contrib/endpoints/src/api_manager/context/request_context.h index 5b29c271aad6..8832b38fc3e0 100644 --- a/contrib/endpoints/src/api_manager/context/request_context.h +++ b/contrib/endpoints/src/api_manager/context/request_context.h @@ -66,6 +66,9 @@ class RequestContext { // Fill CheckRequestInfo void FillCheckRequestInfo(service_control::CheckRequestInfo *info); + // FillAllocateQuotaRequestInfo + void FillAllocateQuotaRequestInfo(service_control::QuotaRequestInfo *info); + // Fill ReportRequestInfo void FillReportRequestInfo(Response *response, service_control::ReportRequestInfo *info); diff --git a/contrib/endpoints/src/api_manager/method_impl.h b/contrib/endpoints/src/api_manager/method_impl.h index e5739d639bc3..d6eaba33724b 100644 --- a/contrib/endpoints/src/api_manager/method_impl.h +++ b/contrib/endpoints/src/api_manager/method_impl.h @@ -18,6 +18,7 @@ #include #include #include +#include #include "contrib/endpoints/include/api_manager/method.h" #include "contrib/endpoints/src/api_manager/utils/stl_util.h" @@ -62,6 +63,10 @@ class MethodInfoImpl : public MethodInfo { const std::string &backend_address() const { return backend_address_; } + const std::vector> &metric_cost_vector() const { + return metric_cost_vector_; + } + const std::string &rpc_method_full_name() const { return rpc_method_full_name_; } @@ -90,6 +95,10 @@ class MethodInfoImpl : public MethodInfo { url_query_parameters_[name].push_back(url_query_parameter); } + void add_metric_cost(const std::string &metric, int64_t cost) { + metric_cost_vector_.push_back(std::make_pair(metric, cost)); + } + // After add all system parameters, lookup some of them to cache // their lookup results. void process_system_parameters(); @@ -139,13 +148,13 @@ class MethodInfoImpl : public MethodInfo { // such as API Key)? bool allow_unregistered_calls_; // Issuers to allowed audiences map. - std::map > issuer_audiences_map_; + std::map> issuer_audiences_map_; // system parameter map of parameter name to http_header name. - std::map > http_header_parameters_; + std::map> http_header_parameters_; // system parameter map of parameter name to url query parameter name. - std::map > url_query_parameters_; + std::map> url_query_parameters_; // all the names of system query parameters std::set system_query_parameter_names_; @@ -175,6 +184,9 @@ class MethodInfoImpl : public MethodInfo { // Whether the response is streaming or not. bool response_streaming_; + + // map of metric and its cost + std::vector> metric_cost_vector_; }; typedef std::unique_ptr MethodInfoImplPtr; diff --git a/contrib/endpoints/src/api_manager/mock_method_info.h b/contrib/endpoints/src/api_manager/mock_method_info.h index a7de28e34228..6b78e86228b6 100644 --- a/contrib/endpoints/src/api_manager/mock_method_info.h +++ b/contrib/endpoints/src/api_manager/mock_method_info.h @@ -48,6 +48,8 @@ class MockMethodInfo : public MethodInfo { MOCK_CONST_METHOD0(response_streaming, bool()); MOCK_CONST_METHOD0(system_query_parameter_names, const std::set&()); + MOCK_CONST_METHOD0(metric_cost_vector, + const std::vector>&()); }; } // namespace api_manager diff --git a/contrib/endpoints/src/api_manager/proto/server_config.proto b/contrib/endpoints/src/api_manager/proto/server_config.proto index 1fe04af6d072..7f18460c10b5 100644 --- a/contrib/endpoints/src/api_manager/proto/server_config.proto +++ b/contrib/endpoints/src/api_manager/proto/server_config.proto @@ -66,6 +66,13 @@ message ServiceControlConfig { // The intermediate reports for streaming calls should not be more frequent // than this value (in seconds) int32 intermediate_report_min_interval = 7; + + // Quota aggregator config + QuotaAggregatorConfig quota_aggregator_config = 8; + + // Timeout in milliseconds on service control allocate quota requests. + // If the value is <= 0, default timeout is 5000 milliseconds. + int32 quota_timeout_ms = 9; } // Check aggregator config @@ -82,6 +89,17 @@ message CheckAggregatorConfig { int32 response_expiration_ms = 3; } +// Quota aggregator config +message QuotaAggregatorConfig { + // The maximum number of cache entries that can be kept in the aggregation + // cache. Cache is disabled when entries <= 0. + int32 cache_entries = 1; + + // The maximum milliseconds before aggregated quota requests are refreshed to + // the server. + int32 refresh_interval_ms = 2; +} + // Report aggregator config message ReportAggregatorConfig { // The maximum number of cache entries that can be kept in the aggregation diff --git a/contrib/endpoints/src/api_manager/quota_control.cc b/contrib/endpoints/src/api_manager/quota_control.cc new file mode 100644 index 000000000000..8a5b65322d68 --- /dev/null +++ b/contrib/endpoints/src/api_manager/quota_control.cc @@ -0,0 +1,57 @@ +// Copyright 2017 Google Inc. 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. +// +//////////////////////////////////////////////////////////////////////////////// +// +#include + +#include "contrib/endpoints/src/api_manager/cloud_trace/cloud_trace.h" +#include "contrib/endpoints/src/api_manager/quota_control.h" +#include "google/protobuf/stubs/status.h" + +using ::google::api_manager::utils::Status; +using ::google::protobuf::util::error::Code; + +namespace google { +namespace api_manager { + +void QuotaControl(std::shared_ptr context, + std::function continuation) { + std::shared_ptr trace_span( + CreateSpan(context->cloud_trace(), "QuotaControl")); + + if (context->method()->metric_cost_vector().size() == 0) { + TRACE(trace_span) << "Quota control check is not needed"; + continuation(Status::OK); + return; + } + + service_control::QuotaRequestInfo info; + context->FillAllocateQuotaRequestInfo(&info); + context->service_context()->service_control()->Quota( + info, trace_span.get(), + [context, continuation, trace_span](utils::Status status) { + + TRACE(trace_span) << "Quota service control request returned with " + << "status " << status.ToString(); + + // quota control is using "failed open" policy. If the server is not + // available, allow the request to go. + continuation((status.code() == Code::UNAVAILABLE) ? utils::Status::OK + : status); + }); +} + +} // namespace service_control_client +} // namespace google diff --git a/contrib/endpoints/src/api_manager/quota_control.h b/contrib/endpoints/src/api_manager/quota_control.h new file mode 100644 index 000000000000..e4f94d6ac93c --- /dev/null +++ b/contrib/endpoints/src/api_manager/quota_control.h @@ -0,0 +1,33 @@ +// Copyright 2017 Google Inc. 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. +// +//////////////////////////////////////////////////////////////////////////////// +// +#ifndef API_MANAGER_QUOTA_CONTROL_H_ +#define API_MANAGER_QUOTA_CONTROL_H_ + +#include "contrib/endpoints/include/api_manager/utils/status.h" +#include "contrib/endpoints/src/api_manager/context/request_context.h" + +namespace google { +namespace api_manager { + +// Call service control quota. +void QuotaControl(std::shared_ptr, + std::function); + +} // namespace api_manager +} // namespace google + +#endif // API_MANAGER_QUOTA_CONTROL_H_ diff --git a/contrib/endpoints/src/api_manager/service_control/BUILD b/contrib/endpoints/src/api_manager/service_control/BUILD index 73b722b48089..c0e35adca072 100644 --- a/contrib/endpoints/src/api_manager/service_control/BUILD +++ b/contrib/endpoints/src/api_manager/service_control/BUILD @@ -122,3 +122,16 @@ cc_test( "//external:googletest_main", ], ) + +cc_test( + name = "allocate_quota_response_test", + size = "small", + srcs = [ + "allocate_quota_response_test.cc", + ], + linkstatic = 1, + deps = [ + ":service_control", + "//external:googletest_main", + ], +) diff --git a/contrib/endpoints/src/api_manager/service_control/aggregated.cc b/contrib/endpoints/src/api_manager/service_control/aggregated.cc index e59a69dc8cca..f1ef15e7ee29 100644 --- a/contrib/endpoints/src/api_manager/service_control/aggregated.cc +++ b/contrib/endpoints/src/api_manager/service_control/aggregated.cc @@ -22,6 +22,8 @@ using ::google::api::servicecontrol::v1::CheckRequest; using ::google::api::servicecontrol::v1::CheckResponse; +using ::google::api::servicecontrol::v1::AllocateQuotaRequest; +using ::google::api::servicecontrol::v1::AllocateQuotaResponse; using ::google::api::servicecontrol::v1::ReportRequest; using ::google::api::servicecontrol::v1::ReportResponse; using ::google::api_manager::proto::ServerConfig; @@ -29,6 +31,7 @@ using ::google::api_manager::utils::Status; using ::google::protobuf::util::error::Code; using ::google::service_control_client::CheckAggregationOptions; +using ::google::service_control_client::QuotaAggregationOptions; using ::google::service_control_client::ReportAggregationOptions; using ::google::service_control_client::ServiceControlClient; using ::google::service_control_client::ServiceControlClientOptions; @@ -40,6 +43,9 @@ namespace service_control { namespace { +const int kQuotaAggregationEntries = 10000; +const int kQuotaAggregationRefreshMs = 1000; + // Default config for check aggregator const int kCheckAggregationEntries = 10000; // Check doesn't support quota yet. It is safe to increase @@ -54,6 +60,8 @@ const int kReportAggregationFlushIntervalMs = 1000; // The default connection timeout for check requests. const int kCheckDefaultTimeoutInMs = 5000; +// The default connection timeout for allocate quota requests. +const int kAllocateQuotaDefaultTimeoutInMs = 1000; // The default connection timeout for report requests. const int kReportDefaultTimeoutInMs = 15000; @@ -69,6 +77,10 @@ const char application_proto[] = "application/x-protobuf"; const char servicecontrol_service[] = "/google.api.servicecontrol.v1.ServiceController"; +// The quota_control service name. used for as audience to generate JWT token. +const char quotacontrol_service[] = + "/google.api.servicecontrol.v1.QuotaController"; + // Generates CheckAggregationOptions. CheckAggregationOptions GetCheckAggregationOptions( const ServerConfig* server_config) { @@ -85,6 +97,24 @@ CheckAggregationOptions GetCheckAggregationOptions( kCheckAggregationExpirationMs); } +// Generate QuotaAggregationOptions +QuotaAggregationOptions GetQuotaAggregationOptions( + const ServerConfig* server_config) { + QuotaAggregationOptions option = QuotaAggregationOptions( + kQuotaAggregationEntries, kQuotaAggregationRefreshMs); + + if (server_config && server_config->has_service_control_config() && + server_config->service_control_config().has_quota_aggregator_config()) { + const auto& quota_config = + server_config->service_control_config().quota_aggregator_config(); + + option.num_entries = quota_config.cache_entries(); + option.refresh_interval_ms = quota_config.refresh_interval_ms(); + } + + return option; +} + // Generates ReportAggregationOptions. ReportAggregationOptions GetReportAggregationOptions( const ServerConfig* server_config) { @@ -143,6 +173,9 @@ Aggregated::Aggregated(const ::google::api::Service& service, sa_token_->SetAudience( auth::ServiceAccountToken::JWT_TOKEN_FOR_SERVICE_CONTROL, url_.service_control() + servicecontrol_service); + sa_token_->SetAudience( + auth::ServiceAccountToken::JWT_TOKEN_FOR_QUOTA_CONTROL, + url_.service_control() + quotacontrol_service); } } @@ -171,6 +204,7 @@ Status Aggregated::Init() { // env->StartPeriodicTimer doens't work at constructor. ServiceControlClientOptions options( GetCheckAggregationOptions(server_config_), + GetQuotaAggregationOptions(server_config_), GetReportAggregationOptions(server_config_)); std::stringstream ss; @@ -186,6 +220,11 @@ Status Aggregated::Init() { options.check_transport = [this]( const CheckRequest& request, CheckResponse* response, TransportDoneFunc on_done) { Call(request, response, on_done, nullptr); }; + + options.quota_transport = [this]( + const AllocateQuotaRequest& request, AllocateQuotaResponse* response, + TransportDoneFunc on_done) { Call(request, response, on_done, nullptr); }; + options.report_transport = [this]( const ReportRequest& request, ReportResponse* response, TransportDoneFunc on_done) { Call(request, response, on_done, nullptr); }; @@ -323,6 +362,57 @@ void Aggregated::Check( check_pool_.Free(std::move(request)); } +void Aggregated::Quota(const QuotaRequestInfo& info, + cloud_trace::CloudTraceSpan* parent_span, + std::function on_done) { + std::shared_ptr trace_span( + CreateChildSpan(parent_span, "QuotaServiceControlCache")); + + if (!client_) { + on_done(Status(Code::INTERNAL, "Missing service control client")); + return; + } + + auto request = quota_pool_.Alloc(); + + Status status = + service_control_proto_.FillAllocateQuotaRequest(info, request.get()); + if (!status.ok()) { + on_done(status); + quota_pool_.Free(std::move(request)); + return; + } + + AllocateQuotaResponse* response = new AllocateQuotaResponse(); + + auto quota_on_done = [this, response, on_done, trace_span]( + const ::google::protobuf::util::Status& status) { + TRACE(trace_span) << "AllocateQuotaRequst returned with status: " + << status.ToString(); + + if (status.ok()) { + on_done(Proto::ConvertAllocateQuotaResponse( + *response, service_control_proto_.service_name())); + } else { + on_done(Status(status.error_code(), status.error_message(), + Status::SERVICE_CONTROL)); + } + + delete response; + }; + + client_->Quota(*request, response, quota_on_done, + [trace_span, this](const AllocateQuotaRequest& request, + AllocateQuotaResponse* response, + TransportDoneFunc on_done) { + Call(request, response, on_done, trace_span.get()); + }); + + // There is no reference to request anymore at this point and it is safe to + // free request now. + quota_pool_.Free(std::move(request)); +} + Status Aggregated::GetStatistics(Statistics* esp_stat) const { if (!client_) { return Status(Code::INTERNAL, "Missing service control client"); @@ -347,13 +437,79 @@ Status Aggregated::GetStatistics(Statistics* esp_stat) const { return Status::OK; } +template +const std::string& Aggregated::GetApiReqeustUrl() { + if (typeid(RequestType) == typeid(CheckRequest)) { + return url_.check_url(); + } else if (typeid(RequestType) == typeid(AllocateQuotaRequest)) { + return url_.quota_url(); + } else { + return url_.report_url(); + } +} + +template +int Aggregated::GetHttpRequestTimeout() { + int timeout_ms = 0; + + // Set timeout on the request if it was so configured. + if (typeid(RequestType) == typeid(CheckRequest)) { + timeout_ms = kCheckDefaultTimeoutInMs; + } else if (typeid(RequestType) == typeid(AllocateQuotaRequest)) { + timeout_ms = kAllocateQuotaDefaultTimeoutInMs; + } else { + timeout_ms = kReportDefaultTimeoutInMs; + } + + if (server_config_ != nullptr && + server_config_->has_service_control_config()) { + const auto& config = server_config_->service_control_config(); + if (typeid(RequestType) == typeid(CheckRequest)) { + if (config.check_timeout_ms() > 0) { + timeout_ms = config.check_timeout_ms(); + } + } else if (typeid(RequestType) == typeid(AllocateQuotaRequest)) { + if (config.quota_timeout_ms() > 0) { + timeout_ms = config.quota_timeout_ms(); + } + } else { + if (config.report_timeout_ms() > 0) { + timeout_ms = config.report_timeout_ms(); + } + } + } + + return timeout_ms; +} + +template +const std::string& Aggregated::GetAuthToken() { + if (sa_token_) { + if (typeid(RequestType) == typeid(AllocateQuotaRequest)) { + return sa_token_->GetAuthToken( + auth::ServiceAccountToken::JWT_TOKEN_FOR_QUOTA_CONTROL); + } else { + return sa_token_->GetAuthToken( + auth::ServiceAccountToken::JWT_TOKEN_FOR_SERVICE_CONTROL); + } + } else { + static std::string empty; + return empty; + } +} + template void Aggregated::Call(const RequestType& request, ResponseType* response, TransportDoneFunc on_done, cloud_trace::CloudTraceSpan* parent_span) { std::shared_ptr trace_span( CreateChildSpan(parent_span, "Call ServiceControl server")); - std::unique_ptr http_request(new HTTPRequest([response, on_done, + + const std::string& url = GetApiReqeustUrl(); + TRACE(trace_span) << "Http request URL: " << url; + + std::unique_ptr http_request(new HTTPRequest([url, response, + on_done, trace_span, this]( Status status, std::map&&, std::string&& body) { TRACE(trace_span) << "HTTP response status: " << status.ToString(); @@ -364,9 +520,6 @@ void Aggregated::Call(const RequestType& request, ResponseType* response, Status(Code::INVALID_ARGUMENT, std::string("Invalid response")); } } else { - const std::string& url = typeid(RequestType) == typeid(CheckRequest) - ? url_.check_url() - : url_.report_url(); env_->LogError(std::string("Failed to call ") + url + ", Error: " + status.ToString() + ", Response body: " + body); @@ -384,56 +537,25 @@ void Aggregated::Call(const RequestType& request, ResponseType* response, on_done(status.ToProto()); })); - bool is_check = (typeid(RequestType) == typeid(CheckRequest)); - const std::string& url = is_check ? url_.check_url() : url_.report_url(); - TRACE(trace_span) << "Http request URL: " << url; - std::string request_body; request.SerializeToString(&request_body); - if (!is_check && (request_body.size() > max_report_size_)) { + if ((typeid(RequestType) == typeid(ReportRequest)) && + (request_body.size() > max_report_size_)) { max_report_size_ = request_body.size(); } http_request->set_url(url) .set_method("POST") - .set_auth_token(GetAuthToken()) + .set_auth_token(GetAuthToken()) .set_header("Content-Type", application_proto) .set_body(request_body); - // Set timeout on the request if it was so configured. - if (is_check) { - http_request->set_timeout_ms(kCheckDefaultTimeoutInMs); - } else { - http_request->set_timeout_ms(kReportDefaultTimeoutInMs); - } - if (server_config_ != nullptr && - server_config_->has_service_control_config()) { - const auto& config = server_config_->service_control_config(); - if (is_check) { - if (config.check_timeout_ms() > 0) { - http_request->set_timeout_ms(config.check_timeout_ms()); - } - } else { - if (config.report_timeout_ms() > 0) { - http_request->set_timeout_ms(config.report_timeout_ms()); - } - } - } + http_request->set_timeout_ms(GetHttpRequestTimeout()); env_->RunHTTPRequest(std::move(http_request)); } -const std::string& Aggregated::GetAuthToken() { - if (sa_token_) { - return sa_token_->GetAuthToken( - auth::ServiceAccountToken::JWT_TOKEN_FOR_SERVICE_CONTROL); - } else { - static std::string empty; - return empty; - } -} - Interface* Aggregated::Create(const ::google::api::Service& service, const ServerConfig* server_config, ApiManagerEnvInterface* env, diff --git a/contrib/endpoints/src/api_manager/service_control/aggregated.h b/contrib/endpoints/src/api_manager/service_control/aggregated.h index 27e42833dbe2..759cdbb41fd6 100644 --- a/contrib/endpoints/src/api_manager/service_control/aggregated.h +++ b/contrib/endpoints/src/api_manager/service_control/aggregated.h @@ -23,6 +23,7 @@ #include "contrib/endpoints/src/api_manager/service_control/proto.h" #include "contrib/endpoints/src/api_manager/service_control/url.h" #include "google/api/service.pb.h" +#include "google/api/servicecontrol/v1/quota_controller.pb.h" #include "google/api/servicecontrol/v1/service_controller.pb.h" #include "include/service_control_client.h" @@ -49,6 +50,10 @@ class Aggregated : public Interface { const CheckRequestInfo& info, cloud_trace::CloudTraceSpan* parent_span, std::function on_done); + virtual void Quota(const QuotaRequestInfo& info, + cloud_trace::CloudTraceSpan* parent_span, + std::function on_done); + virtual utils::Status Init(); virtual utils::Status Close(); @@ -111,7 +116,16 @@ class Aggregated : public Interface { ::google::service_control_client::TransportDoneFunc on_done, cloud_trace::CloudTraceSpan* parent_span); - // Gets the auth token to access service control server. + // Returns API request url based on RequestType + template + const std::string& GetApiReqeustUrl(); + + // Returns API request timeout in ms based on RequestType + template + int GetHttpRequestTimeout(); + + // Returns API request auth token based on RequestType + template const std::string& GetAuthToken(); // the sevice config. @@ -134,6 +148,11 @@ class Aggregated : public Interface { // The service control client instance. std::unique_ptr<::google::service_control_client::ServiceControlClient> client_; + + // The protobuf pool to reuse AllocateQuotaRequest protobuf. + ProtoPool<::google::api::servicecontrol::v1::AllocateQuotaRequest> + quota_pool_; + // The protobuf pool to reuse CheckRequest protobuf. ProtoPool<::google::api::servicecontrol::v1::CheckRequest> check_pool_; // The protobuf pool to reuse ReportRequest protobuf. diff --git a/contrib/endpoints/src/api_manager/service_control/aggregated_test.cc b/contrib/endpoints/src/api_manager/service_control/aggregated_test.cc index 5e9ca38a55d3..c51b2b5f6aa1 100644 --- a/contrib/endpoints/src/api_manager/service_control/aggregated_test.cc +++ b/contrib/endpoints/src/api_manager/service_control/aggregated_test.cc @@ -19,16 +19,20 @@ #include "contrib/endpoints/src/api_manager/mock_api_manager_environment.h" #include "contrib/endpoints/src/api_manager/service_control/proto.h" #include "gmock/gmock.h" +#include "google/protobuf/text_format.h" #include "gtest/gtest.h" using ::google::api::servicecontrol::v1::CheckRequest; using ::google::api::servicecontrol::v1::CheckResponse; +using ::google::api::servicecontrol::v1::AllocateQuotaRequest; +using ::google::api::servicecontrol::v1::AllocateQuotaResponse; using ::google::api::servicecontrol::v1::ReportRequest; using ::google::api::servicecontrol::v1::ReportResponse; using ::google::api_manager::utils::Status; using ::google::protobuf::util::error::Code; using ::google::service_control_client::ServiceControlClient; using ::google::service_control_client::TransportCheckFunc; +using ::google::service_control_client::TransportQuotaFunc; using ::google::service_control_client::TransportReportFunc; using ::testing::Return; using ::testing::Invoke; @@ -39,6 +43,39 @@ namespace api_manager { namespace service_control { namespace { + +const char kAllocateQuotaResponse[] = R"( +operation_id: "test_service" +quota_metrics { + metric_name: "serviceruntime.googleapis.com/api/consumer/quota_used_count" + metric_values { + labels { + key: "/quota_name" + value: "metric_first" + } + int64_value: 2 + } + metric_values { + labels { + key: "/quota_name" + value: "metric" + } + int64_value: 1 + } +}service_config_id: "2017-02-08r9" + +)"; + +const char kAllocateQuotaResponseErrorExhausted[] = R"( +operation_id: "test_service" +allocate_errors { + code: RESOURCE_EXHAUSTED + description: "Insufficient tokens for quota group and limit \'apiWriteQpsPerProject_LOW\' of service \'jaebonginternal.sandbox.google.com\', using the limit by ID \'container:1002409420961\'." +} +service_config_id: "2017-02-08r9" + +)"; + void FillOperationInfo(OperationInfo* op) { op->operation_id = "operation_id"; op->operation_name = "operation_name"; @@ -53,6 +90,15 @@ class MockServiceControClient : public ServiceControlClient { CheckResponse*)); MOCK_METHOD4(Check, void(const CheckRequest&, CheckResponse*, DoneCallback, TransportCheckFunc)); + + MOCK_METHOD2(Quota, + ::google::protobuf::util::Status(const AllocateQuotaRequest&, + AllocateQuotaResponse*)); + MOCK_METHOD3(Quota, void(const AllocateQuotaRequest&, AllocateQuotaResponse*, + DoneCallback)); + MOCK_METHOD4(Quota, void(const AllocateQuotaRequest&, AllocateQuotaResponse*, + DoneCallback, TransportQuotaFunc)); + MOCK_METHOD3(Report, void(const ReportRequest&, ReportResponse*, DoneCallback)); MOCK_METHOD2(Report, ::google::protobuf::util::Status(const ReportRequest&, @@ -195,6 +241,91 @@ TEST_F(AggregatedTestWithRealClient, CheckOKTest) { EXPECT_EQ(stat.send_report_operations, 0); } +class QuotaAllocationTestWithRealClient : public ::testing::Test { + public: + void SetUp() { + service_.set_name("test_service"); + service_.mutable_control()->set_environment( + "servicecontrol.googleapis.com"); + env_.reset(new ::testing::NiceMock); + sc_lib_.reset(Aggregated::Create(service_, nullptr, env_.get(), nullptr)); + ASSERT_TRUE((bool)(sc_lib_)); + // This is the call actually creating the client. + sc_lib_->Init(); + + metric_cost_vector_ = {{"metric_first", 1}, {"metric_second", 2}}; + } + + std::string getResponseBody(const char* response) { + AllocateQuotaResponse quota_response; + ::google::protobuf::TextFormat::ParseFromString(response, "a_response); + return quota_response.SerializeAsString(); + } + + void DoRunHTTPRequest(HTTPRequest* request) { + std::map headers; + + AllocateQuotaRequest quota_request; + + ASSERT_TRUE(quota_request.ParseFromString(request->body())); + ASSERT_EQ(quota_request.allocate_operation().quota_metrics_size(), 2); + + std::set> expected_costs = { + {"metric_first", 1}, {"metric_second", 2}}; + std::set> actual_costs; + + for (auto rule : quota_request.allocate_operation().quota_metrics()) { + actual_costs.insert(std::make_pair(rule.metric_name(), + rule.metric_values(0).int64_value())); + } + + ASSERT_EQ(actual_costs, expected_costs); + + request->OnComplete(Status::OK, std::move(headers), + std::move(getResponseBody(kAllocateQuotaResponse))); + } + + void DoRunHTTPRequestAllocationFailed(HTTPRequest* request) { + std::map headers; + + request->OnComplete( + Status::OK, std::move(headers), + std::move(getResponseBody(kAllocateQuotaResponseErrorExhausted))); + } + + ::google::api::Service service_; + std::unique_ptr env_; + std::unique_ptr sc_lib_; + std::vector> metric_cost_vector_; +}; + +TEST_F(QuotaAllocationTestWithRealClient, AllocateQuotaTest) { + EXPECT_CALL(*env_, DoRunHTTPRequest(_)) + .WillOnce( + Invoke(this, &QuotaAllocationTestWithRealClient::DoRunHTTPRequest)); + + QuotaRequestInfo info; + info.metric_cost_vector = &metric_cost_vector_; + + FillOperationInfo(&info); + sc_lib_->Quota(info, nullptr, + [](Status status) { ASSERT_TRUE(status.ok()); }); +} + +TEST_F(QuotaAllocationTestWithRealClient, AllocateQuotaFailedTest) { + EXPECT_CALL(*env_, DoRunHTTPRequest(_)) + .WillOnce(Invoke(this, &QuotaAllocationTestWithRealClient:: + DoRunHTTPRequestAllocationFailed)); + + QuotaRequestInfo info; + info.metric_cost_vector = &metric_cost_vector_; + + FillOperationInfo(&info); + sc_lib_->Quota(info, nullptr, [](Status status) { + ASSERT_TRUE(status.code() == Code::RESOURCE_EXHAUSTED); + }); +} + TEST(AggregatedServiceControlTest, Create) { // Verify that invalid service config yields nullptr. ::google::api::Service diff --git a/contrib/endpoints/src/api_manager/service_control/allocate_quota_response_test.cc b/contrib/endpoints/src/api_manager/service_control/allocate_quota_response_test.cc new file mode 100644 index 000000000000..729a73025278 --- /dev/null +++ b/contrib/endpoints/src/api_manager/service_control/allocate_quota_response_test.cc @@ -0,0 +1,184 @@ +// Copyright 2016 Google Inc. 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. +// +//////////////////////////////////////////////////////////////////////////////// +// +#include "contrib/endpoints/include/api_manager/utils/status.h" +#include "contrib/endpoints/src/api_manager/service_control/proto.h" +#include "gtest/gtest.h" + +namespace gasv1 = ::google::api::servicecontrol::v1; + +using ::google::api::servicecontrol::v1::QuotaError; +using ::google::api_manager::utils::Status; +using ::google::protobuf::util::error::Code; + +namespace google { +namespace api_manager { +namespace service_control { + +namespace { + +Status ConvertAllocateQuotaErrorToStatus(gasv1::QuotaError::Code code, + const char* error_detail, + const char* service_name) { + gasv1::AllocateQuotaResponse response; + gasv1::QuotaError* quota_error = response.add_allocate_errors(); + QuotaRequestInfo info; + quota_error->set_code(code); + quota_error->set_description(error_detail); + return Proto::ConvertAllocateQuotaResponse(response, service_name); +} + +Status ConvertAllocateQuotaErrorToStatus(gasv1::QuotaError::Code code) { + gasv1::AllocateQuotaResponse response; + std::string service_name; + response.add_allocate_errors()->set_code(code); + return Proto::ConvertAllocateQuotaResponse(response, service_name); +} + +} // namespace + +TEST(AllocateQuotaResponseTest, + AbortedWithInvalidArgumentWhenRespIsKeyInvalid) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::API_KEY_INVALID); + EXPECT_EQ(Code::INVALID_ARGUMENT, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithInvalidArgumentWhenRespIsKeyExpired) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::API_KEY_EXPIRED); + EXPECT_EQ(Code::INVALID_ARGUMENT, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithInvalidArgumentWhenRespIsBlockedWithResourceExausted) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::RESOURCE_EXHAUSTED); + EXPECT_EQ(Code::RESOURCE_EXHAUSTED, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithInvalidArgumentWhenRespIsBlockedWithProjectSuspended) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::PROJECT_SUSPENDED); + EXPECT_EQ(Code::PERMISSION_DENIED, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithPermissionDeniedWhenRespIsBlockedWithServiceNotEnabled) { + Status result = ConvertAllocateQuotaErrorToStatus( + QuotaError::SERVICE_NOT_ENABLED, + "API api_xxxx is not enabled for the project.", "api_xxxx"); + EXPECT_EQ(Code::PERMISSION_DENIED, result.code()); + EXPECT_EQ(result.message(), "API api_xxxx is not enabled for the project."); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithPermissionDeniedWhenRespIsBlockedWithBillingNotActivated) { + Status result = ConvertAllocateQuotaErrorToStatus( + QuotaError::BILLING_NOT_ACTIVE, + "API api_xxxx has billing disabled. Please enable it..", "api_xxxx"); + EXPECT_EQ(Code::PERMISSION_DENIED, result.code()); + EXPECT_EQ(result.message(), + "API api_xxxx has billing disabled. Please enable it."); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithPermissionDeniedWhenRespIsBlockedWithIpAddressBlocked) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::IP_ADDRESS_BLOCKED); + EXPECT_EQ(Code::PERMISSION_DENIED, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithPermissionDeniedWhenRespIsBlockedWithRefererBlocked) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::REFERER_BLOCKED); + EXPECT_EQ(Code::PERMISSION_DENIED, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithPermissionDeniedWhenRespIsBlockedWithClientAppBlocked) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::CLIENT_APP_BLOCKED); + EXPECT_EQ(Code::PERMISSION_DENIED, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithPermissionDeniedWhenResponseIsBlockedWithProjectInvalid) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::PROJECT_INVALID); + EXPECT_EQ(Code::INVALID_ARGUMENT, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithPermissionDeniedWhenRespIsBlockedWithProjectDeleted) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::PROJECT_DELETED); + EXPECT_EQ(Code::INVALID_ARGUMENT, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithPermissionDeniedWhenRespIsBlockedWithApiKeyInvalid) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::API_KEY_INVALID); + EXPECT_EQ(Code::INVALID_ARGUMENT, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AbortedWithPermissionDeniedWhenRespIsBlockedWithApiKeyExpiread) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::API_KEY_EXPIRED); + EXPECT_EQ(Code::INVALID_ARGUMENT, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AcceptOKWhenRespIsBlockedWithProjectStatusUnavailable) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::PROJECT_STATUS_UNVAILABLE); + EXPECT_EQ(Code::OK, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AcceptOKWhenRespIsBlockedWithServiceStatusUnavailable) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::SERVICE_STATUS_UNAVAILABLE); + EXPECT_EQ(Code::OK, result.code()); +} + +TEST(AllocateQuotaResponseTest, + AcceptOKWhenRespIsBlockedWithBillingStatusUnavailable) { + Status result = + ConvertAllocateQuotaErrorToStatus(QuotaError::BILLING_STATUS_UNAVAILABLE); + EXPECT_EQ(Code::OK, result.code()); +} + +TEST(AllocateQuotaResponseTest, FailOpenWhenResponseIsUnknownBillingStatus) { + EXPECT_TRUE( + ConvertAllocateQuotaErrorToStatus(QuotaError::BILLING_STATUS_UNAVAILABLE) + .ok()); +} + +TEST(AllocateQuotaResponseTest, FailOpenWhenResponseIsUnknownServiceStatus) { + EXPECT_TRUE( + ConvertAllocateQuotaErrorToStatus(QuotaError::SERVICE_STATUS_UNAVAILABLE) + .ok()); +} + +} // namespace service_control +} // namespace api_manager +} // namespace google diff --git a/contrib/endpoints/src/api_manager/service_control/info.h b/contrib/endpoints/src/api_manager/service_control/info.h index 1a62fdfed135..184d598dae7a 100644 --- a/contrib/endpoints/src/api_manager/service_control/info.h +++ b/contrib/endpoints/src/api_manager/service_control/info.h @@ -17,7 +17,8 @@ #include "google/protobuf/stubs/stringpiece.h" -#include +#include "google/api/quota.pb.h" + #include #include #include @@ -94,6 +95,12 @@ struct CheckResponseInfo { CheckResponseInfo() : is_api_key_valid(true), service_is_activated(true) {} }; +struct QuotaRequestInfo : public OperationInfo { + std::string method_name; + + const std::vector>* metric_cost_vector; +}; + // Information to fill Report request protobuf. struct ReportRequestInfo : public OperationInfo { // The HTTP response code. diff --git a/contrib/endpoints/src/api_manager/service_control/interface.h b/contrib/endpoints/src/api_manager/service_control/interface.h index 708acc56a880..a6188e0f73be 100644 --- a/contrib/endpoints/src/api_manager/service_control/interface.h +++ b/contrib/endpoints/src/api_manager/service_control/interface.h @@ -70,6 +70,17 @@ class Interface { const CheckRequestInfo& info, cloud_trace::CloudTraceSpan* parent_span, std::function on_done) = 0; + // on_done() function will be called once it is completed. + // utils::Status in the on_done callback: + // If status.code is more than 100, it is the HTTP response status + // from the service control server. + // If status code is less than 20, within the ranges defined by + // google/protobuf/stubs/status.h, is from parsing error response + // body. + virtual void Quota(const QuotaRequestInfo& info, + cloud_trace::CloudTraceSpan* parent_span, + std::function on_done) = 0; + // Get statistics of ServiceControl library. virtual utils::Status GetStatistics(Statistics* stat) const = 0; }; diff --git a/contrib/endpoints/src/api_manager/service_control/proto.cc b/contrib/endpoints/src/api_manager/service_control/proto.cc index cfaa9a5bf53b..aa88fb39eb25 100644 --- a/contrib/endpoints/src/api_manager/service_control/proto.cc +++ b/contrib/endpoints/src/api_manager/service_control/proto.cc @@ -30,6 +30,7 @@ #include "utils/distribution_helper.h" using ::google::api::servicecontrol::v1::CheckError; +using ::google::api::servicecontrol::v1::QuotaError; using ::google::api::servicecontrol::v1::CheckRequest; using ::google::api::servicecontrol::v1::CheckResponse; using ::google::api::servicecontrol::v1::Distribution; @@ -49,6 +50,11 @@ namespace google { namespace api_manager { namespace service_control { +const char kConsumerQuotaUsedCount[] = + "serviceruntime.googleapis.com/api/consumer/quota_used_count"; + +const char kQuotaName[] = "/quota_name"; + struct SupportedMetric { const char* name; ::google::api::MetricDescriptor_MetricKind metric_kind; @@ -911,6 +917,62 @@ Proto::Proto(const std::set& logs, service_name_(service_name), service_config_id_(service_config_id) {} +utils::Status Proto::FillAllocateQuotaRequest( + const QuotaRequestInfo& info, + ::google::api::servicecontrol::v1::AllocateQuotaRequest* request) { + ::google::api::servicecontrol::v1::QuotaOperation* operation = + request->mutable_allocate_operation(); + + // service_name + request->set_service_name(service_name_); + // service_config_id + request->set_service_config_id(service_config_id_); + + // allocate_operation.operation_id + if (!info.operation_id.empty()) { + operation->set_operation_id(info.operation_id); + } + // allocate_operation.method_name + if (!info.method_name.empty()) { + operation->set_method_name(info.method_name); + } + // allocate_operation.consumer_id + if (!info.api_key.empty()) { + operation->set_consumer_id(std::string(kConsumerIdApiKey) + + std::string(info.api_key)); + } + + // allocate_operation.quota_mode + operation->set_quota_mode( + ::google::api::servicecontrol::v1::QuotaOperation_QuotaMode:: + QuotaOperation_QuotaMode_NORMAL); + + // allocate_operation.labels + auto* labels = operation->mutable_labels(); + if (!info.client_ip.empty()) { + (*labels)[kServiceControlCallerIp] = info.client_ip; + } + + if (!info.referer.empty()) { + (*labels)[kServiceControlReferer] = info.referer; + } + (*labels)[kServiceControlUserAgent] = kUserAgent; + (*labels)[kServiceControlServiceAgent] = + kServiceAgentPrefix + utils::Version::instance().get(); + + if (info.metric_cost_vector) { + for (auto metric : *info.metric_cost_vector) { + MetricValueSet* value_set = operation->add_quota_metrics(); + value_set->set_metric_name(metric.first); + MetricValue* value = value_set->add_metric_values(); + const auto& cost = metric.second; + value->set_int64_value(cost <= 0 ? 1 : cost); + } + } + + return Status::OK; +} + Status Proto::FillCheckRequest(const CheckRequestInfo& info, CheckRequest* request) { Status status = VerifyRequiredCheckFields(info); @@ -1010,6 +1072,100 @@ Status Proto::FillReportRequest(const ReportRequestInfo& info, return Status::OK; } +Status Proto::ConvertAllocateQuotaResponse( + const ::google::api::servicecontrol::v1::AllocateQuotaResponse& response, + const std::string& service_name) { + // response.operation_id() + if (response.allocate_errors().size() == 0) { + return Status::OK; + } + + const ::google::api::servicecontrol::v1::QuotaError& error = + response.allocate_errors().Get(0); + + switch (error.code()) { + case ::google::api::servicecontrol::v1::QuotaError::UNSPECIFIED: + // This is never used. + break; + + case ::google::api::servicecontrol::v1::QuotaError::RESOURCE_EXHAUSTED: + // Quota allocation failed. + // Same as [google.rpc.Code.RESOURCE_EXHAUSTED][]. + return Status(Code::RESOURCE_EXHAUSTED, "Quota allocation failed."); + + case ::google::api::servicecontrol::v1::QuotaError::PROJECT_SUSPENDED: + // Consumer project has been suspended. + return Status(Code::PERMISSION_DENIED, "Project suspended."); + + case ::google::api::servicecontrol::v1::QuotaError::SERVICE_NOT_ENABLED: + // Consumer has not enabled the service. + return Status(Code::PERMISSION_DENIED, + std::string("API ") + service_name + + " is not enabled for the project."); + + case ::google::api::servicecontrol::v1::QuotaError::BILLING_NOT_ACTIVE: + // Consumer cannot access the service because billing is disabled. + return Status(Code::PERMISSION_DENIED, + std::string("API ") + service_name + + " has billing disabled. Please enable it."); + + case ::google::api::servicecontrol::v1::QuotaError::PROJECT_DELETED: + // Consumer's project has been marked as deleted (soft deletion). + case ::google::api::servicecontrol::v1::QuotaError::PROJECT_INVALID: + // Consumer's project number or ID does not represent a valid project. + return Status(Code::INVALID_ARGUMENT, + "Client project not valid. Please pass a valid project."); + + case ::google::api::servicecontrol::v1::QuotaError::IP_ADDRESS_BLOCKED: + // IP address of the consumer is invalid for the specific consumer + // project. + return Status(Code::PERMISSION_DENIED, "IP address blocked."); + + case ::google::api::servicecontrol::v1::QuotaError::REFERER_BLOCKED: + // Referer address of the consumer request is invalid for the specific + // consumer project. + return Status(Code::PERMISSION_DENIED, "Referer blocked."); + + case ::google::api::servicecontrol::v1::QuotaError::CLIENT_APP_BLOCKED: + // Client application of the consumer request is invalid for the + // specific consumer project. + return Status(Code::PERMISSION_DENIED, "Client app blocked."); + + case ::google::api::servicecontrol::v1::QuotaError::API_KEY_INVALID: + // Specified API key is invalid. + return Status(Code::INVALID_ARGUMENT, + "API key not valid. Please pass a valid API key."); + + case ::google::api::servicecontrol::v1::QuotaError::API_KEY_EXPIRED: + // Specified API Key has expired. + return Status(Code::INVALID_ARGUMENT, + "API key expired. Please renew the API key."); + + case ::google::api::servicecontrol::v1::QuotaError:: + PROJECT_STATUS_UNVAILABLE: + // The backend server for looking up project id/number is unavailable. + case ::google::api::servicecontrol::v1::QuotaError:: + SERVICE_STATUS_UNAVAILABLE: + // The backend server for checking service status is unavailable. + case ::google::api::servicecontrol::v1::QuotaError:: + BILLING_STATUS_UNAVAILABLE: + // The backend server for checking billing status is unavailable. + // Fail open for internal server errors per recommendation + case ::google::api::servicecontrol::v1::QuotaError:: + QUOTA_SYSTEM_UNAVAILABLE: + // The backend server for checking quota limits is unavailable. + return Status::OK; + + default: + return Status( + Code::INTERNAL, + std::string("Request blocked due to unsupported error code: ") + + std::to_string(error.code())); + } + + return Status::OK; +} + Status Proto::ConvertCheckResponse(const CheckResponse& check_response, const std::string& service_name, CheckResponseInfo* check_response_info) { diff --git a/contrib/endpoints/src/api_manager/service_control/proto.h b/contrib/endpoints/src/api_manager/service_control/proto.h index bea2a948b63f..d4fa09592c9c 100644 --- a/contrib/endpoints/src/api_manager/service_control/proto.h +++ b/contrib/endpoints/src/api_manager/service_control/proto.h @@ -19,6 +19,7 @@ #include "contrib/endpoints/src/api_manager/service_control/info.h" #include "google/api/label.pb.h" #include "google/api/metric.pb.h" +#include "google/api/servicecontrol/v1/quota_controller.pb.h" #include "google/api/servicecontrol/v1/service_controller.pb.h" namespace google { @@ -48,6 +49,10 @@ class Proto final { const CheckRequestInfo& info, ::google::api::servicecontrol::v1::CheckRequest* request); + utils::Status FillAllocateQuotaRequest( + const QuotaRequestInfo& info, + ::google::api::servicecontrol::v1::AllocateQuotaRequest* request); + // Fills the CheckRequest protobuf from info. // FillReportRequest function should copy the strings pointed by info. // These buffers may be freed after the FillReportRequest call. @@ -64,6 +69,10 @@ class Proto final { const ::google::api::servicecontrol::v1::CheckResponse& response, const std::string& service_name, CheckResponseInfo* check_response_info); + static utils::Status ConvertAllocateQuotaResponse( + const ::google::api::servicecontrol::v1::AllocateQuotaResponse& response, + const std::string& service_name); + static bool IsMetricSupported(const ::google::api::MetricDescriptor& metric); static bool IsLabelSupported(const ::google::api::LabelDescriptor& label); const std::string& service_name() const { return service_name_; } diff --git a/contrib/endpoints/src/api_manager/service_control/proto_test.cc b/contrib/endpoints/src/api_manager/service_control/proto_test.cc index 0a3f05c3668a..4e2a0aeb61bb 100644 --- a/contrib/endpoints/src/api_manager/service_control/proto_test.cc +++ b/contrib/endpoints/src/api_manager/service_control/proto_test.cc @@ -76,6 +76,12 @@ void FillCheckRequestInfo(CheckRequestInfo* request) { request->referer = "referer"; } +void FillAllocateQuotaRequestInfo(QuotaRequestInfo* request) { + request->client_ip = "1.2.3.4"; + request->referer = "referer"; + request->method_name = "operation_name"; +} + void FillReportRequestInfo(ReportRequestInfo* request) { request->referer = "referer"; request->response_code = 200; @@ -122,6 +128,12 @@ std::string CheckRequestToString(gasv1::CheckRequest* request) { return text; } +std::string AllocateQuotaRequestToString(gasv1::AllocateQuotaRequest* request) { + std::string text; + google::protobuf::TextFormat::PrintToString(*request, &text); + return text; +} + std::string ReportRequestToString(gasv1::ReportRequest* request) { gasv1::Operation* op = request->mutable_operations(0); SetFixTimeStamps(op); @@ -179,6 +191,44 @@ TEST_F(ProtoTest, FillGoodCheckRequestAndroidIosTest) { ASSERT_EQ(expected_text, text); } +TEST_F(ProtoTest, FillGoodAllocateQuotaRequestTest) { + std::vector> metric_cost_vector = { + {"metric_first", 1}, {"metric_second", 2}}; + + google::api_manager::service_control::QuotaRequestInfo info; + info.metric_cost_vector = &metric_cost_vector; + + FillOperationInfo(&info); + FillAllocateQuotaRequestInfo(&info); + + gasv1::AllocateQuotaRequest request; + ASSERT_TRUE(scp_.FillAllocateQuotaRequest(info, &request).ok()); + + std::string text = AllocateQuotaRequestToString(&request); + std::string expected_text = ReadTestBaseline("allocate_quota_request.golden"); + ASSERT_EQ(expected_text, text); +} + +TEST_F(ProtoTest, FillAllocateQuotaRequestNoMethodNameTest) { + std::vector> metric_cost_vector = { + {"metric_first", 1}, {"metric_second", 2}}; + + google::api_manager::service_control::QuotaRequestInfo info; + FillOperationInfo(&info); + info.metric_cost_vector = &metric_cost_vector; + info.client_ip = "1.2.3.4"; + info.referer = "referer"; + info.method_name = ""; + + gasv1::AllocateQuotaRequest request; + ASSERT_TRUE(scp_.FillAllocateQuotaRequest(info, &request).ok()); + + std::string text = AllocateQuotaRequestToString(&request); + std::string expected_text = + ReadTestBaseline("allocate_quota_request_no_method_name.golden"); + ASSERT_EQ(expected_text, text); +} + TEST_F(ProtoTest, FillNoApiKeyCheckRequestTest) { CheckRequestInfo info; info.operation_id = "operation_id"; diff --git a/contrib/endpoints/src/api_manager/service_control/testdata/allocate_quota_request.golden b/contrib/endpoints/src/api_manager/service_control/testdata/allocate_quota_request.golden new file mode 100644 index 000000000000..73a262392388 --- /dev/null +++ b/contrib/endpoints/src/api_manager/service_control/testdata/allocate_quota_request.golden @@ -0,0 +1,36 @@ +service_name: "test_service" +allocate_operation { + operation_id: "operation_id" + method_name: "operation_name" + consumer_id: "api_key:api_key_x" + labels { + key: "servicecontrol.googleapis.com/caller_ip" + value: "1.2.3.4" + } + labels { + key: "servicecontrol.googleapis.com/referer" + value: "referer" + } + labels { + key: "servicecontrol.googleapis.com/service_agent" + value: "ESP/{{service_agent_version}}" + } + labels { + key: "servicecontrol.googleapis.com/user_agent" + value: "ESP" + } + quota_metrics { + metric_name: "metric_first" + metric_values { + int64_value: 1 + } + } + quota_metrics { + metric_name: "metric_second" + metric_values { + int64_value: 2 + } + } + quota_mode: NORMAL +} +service_config_id: "2016-09-19r0" diff --git a/contrib/endpoints/src/api_manager/service_control/testdata/allocate_quota_request_android_ios.golden b/contrib/endpoints/src/api_manager/service_control/testdata/allocate_quota_request_android_ios.golden new file mode 100644 index 000000000000..476bae4e5f5a --- /dev/null +++ b/contrib/endpoints/src/api_manager/service_control/testdata/allocate_quota_request_android_ios.golden @@ -0,0 +1,48 @@ +service_name: "test_service" +allocate_operation { + operation_id: "operation_id" + method_name: "operation_name" + consumer_id: "api_key:api_key_x" + labels { + key: "servicecontrol.googleapis.com/android_cert_fingerprint" + value: "AIzaSyB4Gz8nyaSaWo63IPUcy5d_L8dpKtOTSD0" + } + labels { + key: "servicecontrol.googleapis.com/android_package_name" + value: "com.google.cloud" + } + labels { + key: "servicecontrol.googleapis.com/caller_ip" + value: "1.2.3.4" + } + labels { + key: "servicecontrol.googleapis.com/ios_bundle_id" + value: "5b40ad6af9a806305a0a56d7cb91b82a27c26909" + } + labels { + key: "servicecontrol.googleapis.com/referer" + value: "referer" + } + labels { + key: "servicecontrol.googleapis.com/service_agent" + value: "ESP/{{service_agent_version}}" + } + labels { + key: "servicecontrol.googleapis.com/user_agent" + value: "ESP" + } + quota_metrics { + metric_name: "metric_first" + metric_values { + int64_value: 1 + } + } + quota_metrics { + metric_name: "metric_second" + metric_values { + int64_value: 2 + } + } + quota_mode: NORMAL +} +service_config_id: "2016-09-19r0" diff --git a/contrib/endpoints/src/api_manager/service_control/testdata/allocate_quota_request_no_method_name.golden b/contrib/endpoints/src/api_manager/service_control/testdata/allocate_quota_request_no_method_name.golden new file mode 100644 index 000000000000..34a59f476529 --- /dev/null +++ b/contrib/endpoints/src/api_manager/service_control/testdata/allocate_quota_request_no_method_name.golden @@ -0,0 +1,35 @@ +service_name: "test_service" +allocate_operation { + operation_id: "operation_id" + consumer_id: "api_key:api_key_x" + labels { + key: "servicecontrol.googleapis.com/caller_ip" + value: "1.2.3.4" + } + labels { + key: "servicecontrol.googleapis.com/referer" + value: "referer" + } + labels { + key: "servicecontrol.googleapis.com/service_agent" + value: "ESP/{{service_agent_version}}" + } + labels { + key: "servicecontrol.googleapis.com/user_agent" + value: "ESP" + } + quota_metrics { + metric_name: "metric_first" + metric_values { + int64_value: 1 + } + } + quota_metrics { + metric_name: "metric_second" + metric_values { + int64_value: 2 + } + } + quota_mode: NORMAL +} +service_config_id: "2016-09-19r0" diff --git a/contrib/endpoints/src/api_manager/service_control/url.cc b/contrib/endpoints/src/api_manager/service_control/url.cc index d113d402d1f6..f106ccd40fa9 100644 --- a/contrib/endpoints/src/api_manager/service_control/url.cc +++ b/contrib/endpoints/src/api_manager/service_control/url.cc @@ -27,6 +27,7 @@ namespace { // /v1/services/{service}:report const char v1_services_path[] = "/v1/services/"; const char check_verb[] = ":check"; +const char quota_verb[] = ":allocateQuota"; const char report_verb[] = ":report"; const char http[] = "http://"; const char https[] = "https://"; @@ -66,6 +67,7 @@ Url::Url(const ::google::api::Service* service, std::string path = service_control_ + v1_services_path + service->name(); check_url_ = path + check_verb; report_url_ = path + report_verb; + quota_url_ = path + quota_verb; } } diff --git a/contrib/endpoints/src/api_manager/service_control/url.h b/contrib/endpoints/src/api_manager/service_control/url.h index 4615864bbdd8..fb17aa5e0069 100644 --- a/contrib/endpoints/src/api_manager/service_control/url.h +++ b/contrib/endpoints/src/api_manager/service_control/url.h @@ -31,12 +31,14 @@ class Url { // Pre-computed url for service control. const std::string& service_control() const { return service_control_; } const std::string& check_url() const { return check_url_; } + const std::string& quota_url() const { return quota_url_; } const std::string& report_url() const { return report_url_; } private: // Pre-computed url for service control methods. std::string service_control_; std::string check_url_; + std::string quota_url_; std::string report_url_; }; diff --git a/contrib/endpoints/src/api_manager/service_control/url_test.cc b/contrib/endpoints/src/api_manager/service_control/url_test.cc index afbf48bd1543..8e0bb2d188ad 100644 --- a/contrib/endpoints/src/api_manager/service_control/url_test.cc +++ b/contrib/endpoints/src/api_manager/service_control/url_test.cc @@ -57,6 +57,10 @@ TEST(UrlTest, PrependHttps) { ASSERT_EQ( "https://servicecontrol.googleapis.com/v1/services/https-config:report", url.report_url()); + ASSERT_EQ( + "https://servicecontrol.googleapis.com/v1/services/" + "https-config:allocateQuota", + url.quota_url()); } TEST(UrlTest, ServerControlOverride) { diff --git a/contrib/endpoints/src/grpc/transcoding/transcoder_test.cc b/contrib/endpoints/src/grpc/transcoding/transcoder_test.cc index 86fcc1911c15..a2d09b9a6428 100644 --- a/contrib/endpoints/src/grpc/transcoding/transcoder_test.cc +++ b/contrib/endpoints/src/grpc/transcoding/transcoder_test.cc @@ -90,6 +90,10 @@ class TestMethodInfo : public MethodInfo { return dummy; }; + const std::vector> &metric_cost_vector() const { + return metric_cost_vector_; + } + // Methods that the Transcoder does use const std::string &request_type_url() const { return request_type_url_; } bool request_streaming() const { return request_streaming_; } @@ -104,6 +108,7 @@ class TestMethodInfo : public MethodInfo { bool response_streaming_; std::string body_field_path_; std::string empty_; + std::vector> metric_cost_vector_; }; class TranscoderTest : public ::testing::Test {