diff --git a/.travis.yml b/.travis.yml index 88be42c285b..845ac9bbd8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,27 @@ sudo: required -dist: xenial + +dist: trusty + +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-4.9 + - g++-4.9 + - wget branches: except: - stable -lang: go - -go: - - 1.7.x +language: cpp jdk: - oraclejdk8 env: - - BAZEL_VERSION=0.4.3 - -addons: - apt: - packages: - - wget + - BAZEL_VERSION=0.4.5 cache: directories: @@ -35,12 +37,12 @@ before_install: - sudo dpkg -i bazel_${BAZEL_VERSION}-linux-x86_64.deb - sudo apt-get -f install -qqy uuid-dev - cd ${TRAVIS_BUILD_DIR} - - mv .bazelrc .bazelrc.orig - - cat .bazelrc.travis .bazelrc.orig > .bazelrc + - mv tools/bazel.rc tools/bazel.rc.orig + - cat tools/bazel.rc.travis tools/bazel.rc.orig > tools/bazel.rc script: - script/check-style - - bazel --output_base=${HOME}/bazel/outbase test //... + - CC=/usr/bin/gcc-4.9 CXX=/usr/bin/g++-4.9 bazel --output_base=${HOME}/bazel/outbase test //... notifications: slack: istio-dev:wEEEbaabdP5ieCgDOFetA9nX diff --git a/BUILD b/BUILD index ac06d245289..510620e8246 100644 --- a/BUILD +++ b/BUILD @@ -24,3 +24,7 @@ config_setting( }, visibility = ["//visibility:public"], ) + +load("@io_bazel_rules_go//go:def.bzl", "go_prefix") + +go_prefix("istio.io/proxy") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 936e3c8f91b..1c21f8a4311 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,37 +1,5 @@ -# Contributing guidelines +# Contribution guidelines -So, you want to hack on the Istio Proxy? Yay! - -- [Contributor license agreements](#contributor-license-agreements) -- [Contributing a patch](#contributing-a-patch) - -## Contributor license agreements - -We'd love to accept your patches! Before we can take them, you have to jump a -few legal hurdles. - -Please fill out the [Google CLA](https://cla.developers.google.com). - -Once you are CLA'ed, we'll be able to accept your pull requests. This is necessary -because you own the copyright to your changes, even after your contribution -becomes part of this project. So this agreement simply gives us permission -to use and redistribute your contributions as part of the project. - -***NOTE***: Only original source code from you and other people that have -signed the CLA can be accepted into the repository. This policy does not -apply to [third_party](third_party/) and [vendor](vendor/). - -## Contributing a patch - -If you're working on an existing issue, simply respond to the issue and express -interest in working on it. This helps other people know that the issue is -active, and hopefully prevents duplicated efforts. - -If you want to work on a new idea of relatively small scope: - -1. Submit an issue describing your proposed change to the repo in question. -1. The repo owners will respond to your issue promptly. -1. If your proposed change is accepted, and you haven't already done so, sign a - Contributor License Agreement (see details above). -1. Fork the repo, develop, and test your changes. -1. Submit a pull request. +So, you want to hack on the Istio proxy? Yay! Please refer to Istio's overall +[contribution guidelines](https://github.com/istio/istio/blob/master/CONTRIBUTING.md) +to find out how you can help. diff --git a/Jenkinsfile b/Jenkinsfile index 9c59a146e50..c1aa89e3f25 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ #!groovy -@Library('testutils') +@Library('testutils@stable-3e4d089') import org.istio.testutils.Utilities import org.istio.testutils.GitUtilities @@ -12,20 +12,17 @@ def utils = new Utilities() def bazel = new Bazel() mainFlow(utils) { - pullRequest(utils) { - node { - gitUtils.initialize() - // Proxy does build work correctly with Hazelcast. - // Must use .bazelrc.jenkins - bazel.setVars('', '') - } - - if (utils.runStage('PRESUBMIT')) { - presubmit(gitUtils, bazel) - } - if (utils.runStage('POSTSUBMIT')) { - postsubmit(gitUtils, bazel, utils) - } + node { + gitUtils.initialize() + // Proxy does build work correctly with Hazelcast. + // Must use .bazelrc.jenkins + bazel.setVars('', '') + } + if (utils.runStage('PRESUBMIT')) { + presubmit(gitUtils, bazel) + } + if (utils.runStage('POSTSUBMIT')) { + postsubmit(gitUtils, bazel, utils) } } @@ -62,4 +59,4 @@ def postsubmit(gitUtils, bazel, utils) { utils.publishDockerImages(images, tags, 'release') } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 26a33ca4b89..9c478e2f3f0 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,7 @@ Server Side Features: - *Monitoring & Logging*. The Proxy can report server-side metrics and logs to the Mixer. -To learn more... - -- User guide coming soon! -- [Contributing to the project](./CONTRIBUTING.md) - -### Filing issues - -If you have a question about an Istio proxy or have a problem using one, please -[file an issue](https://github.com/istio/proxy/issues/new). +Please see [istio.io](https://istio.io) +to learn about the overall Istio project and how to get in touch with us. To learn how you can +contribute to any of the Istio components, including the proxy, please +see the Istio [contribution guidelines](https://github.com/istio/istio/blob/master/CONTRIBUTING.md). diff --git a/WORKSPACE b/WORKSPACE index ddfa05d578b..6f387192fb9 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -96,3 +96,19 @@ http_file( url = "https://storage.googleapis.com/istio-build/manager/ubuntu_xenial_debug-" + DEBUG_BASE_IMAGE_SHA + ".tar.gz", sha256 = DEBUG_BASE_IMAGE_SHA, ) + +# Following go repositories are for building go integration test for mixer filter. +git_repository( + name = "io_bazel_rules_go", + commit = "2d9f328a9723baf2d037ba9db28d9d0e30683938", # Apr 6, 2017 (buildifier fix) + remote = "https://github.com/bazelbuild/rules_go.git", +) + +git_repository( + name = "org_pubref_rules_protobuf", + commit = "d42e895387c658eda90276aea018056fcdcb30e4", # Mar 07 2017 (gogo* support) + remote = "https://github.com/pubref/rules_protobuf", +) + +load("//src/envoy/mixer/integration_test:repositories.bzl", "go_mixer_repositories") +go_mixer_repositories() diff --git a/contrib/endpoints/include/api_manager/method.h b/contrib/endpoints/include/api_manager/method.h index 6b7c01072b8..1680b6950ff 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 c77a3f72f58..0d66babfcc8 100644 --- a/contrib/endpoints/repositories.bzl +++ b/contrib/endpoints/repositories.bzl @@ -214,7 +214,7 @@ def googleapis_repositories(protobuf_repo="@protobuf_git//", bind=True): licenses(["notice"]) -load("{}:protobuf.bzl", "cc_proto_library") +load("@protobuf_git//:protobuf.bzl", "cc_proto_library") exports_files(glob(["google/**"])) @@ -254,16 +254,18 @@ cc_proto_library( "google/api/control.proto", "google/api/documentation.proto", "google/api/endpoint.proto", - "google/api/experimental/authorization_config.proto", - "google/api/experimental/experimental.proto", "google/api/http.proto", "google/api/label.proto", "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", ], @@ -292,10 +294,9 @@ cc_proto_library( ) """.format(protobuf_repo) - native.new_git_repository( name = "googleapis_git", - commit = "412867fb105722fb9d2cd9af90af1f8f120de238", + commit = "2fe0050bd2a6d4c6ba798c0311f0b149b8997314", remote = "https://github.com/googleapis/googleapis.git", build_file_content = BUILD, ) @@ -326,7 +327,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", ) @@ -335,3 +336,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 83351e10a55..ef95d32885e 100644 --- a/contrib/endpoints/src/api_manager/BUILD +++ b/contrib/endpoints/src/api_manager/BUILD @@ -96,6 +96,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 3a3453daf91..ef3cabce600 100644 --- a/contrib/endpoints/src/api_manager/auth/service_account_token.h +++ b/contrib/endpoints/src/api_manager/auth/service_account_token.h @@ -68,6 +68,7 @@ class ServiceAccountToken { // JWT token for accessing the http endpoints defined in Firebase Rules. JWT_TOKEN_FOR_AUTHORIZATION_SERVICE, + 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 7c869ab30cc..79cd74953db 100644 --- a/contrib/endpoints/src/api_manager/check_workflow.cc +++ b/contrib/endpoints/src/api_manager/check_workflow.cc @@ -19,6 +19,7 @@ #include "contrib/endpoints/src/api_manager/check_security_rules.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; @@ -36,6 +37,8 @@ void CheckWorkflow::RegisterAll() { Register(CheckServiceControl); // Check Security Rules. Register(CheckSecurityRules); + // 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 66ac9aff1c1..a548d1450b5 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 f7cca3838ff..b5d56d58461 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 { @@ -113,6 +114,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 2302bd5cbf7..98020a79811 100644 --- a/contrib/endpoints/src/api_manager/config_test.cc +++ b/contrib/endpoints/src/api_manager/config_test.cc @@ -941,6 +941,91 @@ TEST(Config, TestFirebaseServerCheckWithoutServiceConfigWithServerConfig) { ASSERT_EQ(config->GetFirebaseServer(), "https://myfirebaseserver.com/"); } + +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 75bcb177f9d..4703e2e5ee7 100644 --- a/contrib/endpoints/src/api_manager/context/request_context.cc +++ b/contrib/endpoints/src/api_manager/context/request_context.cc @@ -51,6 +51,16 @@ const char kDefaultApiKeyQueryName1[] = "key"; const char kDefaultApiKeyQueryName2[] = "api_key"; const char kDefaultApiKeyHeaderName[] = "x-api-key"; +// Header for android package name, used for api key restriction check. +const char kXAndroidPackage[] = "x-android-package"; + +// Header for android certificate fingerprint, used for api key restriction +// check. +const char kXAndroidCert[] = "x-android-cert"; + +// Header for IOS bundle identifier, used for api key restriction check. +const char kXIosBundleId[] = "x-ios-bundle-identifier"; + // Default location const char kDefaultLocation[] = "us-central1"; @@ -71,7 +81,9 @@ RequestContext::RequestContext(std::shared_ptr service_context, std::unique_ptr request) : service_context_(service_context), request_(std::move(request)), - is_first_report_(true) { + is_first_report_(true), + last_request_bytes_(0), + last_response_bytes_(0) { start_time_ = std::chrono::system_clock::now(); last_report_time_ = std::chrono::steady_clock::now(); operation_id_ = GenerateUUID(); @@ -225,6 +237,20 @@ void RequestContext::FillCheckRequestInfo( service_control::CheckRequestInfo *info) { FillOperationInfo(info); info->allow_unregistered_calls = method()->allow_unregistered_calls(); + + request_->FindHeader(kXAndroidPackage, &info->android_package_name); + request_->FindHeader(kXAndroidCert, &info->android_cert_fingerprint); + 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( @@ -243,13 +269,23 @@ void RequestContext::FillReportRequestInfo( info->auth_audience = auth_audience_; if (!info->is_final_report) { - info->request_bytes = request_->GetGrpcRequestBytes(); - info->response_bytes = request_->GetGrpcResponseBytes(); + // Make sure we send delta metrics for intermediate reports. + info->request_bytes = request_->GetGrpcRequestBytes() - last_request_bytes_; + info->response_bytes = + request_->GetGrpcResponseBytes() - last_response_bytes_; + last_request_bytes_ += info->request_bytes; + last_response_bytes_ += info->response_bytes; } else { info->request_size = response->GetRequestSize(); info->response_size = response->GetResponseSize(); - info->request_bytes = info->request_size; - info->response_bytes = info->response_size; + info->request_bytes = info->request_size - last_request_bytes_; + if (info->request_bytes < 0) { + info->request_bytes = 0; + } + info->response_bytes = info->response_size - last_response_bytes_; + if (info->response_bytes < 0) { + info->response_bytes = 0; + } info->streaming_request_message_counts = request_->GetGrpcRequestMessageCounts(); diff --git a/contrib/endpoints/src/api_manager/context/request_context.h b/contrib/endpoints/src/api_manager/context/request_context.h index 57706c27a2e..3fdb15526b8 100644 --- a/contrib/endpoints/src/api_manager/context/request_context.h +++ b/contrib/endpoints/src/api_manager/context/request_context.h @@ -68,6 +68,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); @@ -186,6 +189,10 @@ class RequestContext { // The time point of last intermediate report std::chrono::steady_clock::time_point last_report_time_; + + // The accumulated data sent till last intermediate report + int64_t last_request_bytes_; + int64_t last_response_bytes_; }; } // namespace context diff --git a/contrib/endpoints/src/api_manager/method_impl.h b/contrib/endpoints/src/api_manager/method_impl.h index e5739d639bc..d6eaba33724 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 a7de28e3422..6b78e86228b 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 d3f3189fcf3..5e26613f140 100644 --- a/contrib/endpoints/src/api_manager/proto/server_config.proto +++ b/contrib/endpoints/src/api_manager/proto/server_config.proto @@ -69,6 +69,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 @@ -85,6 +92,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 00000000000..8a5b65322d6 --- /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 00000000000..e4f94d6ac93 --- /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 73b722b4808..c0e35adca07 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 e59a69dc8cc..f1ef15e7ee2 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 27e42833dbe..759cdbb41fd 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 5e9ca38a55d..c51b2b5f6aa 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 00000000000..729a7302527 --- /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 f203057cc9e..184d598dae7 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 @@ -74,6 +75,11 @@ struct CheckRequestInfo : public OperationInfo { // Whether the method allow unregistered calls. bool allow_unregistered_calls; + // used for api key restriction check + std::string android_package_name; + std::string android_cert_fingerprint; + std::string ios_bundle_id; + CheckRequestInfo() : allow_unregistered_calls(false) {} }; @@ -89,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 708acc56a88..a6188e0f73b 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 694f299d22c..aa88fb39eb2 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; @@ -420,6 +426,12 @@ const char kServiceControlServiceAgent[] = const char kServiceControlUserAgent[] = "servicecontrol.googleapis.com/user_agent"; const char kServiceControlPlatform[] = "servicecontrol.googleapis.com/platform"; +const char kServiceControlAndroidPackageName[] = + "servicecontrol.googleapis.com/android_package_name"; +const char kServiceControlAndroidCertFingerprint[] = + "servicecontrol.googleapis.com/android_cert_fingerprint"; +const char kServiceControlIosBundleId[] = + "servicecontrol.googleapis.com/ios_bundle_id"; // User agent label value // The value for kUserAgent should be configured at service control server. @@ -905,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); @@ -928,6 +996,18 @@ Status Proto::FillCheckRequest(const CheckRequestInfo& info, (*labels)[kServiceControlUserAgent] = kUserAgent; (*labels)[kServiceControlServiceAgent] = kServiceAgentPrefix + utils::Version::instance().get(); + + if (!info.android_package_name.empty()) { + (*labels)[kServiceControlAndroidPackageName] = info.android_package_name; + } + if (!info.android_cert_fingerprint.empty()) { + (*labels)[kServiceControlAndroidCertFingerprint] = + info.android_cert_fingerprint; + } + if (!info.ios_bundle_id.empty()) { + (*labels)[kServiceControlIosBundleId] = info.ios_bundle_id; + } + return Status::OK; } @@ -992,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 bea2a948b63..d4fa09592c9 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 609cb54abe7..4e2a0aeb61b 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); @@ -161,6 +173,62 @@ TEST_F(ProtoTest, FillGoodCheckRequestTest) { ASSERT_EQ(expected_text, text); } +TEST_F(ProtoTest, FillGoodCheckRequestAndroidIosTest) { + CheckRequestInfo info; + FillOperationInfo(&info); + FillCheckRequestInfo(&info); + + info.android_package_name = "com.google.cloud"; + info.android_cert_fingerprint = "AIzaSyB4Gz8nyaSaWo63IPUcy5d_L8dpKtOTSD0"; + info.ios_bundle_id = "5b40ad6af9a806305a0a56d7cb91b82a27c26909"; + + gasv1::CheckRequest request; + ASSERT_TRUE(scp_.FillCheckRequest(info, &request).ok()); + + std::string text = CheckRequestToString(&request); + std::string expected_text = + ReadTestBaseline("check_request_android_ios.golden"); + 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 00000000000..73a26239238 --- /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 00000000000..476bae4e5f5 --- /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 00000000000..34a59f47652 --- /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/testdata/check_request_android_ios.golden b/contrib/endpoints/src/api_manager/service_control/testdata/check_request_android_ios.golden new file mode 100644 index 00000000000..c847061da4f --- /dev/null +++ b/contrib/endpoints/src/api_manager/service_control/testdata/check_request_android_ios.golden @@ -0,0 +1,43 @@ +service_name: "test_service" +operation { + operation_id: "operation_id" + operation_name: "operation_name" + consumer_id: "api_key:api_key_x" + start_time { + seconds: 100000 + nanos: 100000 + } + end_time { + seconds: 100000 + nanos: 100000 + } + 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" + } +} +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 d113d402d1f..f106ccd40fa 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 4615864bbdd..fb17aa5e006 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 afbf48bd154..8e0bb2d188a 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/BUILD b/contrib/endpoints/src/grpc/transcoding/BUILD index 894fd27b960..dce09d218c4 100644 --- a/contrib/endpoints/src/grpc/transcoding/BUILD +++ b/contrib/endpoints/src/grpc/transcoding/BUILD @@ -67,6 +67,7 @@ cc_library( "message_stream.h", ], deps = [ + ":transcoder_input_stream", "//external:protobuf", ], ) @@ -125,6 +126,7 @@ cc_library( "message_reader.h", ], deps = [ + ":transcoder_input_stream", "//external:protobuf", ], ) @@ -144,6 +146,17 @@ cc_library( ], ) +cc_library( + name = "transcoder_input_stream", + srcs = [ + "transcoder_input_stream.h", + ], + visibility = ["//visibility:public"], + deps = [ + "@protobuf_git//:protobuf", + ], +) + cc_library( name = "transcoding", srcs = [ @@ -223,6 +236,7 @@ cc_library( srcs = ["test_common.cc"], hdrs = ["test_common.h"], deps = [ + ":transcoder_input_stream", "//external:googletest", "//external:protobuf", "//external:service_config", diff --git a/contrib/endpoints/src/grpc/transcoding/message_reader.cc b/contrib/endpoints/src/grpc/transcoding/message_reader.cc index e5e4aeeae14..23b6a0cbad3 100644 --- a/contrib/endpoints/src/grpc/transcoding/message_reader.cc +++ b/contrib/endpoints/src/grpc/transcoding/message_reader.cc @@ -18,7 +18,6 @@ #include -#include "google/protobuf/io/zero_copy_stream.h" #include "google/protobuf/io/zero_copy_stream_impl.h" namespace google { @@ -29,7 +28,7 @@ namespace transcoding { namespace pb = ::google::protobuf; namespace pbio = ::google::protobuf::io; -MessageReader::MessageReader(pbio::ZeroCopyInputStream* in) +MessageReader::MessageReader(TranscoderInputStream* in) : in_(in), current_message_size_(0), have_current_message_size_(false), @@ -99,7 +98,7 @@ std::unique_ptr MessageReader::NextMessage() { // Check if we have the current message size. If not try to read it. if (!have_current_message_size_) { const size_t kDelimiterSize = 5; - if (in_->ByteCount() < static_cast(kDelimiterSize)) { + if (in_->BytesAvailable() < static_cast(kDelimiterSize)) { // We don't have 5 bytes available to read the length of the message. // Find out whether the stream is finished and return false. finished_ = IsStreamFinished(in_); @@ -117,10 +116,7 @@ std::unique_ptr MessageReader::NextMessage() { have_current_message_size_ = true; } - // We interpret ZeroCopyInputStream::ByteCount() as the number of bytes - // available for reading at the moment. Check if we have the full message - // available to read. - if (in_->ByteCount() < static_cast(current_message_size_)) { + if (in_->BytesAvailable() < static_cast(current_message_size_)) { // We don't have a full message return std::unique_ptr(); } diff --git a/contrib/endpoints/src/grpc/transcoding/message_reader.h b/contrib/endpoints/src/grpc/transcoding/message_reader.h index df175561f47..f07408e23af 100644 --- a/contrib/endpoints/src/grpc/transcoding/message_reader.h +++ b/contrib/endpoints/src/grpc/transcoding/message_reader.h @@ -17,7 +17,7 @@ #include -#include "google/protobuf/io/zero_copy_stream.h" +#include "contrib/endpoints/src/grpc/transcoding/transcoder_input_stream.h" #include "google/protobuf/stubs/status.h" namespace google { @@ -49,11 +49,6 @@ namespace transcoding { // } // } // -// NOTE: MesssageReader assumes that ZeroCopyInputStream::ByteCount() returns -// the number of bytes available to read at the moment. That's what -// MessageReader uses to determine whether there is a complete message -// available or not. -// // NOTE: MessageReader is unable to recognize the case when there is an // incomplete message at the end of the input. The callers will need to // detect it and act appropriately. @@ -64,7 +59,7 @@ namespace transcoding { // class MessageReader { public: - MessageReader(::google::protobuf::io::ZeroCopyInputStream* in); + MessageReader(TranscoderInputStream* in); // If a full message is available, NextMessage() returns a ZeroCopyInputStream // over the message. Otherwise returns nullptr - this might be temporary, the @@ -82,7 +77,7 @@ class MessageReader { bool Finished() const { return finished_; } private: - ::google::protobuf::io::ZeroCopyInputStream* in_; + TranscoderInputStream* in_; // The size of the current message. unsigned int current_message_size_; // Whether we have read the current message size or not diff --git a/contrib/endpoints/src/grpc/transcoding/message_stream.cc b/contrib/endpoints/src/grpc/transcoding/message_stream.cc index 11af66bb170..26a09a23361 100644 --- a/contrib/endpoints/src/grpc/transcoding/message_stream.cc +++ b/contrib/endpoints/src/grpc/transcoding/message_stream.cc @@ -19,7 +19,6 @@ #include #include -#include "google/protobuf/io/zero_copy_stream.h" #include "google/protobuf/io/zero_copy_stream_impl_lite.h" namespace google { @@ -32,12 +31,12 @@ namespace pbio = ::google::protobuf::io; namespace { // a ZeroCopyInputStream implementation over a MessageStream implementation -class ZeroCopyStreamOverMessageStream : public pbio::ZeroCopyInputStream { +class InputStreamOverMessageStream : public TranscoderInputStream { public: - // src - the underlying MessageStream. ZeroCopyStreamOverMessageStream doesn't + // src - the underlying MessageStream. InputStreamOverMessageStream doesn't // maintain the ownership of src, the caller must make sure it exists - // throughtout the lifetime of ZeroCopyStreamOverMessageStream. - ZeroCopyStreamOverMessageStream(MessageStream* src) + // throughtout the lifetime of InputStreamOverMessageStream. + InputStreamOverMessageStream(MessageStream* src) : src_(src), message_(), position_(0) {} // ZeroCopyInputStream implementation @@ -72,19 +71,15 @@ class ZeroCopyStreamOverMessageStream : public pbio::ZeroCopyInputStream { bool Skip(int) { return false; } // Not implemented (no need) - ::google::protobuf::int64 ByteCount() const { - // NOTE: we are changing the ByteCount() interpretation. In our case - // ByteCount() returns the number of bytes available for reading at this - // moment. In the original interpretation it is supposed to be the number - // of bytes read so far. - // We need this such that the consumers are able to read the gRPC delimited - // message stream only if there is a full message available. + google::protobuf::int64 ByteCount() const { return 0; } // Not implemented + + int64_t BytesAvailable() const { if (position_ >= message_.size()) { // If the current message is all done, try to read the next message // to make sure we return the correct byte count. - const_cast(this)->ReadNextMessage(); + const_cast(this)->ReadNextMessage(); } - return static_cast<::google::protobuf::int64>(message_.size() - position_); + return static_cast(message_.size() - position_); } private: @@ -109,10 +104,9 @@ class ZeroCopyStreamOverMessageStream : public pbio::ZeroCopyInputStream { } // namespace -std::unique_ptr<::google::protobuf::io::ZeroCopyInputStream> -MessageStream::CreateZeroCopyInputStream() { - return std::unique_ptr<::google::protobuf::io::ZeroCopyInputStream>( - new ZeroCopyStreamOverMessageStream(this)); +std::unique_ptr MessageStream::CreateInputStream() { + return std::unique_ptr( + new InputStreamOverMessageStream(this)); } } // namespace transcoding diff --git a/contrib/endpoints/src/grpc/transcoding/message_stream.h b/contrib/endpoints/src/grpc/transcoding/message_stream.h index 435040332a7..aef90f524f6 100644 --- a/contrib/endpoints/src/grpc/transcoding/message_stream.h +++ b/contrib/endpoints/src/grpc/transcoding/message_stream.h @@ -18,6 +18,7 @@ #include #include +#include "contrib/endpoints/src/grpc/transcoding/transcoder_input_stream.h" #include "google/protobuf/io/zero_copy_stream.h" #include "google/protobuf/stubs/status.h" @@ -73,8 +74,7 @@ class MessageStream { // Virtual destructor virtual ~MessageStream() {} // Creates ZeroCopyInputStream implementation based on this stream - std::unique_ptr<::google::protobuf::io::ZeroCopyInputStream> - CreateZeroCopyInputStream(); + std::unique_ptr CreateInputStream(); }; } // namespace transcoding diff --git a/contrib/endpoints/src/grpc/transcoding/message_stream_test.cc b/contrib/endpoints/src/grpc/transcoding/message_stream_test.cc index 3699b499bfc..81276310f01 100644 --- a/contrib/endpoints/src/grpc/transcoding/message_stream_test.cc +++ b/contrib/endpoints/src/grpc/transcoding/message_stream_test.cc @@ -70,14 +70,14 @@ class ZeroCopyInputStreamOverMessageStreamTest : public ::testing::Test { bool Test(const Messages& messages) { TestMessageStream test_message_stream; - auto zero_copy_stream = test_message_stream.CreateZeroCopyInputStream(); + auto input_stream = test_message_stream.CreateInputStream(); const void* data = nullptr; int size = 0; // Check that Next() returns true and a 0-sized buffer meaning that // nothing is available at the moment. - if (!zero_copy_stream->Next(&data, &size)) { + if (!input_stream->Next(&data, &size)) { ADD_FAILURE() << "The stream finished unexpectedly" << std::endl; return false; } @@ -91,13 +91,13 @@ class ZeroCopyInputStreamOverMessageStreamTest : public ::testing::Test { test_message_stream.AddMessage(message); // message.size() bytes must be available for reading - if (static_cast(message.size()) != zero_copy_stream->ByteCount()) { - EXPECT_EQ(message.size(), zero_copy_stream->ByteCount()); + if (static_cast(message.size()) != input_stream->BytesAvailable()) { + EXPECT_EQ(message.size(), input_stream->BytesAvailable()); return false; } // Now try to read & match the message - if (!zero_copy_stream->Next(&data, &size)) { + if (!input_stream->Next(&data, &size)) { ADD_FAILURE() << "The stream finished unexpectedly" << std::endl; return false; } @@ -120,16 +120,16 @@ class ZeroCopyInputStreamOverMessageStreamTest : public ::testing::Test { // Not a valid test case continue; } - zero_copy_stream->BackUp(backup_size); + input_stream->BackUp(backup_size); // backup_size bytes must be available for reading again - if (static_cast(backup_size) != zero_copy_stream->ByteCount()) { - EXPECT_EQ(message.size(), zero_copy_stream->ByteCount()); + if (static_cast(backup_size) != input_stream->BytesAvailable()) { + EXPECT_EQ(message.size(), input_stream->BytesAvailable()); return false; } // Now Next() must return the backed up data again. - if (!zero_copy_stream->Next(&data, &size)) { + if (!input_stream->Next(&data, &size)) { ADD_FAILURE() << "The stream finished unexpectedly" << std::endl; return false; } @@ -143,7 +143,7 @@ class ZeroCopyInputStreamOverMessageStreamTest : public ::testing::Test { } // At this point no data should be available - if (!zero_copy_stream->Next(&data, &size)) { + if (!input_stream->Next(&data, &size)) { ADD_FAILURE() << "The stream finished unexpectedly" << std::endl; return false; } @@ -156,7 +156,7 @@ class ZeroCopyInputStreamOverMessageStreamTest : public ::testing::Test { // Now finish the MessageStream & make sure the ZeroCopyInputStream has // ended. test_message_stream.Finish(); - if (zero_copy_stream->Next(&data, &size)) { + if (input_stream->Next(&data, &size)) { ADD_FAILURE() << "The stream still hasn't finished" << std::endl; return false; } @@ -201,14 +201,14 @@ TEST_F(ZeroCopyInputStreamOverMessageStreamTest, DifferenteSizesOneStream) { TEST_F(ZeroCopyInputStreamOverMessageStreamTest, DirectTest) { TestMessageStream test_message_stream; - auto zero_copy_stream = test_message_stream.CreateZeroCopyInputStream(); + auto input_stream = test_message_stream.CreateInputStream(); const void* data = nullptr; int size = 0; // Check that Next() returns true and a 0-sized buffer meaning that // nothing is available at the moment. - EXPECT_TRUE(zero_copy_stream->Next(&data, &size)); + EXPECT_TRUE(input_stream->Next(&data, &size)); EXPECT_EQ(0, size); // Test messages @@ -221,16 +221,16 @@ TEST_F(ZeroCopyInputStreamOverMessageStreamTest, DirectTest) { test_message_stream.AddMessage(message1); // message1 is available for reading - EXPECT_EQ(message1.size(), zero_copy_stream->ByteCount()); - EXPECT_TRUE(zero_copy_stream->Next(&data, &size)); + EXPECT_EQ(message1.size(), input_stream->BytesAvailable()); + EXPECT_TRUE(input_stream->Next(&data, &size)); EXPECT_EQ(message1, std::string(reinterpret_cast(data), size)); // Back up a bit - zero_copy_stream->BackUp(5); + input_stream->BackUp(5); // Now read the backed up data again - EXPECT_EQ(5, zero_copy_stream->ByteCount()); - EXPECT_TRUE(zero_copy_stream->Next(&data, &size)); + EXPECT_EQ(5, input_stream->BytesAvailable()); + EXPECT_TRUE(input_stream->Next(&data, &size)); EXPECT_EQ(message1.substr(message1.size() - 5), std::string(reinterpret_cast(data), size)); @@ -238,20 +238,20 @@ TEST_F(ZeroCopyInputStreamOverMessageStreamTest, DirectTest) { test_message_stream.AddMessage(message2); // message2 is available for reading - EXPECT_EQ(message2.size(), zero_copy_stream->ByteCount()); - EXPECT_TRUE(zero_copy_stream->Next(&data, &size)); + EXPECT_EQ(message2.size(), input_stream->BytesAvailable()); + EXPECT_TRUE(input_stream->Next(&data, &size)); EXPECT_EQ(message2, std::string(reinterpret_cast(data), size)); // Back up all of message2 - zero_copy_stream->BackUp(message2.size()); + input_stream->BackUp(message2.size()); // Now read message2 again - EXPECT_EQ(message2.size(), zero_copy_stream->ByteCount()); - EXPECT_TRUE(zero_copy_stream->Next(&data, &size)); + EXPECT_EQ(message2.size(), input_stream->BytesAvailable()); + EXPECT_TRUE(input_stream->Next(&data, &size)); EXPECT_EQ(message2, std::string(reinterpret_cast(data), size)); // At this point no data should be available - EXPECT_TRUE(zero_copy_stream->Next(&data, &size)); + EXPECT_TRUE(input_stream->Next(&data, &size)); EXPECT_EQ(0, size); // Add both message3 & message4 & finish the MessageStream afterwards @@ -260,16 +260,16 @@ TEST_F(ZeroCopyInputStreamOverMessageStreamTest, DirectTest) { test_message_stream.Finish(); // Read & match both message3 & message4 - EXPECT_EQ(message3.size(), zero_copy_stream->ByteCount()); - EXPECT_TRUE(zero_copy_stream->Next(&data, &size)); + EXPECT_EQ(message3.size(), input_stream->BytesAvailable()); + EXPECT_TRUE(input_stream->Next(&data, &size)); EXPECT_EQ(message3, std::string(reinterpret_cast(data), size)); - EXPECT_EQ(message4.size(), zero_copy_stream->ByteCount()); - EXPECT_TRUE(zero_copy_stream->Next(&data, &size)); + EXPECT_EQ(message4.size(), input_stream->BytesAvailable()); + EXPECT_TRUE(input_stream->Next(&data, &size)); EXPECT_EQ(message4, std::string(reinterpret_cast(data), size)); // All done! - EXPECT_FALSE(zero_copy_stream->Next(&data, &size)); + EXPECT_FALSE(input_stream->Next(&data, &size)); } } // namespace diff --git a/contrib/endpoints/src/grpc/transcoding/response_to_json_translator.cc b/contrib/endpoints/src/grpc/transcoding/response_to_json_translator.cc index eb5e54f9bec..8354cc07f25 100644 --- a/contrib/endpoints/src/grpc/transcoding/response_to_json_translator.cc +++ b/contrib/endpoints/src/grpc/transcoding/response_to_json_translator.cc @@ -18,7 +18,6 @@ #include -#include "google/protobuf/io/zero_copy_stream.h" #include "google/protobuf/io/zero_copy_stream_impl_lite.h" #include "google/protobuf/stubs/status.h" #include "google/protobuf/util/json_util.h" @@ -31,7 +30,7 @@ namespace transcoding { ResponseToJsonTranslator::ResponseToJsonTranslator( ::google::protobuf::util::TypeResolver* type_resolver, std::string type_url, - bool streaming, ::google::protobuf::io::ZeroCopyInputStream* in) + bool streaming, TranscoderInputStream* in) : type_resolver_(type_resolver), type_url_(std::move(type_url)), streaming_(streaming), diff --git a/contrib/endpoints/src/grpc/transcoding/response_to_json_translator.h b/contrib/endpoints/src/grpc/transcoding/response_to_json_translator.h index e41791467f8..d674d71d258 100644 --- a/contrib/endpoints/src/grpc/transcoding/response_to_json_translator.h +++ b/contrib/endpoints/src/grpc/transcoding/response_to_json_translator.h @@ -66,8 +66,7 @@ class ResponseToJsonTranslator : public MessageStream { // format (http://www.grpc.io/docs/guides/wire.html) ResponseToJsonTranslator( ::google::protobuf::util::TypeResolver* type_resolver, - std::string type_url, bool streaming, - ::google::protobuf::io::ZeroCopyInputStream* in); + std::string type_url, bool streaming, TranscoderInputStream* in); // MessageStream implementation bool NextMessage(std::string* message); diff --git a/contrib/endpoints/src/grpc/transcoding/test_common.cc b/contrib/endpoints/src/grpc/transcoding/test_common.cc index 269bc1172d7..12a278dbfa3 100644 --- a/contrib/endpoints/src/grpc/transcoding/test_common.cc +++ b/contrib/endpoints/src/grpc/transcoding/test_common.cc @@ -81,7 +81,7 @@ void TestZeroCopyInputStream::BackUp(int count) { position_ -= count; } -pb::int64 TestZeroCopyInputStream::ByteCount() const { +int64_t TestZeroCopyInputStream::BytesAvailable() const { auto total = current_.size() - position_; for (auto chunk : chunks_) { total += chunk.size(); diff --git a/contrib/endpoints/src/grpc/transcoding/test_common.h b/contrib/endpoints/src/grpc/transcoding/test_common.h index bc452e052fc..b6f2532b867 100644 --- a/contrib/endpoints/src/grpc/transcoding/test_common.h +++ b/contrib/endpoints/src/grpc/transcoding/test_common.h @@ -20,6 +20,7 @@ #include #include +#include "contrib/endpoints/src/grpc/transcoding/transcoder_input_stream.h" #include "google/api/service.pb.h" #include "google/protobuf/io/zero_copy_stream.h" #include "google/protobuf/text_format.h" @@ -33,8 +34,7 @@ namespace testing { // An implementation of ZeroCopyInputStream for testing. // The tests define the chunks that TestZeroCopyInputStream produces. -class TestZeroCopyInputStream - : public ::google::protobuf::io::ZeroCopyInputStream { +class TestZeroCopyInputStream : public TranscoderInputStream { public: TestZeroCopyInputStream(); @@ -50,8 +50,9 @@ class TestZeroCopyInputStream // ZeroCopyInputStream methods bool Next(const void** data, int* size); void BackUp(int count); - ::google::protobuf::int64 ByteCount() const; - bool Skip(int) { return false; } // Not implemented + int64_t BytesAvailable() const; + ::google::protobuf::int64 ByteCount() const { return 0; } // Not implemented + bool Skip(int) { return false; } // Not implemented private: std::deque chunks_; diff --git a/contrib/endpoints/src/grpc/transcoding/transcoder.h b/contrib/endpoints/src/grpc/transcoding/transcoder.h index da58386089d..806c519f921 100644 --- a/contrib/endpoints/src/grpc/transcoding/transcoder.h +++ b/contrib/endpoints/src/grpc/transcoding/transcoder.h @@ -15,7 +15,7 @@ #ifndef GRPC_TRANSCODING_TRANSCODER_H_ #define GRPC_TRANSCODING_TRANSCODER_H_ -#include "google/protobuf/io/zero_copy_stream.h" +#include "contrib/endpoints/src/grpc/transcoding/transcoder_input_stream.h" #include "google/protobuf/stubs/status.h" namespace google { @@ -28,11 +28,11 @@ namespace transcoding { // - translated response stream, // - status of response translation. // -// NOTE: Transcoder uses ::google::protobuf::io::ZeroCopyInputStream for -// carrying the payloads both for input and output. It assumes the -// following interpretation of the ZeroCopyInputStream interface: +// NOTE: Transcoder uses TranscoderInputStream for carrying the payloads +// both for input and output. It assumes the following interpretation +// of the TranscoderInputStream interface: // -// bool ZeroCopyInputStream::Next(const void** data, int* size); +// bool TranscoderInputStream::Next(const void** data, int* size); // // Obtains a chunk of data from the stream. // @@ -52,7 +52,7 @@ namespace transcoding { // again later. // // -// void ZeroCopyInputStream::BackUp(int count); +// void TranscoderInputStream::BackUp(int count); // // Backs up a number of bytes, so that the next call to Next() returns // data again that was already returned by the last call to Next(). This @@ -72,12 +72,12 @@ namespace transcoding { // the same data again before producing new data. // // -// bool ZeroCopyInputStream::Skip(int count); +// bool TranscoderInputStream::Skip(int count); // // Not used and not implemented by the Transcoder. // // -// int64 ZeroCopyInputStream::ByteCount() const; +// int64_t TranscoderInputStream::BytesAvailable() const; // // Returns the number of bytes available for reading at this moment // @@ -133,7 +133,7 @@ namespace transcoding { class Transcoder { public: // ZeroCopyInputStream to read the transcoded request. - virtual ::google::protobuf::io::ZeroCopyInputStream* RequestOutput() = 0; + virtual TranscoderInputStream* RequestOutput() = 0; // The status of request transcoding virtual ::google::protobuf::util::Status RequestStatus() = 0; diff --git a/contrib/endpoints/src/grpc/transcoding/transcoder_factory.cc b/contrib/endpoints/src/grpc/transcoding/transcoder_factory.cc index 2dfcf511a17..f42b6d23108 100644 --- a/contrib/endpoints/src/grpc/transcoding/transcoder_factory.cc +++ b/contrib/endpoints/src/grpc/transcoding/transcoder_factory.cc @@ -52,29 +52,23 @@ class TranscoderImpl : public Transcoder { std::unique_ptr response_translator) : request_translator_(std::move(request_translator)), response_translator_(std::move(response_translator)), - request_zero_copy_stream_( - request_translator_->Output().CreateZeroCopyInputStream()), - response_zero_copy_stream_( - response_translator_->CreateZeroCopyInputStream()) {} + request_stream_(request_translator_->Output().CreateInputStream()), + response_stream_(response_translator_->CreateInputStream()) {} // Transcoder implementation - pbio::ZeroCopyInputStream* RequestOutput() { - return request_zero_copy_stream_.get(); - } + TranscoderInputStream* RequestOutput() { return request_stream_.get(); } pbutil::Status RequestStatus() { return request_translator_->Output().Status(); } - pbio::ZeroCopyInputStream* ResponseOutput() { - return response_zero_copy_stream_.get(); - } + pbio::ZeroCopyInputStream* ResponseOutput() { return response_stream_.get(); } pbutil::Status ResponseStatus() { return response_translator_->Status(); } private: std::unique_ptr request_translator_; std::unique_ptr response_translator_; - std::unique_ptr request_zero_copy_stream_; - std::unique_ptr response_zero_copy_stream_; + std::unique_ptr request_stream_; + std::unique_ptr response_stream_; }; // Converts MethodCallInfo into a RequestInfo structure needed by the @@ -132,7 +126,7 @@ TranscoderFactory::TranscoderFactory(const ::google::api::Service& service) pbutil::Status TranscoderFactory::Create( const MethodCallInfo& call_info, pbio::ZeroCopyInputStream* request_input, - pbio::ZeroCopyInputStream* response_input, + TranscoderInputStream* response_input, std::unique_ptr* transcoder) { // Convert MethodCallInfo into RequestInfo RequestInfo request_info; diff --git a/contrib/endpoints/src/grpc/transcoding/transcoder_factory.h b/contrib/endpoints/src/grpc/transcoding/transcoder_factory.h index b7c10865025..d80c93c7f86 100644 --- a/contrib/endpoints/src/grpc/transcoding/transcoder_factory.h +++ b/contrib/endpoints/src/grpc/transcoding/transcoder_factory.h @@ -19,6 +19,7 @@ #include "contrib/endpoints/include/api_manager/method_call_info.h" #include "contrib/endpoints/src/grpc/transcoding/transcoder.h" +#include "contrib/endpoints/src/grpc/transcoding/transcoder_input_stream.h" #include "contrib/endpoints/src/grpc/transcoding/type_helper.h" #include "google/api/service.pb.h" #include "google/protobuf/io/zero_copy_stream.h" @@ -71,7 +72,7 @@ class TranscoderFactory { ::google::protobuf::util::Status Create( const MethodCallInfo& call_info, ::google::protobuf::io::ZeroCopyInputStream* request_input, - ::google::protobuf::io::ZeroCopyInputStream* response_input, + TranscoderInputStream* response_input, std::unique_ptr* transcoder); private: diff --git a/contrib/endpoints/src/grpc/transcoding/transcoder_input_stream.h b/contrib/endpoints/src/grpc/transcoding/transcoder_input_stream.h new file mode 100644 index 00000000000..240067d600b --- /dev/null +++ b/contrib/endpoints/src/grpc/transcoding/transcoder_input_stream.h @@ -0,0 +1,35 @@ +/* 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. + */ +#ifndef GRPC_TRANSCODING_TRANSCODER_INPUT_STREAM_H_ +#define GRPC_TRANSCODING_TRANSCODER_INPUT_STREAM_H_ + +#include "google/protobuf/io/zero_copy_stream.h" + +namespace google { +namespace api_manager { +namespace transcoding { + +class TranscoderInputStream + : public virtual google::protobuf::io::ZeroCopyInputStream { + public: + // returns the number of bytes available to read at the moment. + virtual int64_t BytesAvailable() const = 0; +}; + +} // namespace transcoding +} // namespace api_manager +} // namespace google + +#endif // API_MANAGER_TRANSCODER_H_ diff --git a/contrib/endpoints/src/grpc/transcoding/transcoder_test.cc b/contrib/endpoints/src/grpc/transcoding/transcoder_test.cc index eb2a38eaf5d..a2d09b9a642 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 { @@ -139,7 +144,7 @@ class TranscoderTest : public ::testing::Test { } pbutil::Status Build(pbio::ZeroCopyInputStream *request_input, - pbio::ZeroCopyInputStream *response_input, + TranscoderInputStream *response_input, std::unique_ptr *transcoder) { MethodCallInfo call_info; call_info.method_info = method_info_.get(); diff --git a/src/envoy/BUILD b/src/envoy/BUILD index 1c75af50a75..e69de29bb2d 100644 --- a/src/envoy/BUILD +++ b/src/envoy/BUILD @@ -1,34 +0,0 @@ -# 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. -# -################################################################################ -# -cc_test( - name = "envoy_test", - data = [ - "@envoy_git//:envoy-testdata", - ], - copts = [ - "-include ./external/envoy_git/test/precompiled/precompiled_test.h", - ], - deps = [ - "@envoy_git//:envoy-test-lib", - "//external:googletest_main", - ], - args = [ - #TODO: Make all test pass - "--gtest_filter=RouterTest.*", - ], - linkstatic=1, -) diff --git a/src/envoy/mixer/BUILD b/src/envoy/mixer/BUILD index 132bc364a1d..45c69b7f2d5 100644 --- a/src/envoy/mixer/BUILD +++ b/src/envoy/mixer/BUILD @@ -15,7 +15,6 @@ ################################################################################ # - load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar") load("//src/envoy/mixer:proxy_docker.bzl", "proxy_docker_build") load("@protobuf_git//:protobuf.bzl", "cc_proto_library") @@ -31,7 +30,8 @@ cc_proto_library( cc_library( name = "filter_lib", srcs = [ - "forward_attribute_filter.cc", + "config.cc", + "config.h", "http_control.cc", "http_control.h", "http_filter.cc", @@ -41,7 +41,7 @@ cc_library( deps = [ ":string_map_proto", "//external:mixer_client_lib", - "@envoy_git//:envoy-common", + "@envoy//source/exe:envoy_common_lib", ], alwayslink = 1, ) @@ -49,9 +49,11 @@ cc_library( cc_binary( name = "envoy", linkstatic = 1, + linkopts = ["-lrt"], + visibility = [":__subpackages__"], deps = [ ":filter_lib", - "@envoy_git//:envoy-main", + "@envoy//source/exe:envoy_main_lib", ], ) @@ -81,10 +83,6 @@ pkg_tar( ) proxy_docker_build( - images = [ - {"name": "proxy", "base": "@docker_ubuntu//:xenial"}, - {"name": "proxy_debug", "base": "@ubuntu_xenial_debug//file"}, - ], entrypoint = [ "/usr/local/bin/start_envoy", "-e", @@ -94,6 +92,16 @@ proxy_docker_build( "-t", "/etc/opt/proxy/envoy.conf.template", ], + images = [ + { + "name": "proxy", + "base": "@docker_ubuntu//:xenial", + }, + { + "name": "proxy_debug", + "base": "@ubuntu_xenial_debug//file", + }, + ], ports = ["9090"], repository = "istio", tags = ["manual"], diff --git a/src/envoy/mixer/README.md b/src/envoy/mixer/README.md index d7d39f30e03..48229a3631e 100644 --- a/src/envoy/mixer/README.md +++ b/src/envoy/mixer/README.md @@ -3,7 +3,7 @@ This Proxy will use Envoy and talk to Mixer server. ## Build Mixer server -* Follow https://github.com/istio/mixer/blob/master/doc/devel/development.md to set up environment, and build via: +* Follow https://github.com/istio/mixer/blob/master/doc/dev/development.md to set up environment, and build via: ``` cd $(ISTIO)/mixer @@ -46,54 +46,98 @@ This Proxy will use Envoy and talk to Mixer server. * Then issue HTTP request to proxy. ``` + # request to server-side proxy curl http://localhost:9090/echo -d "hello world" + # request to client-side proxy that gets sent to server-side proxy + curl http://localhost:7070/echo -d "hello world" ``` -## How to configurate HTTP filters - -This module has two HTTP filters: -1. mixer filter: intercept all HTTP requests, call the mixer. -2. forward_attribute filter: Forward attributes to the upstream istio/proxy. - -### *mixer* filter: +## How to configurate HTTP Mixer filters This filter will intercept all HTTP requests and call Mixer. Here is its config: ``` "filters": [ - "type": "both", + "type": "decoder", "name": "mixer", "config": { "mixer_server": "${MIXER_SERVER}", - "attributes" : { + "mixer_attributes" : { + "attribute_name1": "attribute_value1", + "attribute_name2": "attribute_value2", + "quota.name": "RequestCount" + }, + "forward_attributes" : { "attribute_name1": "attribute_value1", "attribute_name2": "attribute_value2" - } + }, + "quota_name": "RequestCount", + "quota_amount": "1", + "check_cache_expiration_in_seconds": "600", + "check_cache_keys": [ + "request.host", + "request.path", + "origin.user" + ] } ``` Notes: * mixer_server is required -* attributes: these attributes will be send to the mixer - -### *forward_attribute* HTTP filter: +* mixer_attributes: these attributes will be sent to the mixer in both Check and Report calls. +* forward_attributes: these attributes will be forwarded to the upstream istio/proxy. It will send them to mixer in Check and Report calls. +* quota_name, quota_amount are used for making quota call. quota_amount defaults to 1. +* check_cache_keys is to cache check calls. If missing or empty, check calls are not cached. -This filer will forward attributes to the upstream istio/proxy. +## HTTP Route opaque config +By default, the mixer filter only forwards attributes and does not call mixer server. This behavior can be changed per HTTP route by supplying an opaque config: ``` - "filters": [ - "type": "decoder", - "name": "forward_attribute", - "config": { - "attributes": { - "attribute_name1": "attribute_value1", - "attribute_name2": "attribute_value2" - } - } + "routes": [ + { + "timeout_ms": 0, + "prefix": "/", + "cluster": "service1", + "opaque_config": { + "mixer_control": "on", + "mixer_forward": "off" + } + } ``` -Notes: -* attributes: these attributes will be forwarded to the upstream istio/proxy. +This route opaque config reverts the behavior by sending requests to mixer server but not forwarding any attributes. +## How to enable quota (rate limiting) + +Quota (rate limiting) is enforced by the mixer. Mixer needs to be configured with Quota in its global config and service config. Its quota config will have +"quota name", its limit within a window. If "Quota" is added but param is missing, the default config is: quota name is "RequestCount", the limit is 10 with 1 second window. Essentially, it is imposing 10 qps rate limiting. + +Mixer client can be configured to make Quota call for all requests. If "quota_name" is specified in the mixer filter config, mixer client will call Quota with the specified quota name. If "quota_amount" is specified, it will call with that amount, otherwise the used amount is 1. + + +## How to pass some attributes from client proxy to mixer. + +Usually client proxy is not configured to call mixer (it can be enabled in the route opaque_config). Client proxy can pass some attributes to mixer by using "forward_attributes" field. Its attributes will be sent to the upstream proxy (the server proxy). If the server proxy is calling mixer, these attributes will be sent to the mixer. + + +## How to enable cache for Check calls + +Check calls can be cached. By default, it is not enabled. It can be enabled by supplying non-empty "check_cache_keys" string list in the mixer filter config. Only these attributes in the Check request, their keys and values, are used to calculate the key for the cache lookup. If it is a cache hit, the cached response will be used. +The cached response will be expired in 5 minutes by default. It can be overrided by supplying "check_cache_expiration_in_seconds" in the mixer filter config. The Check response from the mixer has an expiration field. If it is filled, it will be used. By design, the mixer will control the cache expiration time. + +Following is a sample mixer filter config to enable the Check call cache: +``` + "check_cache_expiration_in_seconds": "600", + "check_cache_keys": [ + "request.host", + "request.path", + "source.labels", + "request.headers/:method", + "origin.user" + ] +``` +For the string map attributes in the above example: +1) "request.headers" attribute is a string map, "request.headers/:method" cache key means only its ":method" key and value are used for cache key. +2) "source.labels" attribute is a string map, "source.labels" cache key means all key value pairs for the string map will be used. diff --git a/src/envoy/mixer/config.cc b/src/envoy/mixer/config.cc new file mode 100644 index 00000000000..8e06b1e9c2b --- /dev/null +++ b/src/envoy/mixer/config.cc @@ -0,0 +1,97 @@ +/* Copyright 2017 Istio Authors. 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 "src/envoy/mixer/config.h" + +using ::istio::mixer_client::Attributes; + +namespace Http { +namespace Mixer { +namespace { + +// The Json object name for mixer-server. +const std::string kMixerServer("mixer_server"); + +// The Json object name for static attributes. +const std::string kMixerAttributes("mixer_attributes"); + +// The Json object name to specify attributes which will be forwarded +// to the upstream istio proxy. +const std::string kForwardAttributes("forward_attributes"); + +// The Json object name for quota name and amount. +const std::string kQuotaName("quota_name"); +const std::string kQuotaAmount("quota_amount"); + +// The Json object name for check cache keys. +const std::string kCheckCacheKeys("check_cache_keys"); +const std::string kCheckCacheExpiration("check_cache_expiration_in_seconds"); + +void ReadString(const Json::Object& json, const std::string& name, + std::string* value) { + if (json.hasObject(name)) { + *value = json.getString(name); + } +} + +void ReadStringMap(const Json::Object& json, const std::string& name, + std::map* map) { + if (json.hasObject(name)) { + json.getObject(name)->iterate( + [map](const std::string& key, const Json::Object& obj) -> bool { + (*map)[key] = obj.asString(); + return true; + }); + } +} + +void ReadStringVector(const Json::Object& json, const std::string& name, + std::vector* value) { + if (json.hasObject(name)) { + auto v = json.getStringArray(name); + value->swap(v); + } +} + +} // namespace + +void MixerConfig::Load(const Json::Object& json) { + ReadString(json, kMixerServer, &mixer_server); + + ReadStringMap(json, kMixerAttributes, &mixer_attributes); + ReadStringMap(json, kForwardAttributes, &forward_attributes); + + ReadString(json, kQuotaName, "a_name); + ReadString(json, kQuotaAmount, "a_amount); + + ReadStringVector(json, kCheckCacheKeys, &check_cache_keys); + ReadString(json, kCheckCacheExpiration, &check_cache_expiration); +} + +void MixerConfig::ExtractQuotaAttributes(Attributes* attr) const { + if (!quota_name.empty()) { + attr->attributes[Attributes::kQuotaName] = + Attributes::StringValue(quota_name); + + int64_t amount = 1; // default amount to 1. + if (!quota_amount.empty()) { + amount = std::stoi(quota_amount); + } + attr->attributes[Attributes::kQuotaAmount] = Attributes::Int64Value(amount); + } +} + +} // namespace Mixer +} // namespace Http diff --git a/src/envoy/mixer/config.h b/src/envoy/mixer/config.h new file mode 100644 index 00000000000..fc08f33c439 --- /dev/null +++ b/src/envoy/mixer/config.h @@ -0,0 +1,54 @@ +/* Copyright 2017 Istio Authors. 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. + */ + +#pragma once + +#include "precompiled/precompiled.h" + +#include "envoy/json/json_object.h" +#include "include/attribute.h" + +namespace Http { +namespace Mixer { + +// A config for mixer filter +struct MixerConfig { + // the mixer server address + std::string mixer_server; + + // These static attributes will be send to mixer in both + // Check and Report. + std::map mixer_attributes; + + // These attributes will be forwarded to upstream. + std::map forward_attributes; + + // Quota attributes. + std::string quota_name; + std::string quota_amount; + + // The attribute names for check cache. + std::vector check_cache_keys; + std::string check_cache_expiration; + + // Load the config from envoy config. + void Load(const Json::Object& json); + + // Extract quota attributes. + void ExtractQuotaAttributes(::istio::mixer_client::Attributes* attr) const; +}; + +} // namespace Mixer +} // namespace Http diff --git a/src/envoy/mixer/envoy.conf.template b/src/envoy/mixer/envoy.conf.template index c5377632388..c7574734ff3 100644 --- a/src/envoy/mixer/envoy.conf.template +++ b/src/envoy/mixer/envoy.conf.template @@ -1,7 +1,7 @@ { "listeners": [ { - "port": ${PORT}, + "address": "tcp://0.0.0.0:${PORT}", "bind_to_port": true, "filters": [ { @@ -19,7 +19,11 @@ { "timeout_ms": 0, "prefix": "/", - "cluster": "service1" + "cluster": "service1", + "opaque_config": { + "mixer_control": "on", + "mixer_forward": "off" + } } ] } @@ -32,14 +36,21 @@ ], "filters": [ { - "type": "both", + "type": "decoder", "name": "mixer", "config": { "mixer_server": "${MIXER_SERVER}", - "attributes": { + "mixer_attributes": { "target.uid": "POD222", "target.namespace": "XYZ222" - } + }, + "quota_name": "RequestCount", + "quota_amount": "1", + "check_cache_keys": [ + "request.host", + "request.path", + "origin.user" + ] } }, { @@ -53,7 +64,7 @@ ] }, { - "port": 7070, + "address": "tcp://0.0.0.0:7070", "bind_to_port": true, "filters": [ { @@ -85,9 +96,10 @@ "filters": [ { "type": "decoder", - "name": "forward_attribute", + "name": "mixer", "config": { - "attributes": { + "mixer_server": "${MIXER_SERVER}", + "forward_attributes": { "source.uid": "POD11", "source.namespace": "XYZ11" } @@ -106,7 +118,7 @@ ], "admin": { "access_log_path": "/dev/stdout", - "port": 9001 + "address": "tcp://0.0.0.0:9001" }, "cluster_manager": { "clusters": [ diff --git a/src/envoy/mixer/http_control.cc b/src/envoy/mixer/http_control.cc index 3b97d20a35f..61aa3285a62 100644 --- a/src/envoy/mixer/http_control.cc +++ b/src/envoy/mixer/http_control.cc @@ -23,8 +23,12 @@ #include "src/envoy/mixer/utils.h" using ::google::protobuf::util::Status; +using ::istio::mixer_client::CheckOptions; using ::istio::mixer_client::Attributes; using ::istio::mixer_client::DoneFunc; +using ::istio::mixer_client::MixerClientOptions; +using ::istio::mixer_client::ReportOptions; +using ::istio::mixer_client::QuotaOptions; namespace Http { namespace Mixer { @@ -40,11 +44,31 @@ const std::string kRequestSize = "request.size"; const std::string kRequestTime = "request.time"; const std::string kResponseHeaders = "response.headers"; -const std::string kResponseHttpCode = "response.http.code"; -const std::string kResponseLatency = "response.latency"; +const std::string kResponseCode = "response.code"; +const std::string kResponseDuration = "response.duration"; const std::string kResponseSize = "response.size"; const std::string kResponseTime = "response.time"; +// Check cache size: 10000 cache entries. +const int kCheckCacheEntries = 10000; +// Default check cache expired in 5 minutes. +const int kCheckCacheExpirationInSeconds = 300; + +CheckOptions GetCheckOptions(const MixerConfig& config) { + int expiration = kCheckCacheExpirationInSeconds; + if (!config.check_cache_expiration.empty()) { + expiration = std::stoi(config.check_cache_expiration); + } + + // Remove expired items from cache 1 second later. + CheckOptions options(kCheckCacheEntries, expiration * 1000, + (expiration + 1) * 1000); + + options.cache_keys = config.check_cache_keys; + + return options; +} + void SetStringAttribute(const std::string& name, const std::string& value, Attributes* attr) { if (!value.empty()) { @@ -94,28 +118,27 @@ void FillRequestInfoAttributes(const AccessLog::RequestInfo& info, attr->attributes[kResponseSize] = Attributes::Int64Value(info.bytesSent()); } - if (info.duration().count() > 0) { - attr->attributes[kResponseLatency] = Attributes::DurationValue( - std::chrono::duration_cast(info.duration())); - } + attr->attributes[kResponseDuration] = Attributes::DurationValue( + std::chrono::duration_cast(info.duration())); if (info.responseCode().valid()) { - attr->attributes[kResponseHttpCode] = + attr->attributes[kResponseCode] = Attributes::Int64Value(info.responseCode().value()); } else { - attr->attributes[kResponseHttpCode] = - Attributes::Int64Value(check_status_code); + attr->attributes[kResponseCode] = Attributes::Int64Value(check_status_code); } } } // namespace -HttpControl::HttpControl(const std::string& mixer_server, - std::map&& attributes) - : config_attributes_(std::move(attributes)) { - ::istio::mixer_client::MixerClientOptions options; - options.mixer_server = mixer_server; +HttpControl::HttpControl(const MixerConfig& mixer_config) + : mixer_config_(mixer_config) { + MixerClientOptions options(GetCheckOptions(mixer_config), ReportOptions(), + QuotaOptions()); + options.mixer_server = mixer_config_.mixer_server; mixer_client_ = ::istio::mixer_client::CreateMixerClient(options); + + mixer_config_.ExtractQuotaAttributes("a_attributes_); } void HttpControl::FillCheckAttributes(HeaderMap& header_map, Attributes* attr) { @@ -133,7 +156,7 @@ void HttpControl::FillCheckAttributes(HeaderMap& header_map, Attributes* attr) { FillRequestHeaderAttributes(header_map, attr); - for (const auto& attribute : config_attributes_) { + for (const auto& attribute : mixer_config_.mixer_attributes) { SetStringAttribute(attribute.first, attribute.second, attr); } } @@ -143,7 +166,18 @@ void HttpControl::Check(HttpRequestDataPtr request_data, HeaderMap& headers, FillCheckAttributes(headers, &request_data->attributes); SetStringAttribute(kOriginUser, origin_user, &request_data->attributes); log().debug("Send Check: {}", request_data->attributes.DebugString()); - mixer_client_->Check(request_data->attributes, on_done); + + auto check_on_done = [this, on_done](const Status& status) { + if (status.ok()) { + if (!quota_attributes_.attributes.empty()) { + log().debug("Send Quota: {}", quota_attributes_.DebugString()); + mixer_client_->Quota(quota_attributes_, on_done); + return; // Not to call on_done again. + } + } + on_done(status); + }; + mixer_client_->Check(request_data->attributes, check_on_done); } void HttpControl::Report(HttpRequestDataPtr request_data, diff --git a/src/envoy/mixer/http_control.h b/src/envoy/mixer/http_control.h index e9ddc734f45..c473facef3c 100644 --- a/src/envoy/mixer/http_control.h +++ b/src/envoy/mixer/http_control.h @@ -21,6 +21,7 @@ #include "common/http/headers.h" #include "envoy/http/access_log.h" #include "include/client.h" +#include "src/envoy/mixer/config.h" namespace Http { namespace Mixer { @@ -37,8 +38,7 @@ typedef std::shared_ptr HttpRequestDataPtr; class HttpControl final : public Logger::Loggable { public: // The constructor. - HttpControl(const std::string& mixer_server, - std::map&& attributes); + HttpControl(const MixerConfig& mixer_config); // Make mixer check call. void Check(HttpRequestDataPtr request_data, HeaderMap& headers, @@ -56,8 +56,10 @@ class HttpControl final : public Logger::Loggable { // The mixer client std::unique_ptr<::istio::mixer_client::MixerClient> mixer_client_; - // The attributes read from the config file. - std::map config_attributes_; + // The mixer config + const MixerConfig& mixer_config_; + // Quota attributes; extracted from envoy filter config. + ::istio::mixer_client::Attributes quota_attributes_; }; } // namespace Mixer diff --git a/src/envoy/mixer/http_filter.cc b/src/envoy/mixer/http_filter.cc index c99329aea4f..9f8e4ffb75c 100644 --- a/src/envoy/mixer/http_filter.cc +++ b/src/envoy/mixer/http_filter.cc @@ -15,12 +15,14 @@ #include "precompiled/precompiled.h" +#include "common/common/base64.h" #include "common/common/logger.h" #include "common/http/headers.h" #include "common/http/utility.h" #include "envoy/server/instance.h" #include "envoy/ssl/connection.h" #include "server/config/network/http_connection_manager.h" +#include "src/envoy/mixer/config.h" #include "src/envoy/mixer/http_control.h" #include "src/envoy/mixer/utils.h" @@ -32,11 +34,11 @@ namespace Http { namespace Mixer { namespace { -// The Json object name for mixer-server. -const std::string kJsonNameMixerServer("mixer_server"); +// Switch to turn off attribute forwarding +const std::string kJsonNameForwardSwitch("mixer_forward"); -// The Json object name for static attributes. -const std::string kJsonNameMixerAttributes("attributes"); +// Switch to turn off mixer check/report/quota +const std::string kJsonNameMixerSwitch("mixer_control"); // Convert Status::code to HTTP code int HttpCode(int code) { @@ -88,50 +90,90 @@ class Config : public Logger::Loggable { private: std::shared_ptr http_control_; Upstream::ClusterManager& cm_; + std::string forward_attributes_; + MixerConfig mixer_config_; public: Config(const Json::Object& config, Server::Instance& server) : cm_(server.clusterManager()) { - std::string mixer_server; - if (config.hasObject(kJsonNameMixerServer)) { - mixer_server = config.getString(kJsonNameMixerServer); - } else { + mixer_config_.Load(config); + if (mixer_config_.mixer_server.empty()) { log().error( "mixer_server is required but not specified in the config: {}", __func__); + } else { + log().debug("Called Mixer::Config constructor with mixer_server: ", + mixer_config_.mixer_server); } - std::map attributes = - Utils::ExtractStringMap(config, kJsonNameMixerAttributes); + if (!mixer_config_.forward_attributes.empty()) { + std::string serialized_str = + Utils::SerializeStringMap(mixer_config_.forward_attributes); + forward_attributes_ = + Base64::encode(serialized_str.c_str(), serialized_str.size()); + log().debug("Mixer forward attributes set: ", serialized_str); + } - http_control_ = - std::make_shared(mixer_server, std::move(attributes)); - log().debug("Called Mixer::Config constructor with mixer_server: ", - mixer_server); + http_control_ = std::make_shared(mixer_config_); } std::shared_ptr& http_control() { return http_control_; } + const std::string& forward_attributes() const { return forward_attributes_; } }; typedef std::shared_ptr ConfigPtr; -class Instance : public Http::StreamFilter, public Http::AccessLog::Instance { +class Instance : public Http::StreamDecoderFilter, + public Http::AccessLog::Instance { private: std::shared_ptr http_control_; + ConfigPtr config_; std::shared_ptr request_data_; enum State { NotStarted, Calling, Complete, Responded }; State state_; StreamDecoderFilterCallbacks* decoder_callbacks_; - StreamEncoderFilterCallbacks* encoder_callbacks_; bool initiating_call_; int check_status_code_; + bool mixer_disabled_; + + // mixer control switch (off by default) + bool mixer_disabled() { + auto route = decoder_callbacks_->route(); + if (route != nullptr) { + auto entry = route->routeEntry(); + if (entry != nullptr) { + auto key = entry->opaqueConfig().find(kJsonNameMixerSwitch); + if (key != entry->opaqueConfig().end() && key->second == "on") { + return false; + } + } + } + return true; + } + + // attribute forward switch (on by default) + bool forward_disabled() { + auto route = decoder_callbacks_->route(); + if (route != nullptr) { + auto entry = route->routeEntry(); + if (entry != nullptr) { + auto key = entry->opaqueConfig().find(kJsonNameForwardSwitch); + if (key != entry->opaqueConfig().end() && key->second == "off") { + return true; + } + } + } + return false; + } + public: Instance(ConfigPtr config) : http_control_(config->http_control()), + config_(config), state_(NotStarted), initiating_call_(false), check_status_code_(HttpCode(StatusCode::UNKNOWN)) { @@ -149,6 +191,17 @@ class Instance : public Http::StreamFilter, public Http::AccessLog::Instance { FilterHeadersStatus decodeHeaders(HeaderMap& headers, bool end_stream) override { Log().debug("Called Mixer::Instance : {}", __func__); + + if (!config_->forward_attributes().empty() && !forward_disabled()) { + headers.addStatic(Utils::kIstioAttributeHeader, + config_->forward_attributes()); + } + + mixer_disabled_ = mixer_disabled(); + if (mixer_disabled_) { + return FilterHeadersStatus::Continue; + } + state_ = Calling; initiating_call_ = true; request_data_ = std::make_shared(); @@ -174,6 +227,10 @@ class Instance : public Http::StreamFilter, public Http::AccessLog::Instance { FilterDataStatus decodeData(Buffer::Instance& data, bool end_stream) override { + if (mixer_disabled_) { + return FilterDataStatus::Continue; + } + Log().debug("Called Mixer::Instance : {} ({}, {})", __func__, data.length(), end_stream); if (state_ == Calling) { @@ -183,6 +240,10 @@ class Instance : public Http::StreamFilter, public Http::AccessLog::Instance { } FilterTrailersStatus decodeTrailers(HeaderMap& trailers) override { + if (mixer_disabled_) { + return FilterTrailersStatus::Continue; + } + Log().debug("Called Mixer::Instance : {}", __func__); if (state_ == Calling) { return FilterTrailersStatus::StopIteration; @@ -197,6 +258,7 @@ class Instance : public Http::StreamFilter, public Http::AccessLog::Instance { decoder_callbacks_->addResetStreamCallback( [this]() { state_ = Responded; }); } + void completeCheck(const Status& status) { Log().debug("Called Mixer::Instance : check complete {}", status.ToString()); @@ -207,32 +269,13 @@ class Instance : public Http::StreamFilter, public Http::AccessLog::Instance { status.ToString()); return; } + state_ = Complete; if (!initiating_call_) { decoder_callbacks_->continueDecoding(); } } - virtual FilterHeadersStatus encodeHeaders(HeaderMap& headers, - bool end_stream) override { - Log().debug("Called Mixer::Instance : {}", __func__); - return FilterHeadersStatus::Continue; - } - virtual FilterDataStatus encodeData(Buffer::Instance& data, - bool end_stream) override { - Log().debug("Called Mixer::Instance : {}", __func__); - return FilterDataStatus::Continue; - } - virtual FilterTrailersStatus encodeTrailers(HeaderMap& trailers) override { - Log().debug("Called Mixer::Instance : {}", __func__); - return FilterTrailersStatus::Continue; - } - virtual void setEncoderFilterCallbacks( - StreamEncoderFilterCallbacks& callbacks) override { - Log().debug("Called Mixer::Instance : {}", __func__); - encoder_callbacks_ = &callbacks; - } - virtual void log(const HeaderMap* request_headers, const HeaderMap* response_headers, const AccessLog::RequestInfo& request_info) override { @@ -267,7 +310,7 @@ class MixerConfig : public HttpFilterConfigFactory { HttpFilterFactoryCb tryCreateFilterFactory( HttpFilterType type, const std::string& name, const Json::Object& config, const std::string&, Server::Instance& server) override { - if (type != HttpFilterType::Both || name != "mixer") { + if (type != HttpFilterType::Decoder || name != "mixer") { return nullptr; } @@ -277,8 +320,10 @@ class MixerConfig : public HttpFilterConfigFactory { [mixer_config](Http::FilterChainFactoryCallbacks& callbacks) -> void { std::shared_ptr instance( new Http::Mixer::Instance(mixer_config)); - callbacks.addStreamFilter(Http::StreamFilterPtr(instance)); - callbacks.addAccessLogHandler(Http::AccessLog::InstancePtr(instance)); + callbacks.addStreamDecoderFilter( + Http::StreamDecoderFilterSharedPtr(instance)); + callbacks.addAccessLogHandler( + Http::AccessLog::InstanceSharedPtr(instance)); }; } }; @@ -286,4 +331,4 @@ class MixerConfig : public HttpFilterConfigFactory { static RegisterHttpFilterConfigFactory register_; } // namespace Configuration -} // namespace server +} // namespace Server diff --git a/src/envoy/mixer/integration_test/BUILD b/src/envoy/mixer/integration_test/BUILD new file mode 100644 index 00000000000..a6ff4916db5 --- /dev/null +++ b/src/envoy/mixer/integration_test/BUILD @@ -0,0 +1,64 @@ +# Copyright 2017 Istio Authors. 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. +# +################################################################################ +# + +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load(":test_suite.bzl", "go_test_suite") + +go_library( + name = "go_default_library", + srcs = [ + "attributes.go", + "envoy.go", + "envoy_conf.go", + "http_client.go", + "http_server.go", + "mixer_server.go", + "setup.go", + ], + deps = [ + "@com_github_gogo_protobuf//types:go_default_library", + "@com_github_golang_glog//:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + "@com_github_googleapis_googleapis//:google/rpc", + "@com_github_istio_api//:mixer/v1", + "@com_github_istio_mixer//pkg/api:go_default_library", + "@com_github_istio_mixer//pkg/attribute:go_default_library", + "@com_github_istio_mixer//pkg/pool:go_default_library", + "@com_github_istio_mixer//pkg/tracing:go_default_library", + "@org_golang_google_grpc//:go_default_library", + ], +) + +go_test_suite( + data = [ + "//src/envoy/mixer:envoy", + ], + library = ":go_default_library", + tags = [ + # Use fixed ports, not in sanbbox, have to be run exclusively. + "exclusive", + # shared memory path /envoy_shared_memory_0 used by Envoy + # hot start is not working in sandbox mode. + "local", + ], + tests = [ + "check_cache_test.go", + "check_report_test.go", + "failed_request_test.go", + "quota_test.go", + ], +) diff --git a/src/envoy/mixer/integration_test/attributes.go b/src/envoy/mixer/integration_test/attributes.go new file mode 100644 index 00000000000..6aecd20d8bb --- /dev/null +++ b/src/envoy/mixer/integration_test/attributes.go @@ -0,0 +1,128 @@ +// Copyright 2017 Istio Authors +// +// 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. + +package test + +import ( + "encoding/json" + "fmt" + "reflect" + + "istio.io/mixer/pkg/attribute" +) + +func verifyStringMap(actual map[string]string, expected map[string]interface{}) error { + for k, v := range expected { + vstring := v.(string) + // "-" make sure the key does not exist. + if vstring == "-" { + if _, ok := actual[k]; ok { + return fmt.Errorf("key %+v is NOT expected", k) + } + } else { + if val, ok := actual[k]; ok { + // "*" only check key exist + if val != vstring && vstring != "*" { + return fmt.Errorf("key %+v value doesn't match. Actual %+v, expected %+v", + k, val, vstring) + } + } else { + return fmt.Errorf("key %+v is expected", k) + } + } + } + return nil +} + +// Attributes verification rules: +// +// 1) If value is *, key must exist, but value is not checked. +// 1) If value is -, key must NOT exist. +// 3) At top level attributes, not inside StringMap, all keys must +// be listed. Extra keys are NOT allowed +// 3) Inside StringMap, not need to list all keys. Extra keys are allowed +// +// Attributes provided from envoy config +// * source.id and source.namespace are forwarded from client proxy +// * target.id and target.namespace are from server proxy +// +// HTTP header "x-istio-attributes" is used to forward attributes between +// proxy. It should be removed before calling mixer and backend. +// +func Verify(b *attribute.MutableBag, json_results string) error { + var r map[string]interface{} + if err := json.Unmarshal([]byte(json_results), &r); err != nil { + return fmt.Errorf("unable to decode json %v", err) + } + + all_keys := make(map[string]bool) + for _, k := range b.Names() { + all_keys[k] = true + } + + for k, v := range r { + switch vv := v.(type) { + case string: + // "*" means only checking key. + if vv == "*" { + if _, ok := b.Get(k); !ok { + return fmt.Errorf("attribute %+v is expected", k) + } + } else { + if val, ok := b.Get(k); ok { + if val.(string) != v.(string) { + return fmt.Errorf("attribute %+v value doesn't match. Actual %+v, expected %+v", + k, val.(string), v.(string)) + } + } else { + return fmt.Errorf("attribute %+v is expected", k) + } + } + case float64: + // Json converts all integers to float64, + // Our tests only verify size related attributes which are int64 type + if val, ok := b.Get(k); ok { + vint64 := int64(vv) + if val.(int64) != vint64 { + return fmt.Errorf("attribute %+v value doesn't match. Actual %+v, expected %+v", + k, val.(int64), vint64) + } + } else { + return fmt.Errorf("attribute %+v is expected", k) + } + case map[string]interface{}: + if val, ok := b.Get(k); ok { + if err := verifyStringMap(val.(map[string]string), v.(map[string]interface{})); err != nil { + return fmt.Errorf("attribute %+v StringMap doesn't match: %+v", k, err) + } + } else { + return fmt.Errorf("attribute %+v is expected", k) + } + default: + return fmt.Errorf("attribute %+v is of a type %+v that I don't know how to handle ", + k, reflect.TypeOf(v)) + } + delete(all_keys, k) + + } + + if len(all_keys) > 0 { + var s string + for k, _ := range all_keys { + s += k + ", " + } + return fmt.Errorf("Following attributes are not expected: %s", s) + } + return nil +} diff --git a/src/envoy/mixer/integration_test/check_cache_test.go b/src/envoy/mixer/integration_test/check_cache_test.go new file mode 100644 index 00000000000..ab79320ae82 --- /dev/null +++ b/src/envoy/mixer/integration_test/check_cache_test.go @@ -0,0 +1,40 @@ +// Copyright 2017 Istio Authors +// +// 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. + +package test + +import ( + "fmt" + "testing" +) + +func TestCheckCache(t *testing.T) { + s, err := SetUp(t, basicConfig+","+checkCacheConfig) + if err != nil { + t.Fatalf("Failed to setup test: %v", err) + } + defer s.TearDown() + + url := fmt.Sprintf("http://localhost:%d/echo", ClientProxyPort) + + // Issues a GET echo request with 0 size body + tag := "OKGet" + for i := 0; i < 10; i++ { + if _, _, err := HTTPGet(url); err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + // Only the first check is called. + s.VerifyCheckCount(tag, 1) + } +} diff --git a/src/envoy/mixer/integration_test/check_report_test.go b/src/envoy/mixer/integration_test/check_report_test.go new file mode 100644 index 00000000000..9b9708c19b2 --- /dev/null +++ b/src/envoy/mixer/integration_test/check_report_test.go @@ -0,0 +1,154 @@ +// Copyright 2017 Istio Authors +// +// 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. + +package test + +import ( + "fmt" + "testing" +) + +// Check attributes from a good GET request +const checkAttributesOkGet = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "GET", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + } +} +` + +// Report attributes from a good GET request +const reportAttributesOkGet = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "GET", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + }, + "request.size": 0, + "response.time": "*", + "response.size": 0, + "response.duration": "*", + "response.code": 200, + "response.headers": { + "date": "*", + "content-type": "text/plain; charset=utf-8", + "content-length": "0", + ":status": "200", + "server": "envoy" + } +} +` + +// Check attributes from a good POST request +const checkAttributesOkPost = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "POST", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + } +} +` + +// Report attributes from a good POST request +const reportAttributesOkPost = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "POST", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + }, + "request.size": 12, + "response.time": "*", + "response.size": 12, + "response.duration": "*", + "response.code": 200, + "response.headers": { + "date": "*", + "content-type": "text/plain", + "content-length": "12", + ":status": "200", + "server": "envoy" + } +} +` + +func TestCheckReportAttributes(t *testing.T) { + s, err := SetUp(t, basicConfig) + if err != nil { + t.Fatalf("Failed to setup test: %v", err) + } + defer s.TearDown() + + url := fmt.Sprintf("http://localhost:%d/echo", ClientProxyPort) + + // Issues a GET echo request with 0 size body + tag := "OKGet" + if _, _, err := HTTPGet(url); err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + s.VerifyCheck(tag, checkAttributesOkGet) + s.VerifyReport(tag, reportAttributesOkGet) + + // Issues a POST request. + tag = "OKPost" + if _, _, err := HTTPPost(url, "text/plain", "Hello World!"); err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + s.VerifyCheck(tag, checkAttributesOkPost) + s.VerifyReport(tag, reportAttributesOkPost) +} diff --git a/src/envoy/mixer/integration_test/envoy.go b/src/envoy/mixer/integration_test/envoy.go new file mode 100644 index 00000000000..b34fb5c240a --- /dev/null +++ b/src/envoy/mixer/integration_test/envoy.go @@ -0,0 +1,88 @@ +// Copyright 2017 Istio Authors +// +// 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. + +package test + +import ( + "log" + "os" + "os/exec" + "strings" +) + +func getTestBinRootPath() string { + switch { + // custom path + case os.Getenv("TEST_BIN_ROOT") != "": + return os.Getenv("TEST_BIN_ROOT") + // running under bazel + case os.Getenv("TEST_SRCDIR") != "": + return os.Getenv("TEST_SRCDIR") + "/__main__" + // running with native go + case os.Getenv("GOPATH") != "": + list := strings.Split(os.Getenv("GOPATH"), + string(os.PathListSeparator)) + return list[0] + "/bazel-bin" + default: + return "bazel-bin" + } +} + +type Envoy struct { + cmd *exec.Cmd +} + +// Run command and return the merged output from stderr and stdout, error code +func Run(name string, args ...string) (s string, err error) { + log.Println(">", name, strings.Join(args, " ")) + c := exec.Command(name, args...) + bytes, err := c.CombinedOutput() + s = string(bytes) + for _, line := range strings.Split(s, "\n") { + log.Println(line) + } + if err != nil { + log.Println(err) + } + return +} + +func NewEnvoy(conf string) (*Envoy, error) { + bin_path := getTestBinRootPath() + "/src/envoy/mixer/envoy" + log.Printf("Envoy binary: %v\n", bin_path) + + conf_path := "/tmp/envoy.conf" + log.Printf("Envoy config: in %v\n%v\n", conf_path, conf) + if err := CreateEnvoyConf(conf_path, conf); err != nil { + return nil, err + } + + cmd := exec.Command(bin_path, "-c", conf_path, "-l", "debug") + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return &Envoy{ + cmd: cmd, + }, nil +} + +func (s *Envoy) Start() error { + return s.cmd.Start() +} + +func (s *Envoy) Stop() error { + log.Printf("Kill Envoy ...\n") + err := s.cmd.Process.Kill() + log.Printf("Kill Envoy ... Done\n") + return err +} diff --git a/src/envoy/mixer/integration_test/envoy_conf.go b/src/envoy/mixer/integration_test/envoy_conf.go new file mode 100644 index 00000000000..166b11543aa --- /dev/null +++ b/src/envoy/mixer/integration_test/envoy_conf.go @@ -0,0 +1,244 @@ +// Copyright 2017 Istio Authors. 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. +// + +package test + +import ( + "fmt" + "os" + "text/template" +) + +const ( + // These ports should match with used envoy.conf + // Default is using one in this folder. + ServerProxyPort = 29090 + ClientProxyPort = 27070 + MixerPort = 29091 + BackendPort = 28080 + AdminPort = 29001 +) + +type ConfParam struct { + ClientPort int + ServerPort int + AdminPort int + MixerServer string + Backend string + ClientConfig string + ServerConfig string +} + +// A basic config +const basicConfig = ` + "mixer_attributes": { + "target.uid": "POD222", + "target.namespace": "XYZ222" + } +` + +// A config with quota +const quotaConfig = ` + "quota_name": "RequestCount", + "quota_amount": "5" +` + +// A config with check cache keys +const checkCacheConfig = ` + "check_cache_keys": [ + "request.host", + "request.path", + "origin.user" + ] +` + +// The default client proxy mixer config +const defaultClientMixerConfig = ` + "forward_attributes": { + "source.uid": "POD11", + "source.namespace": "XYZ11" + } +` + +// The envoy config template +const envoyConfTempl = ` +{ + "listeners": [ + { + "address": "tcp://0.0.0.0:{{.ServerPort}}", + "bind_to_port": true, + "filters": [ + { + "type": "read", + "name": "http_connection_manager", + "config": { + "codec_type": "auto", + "stat_prefix": "ingress_http", + "route_config": { + "virtual_hosts": [ + { + "name": "backend", + "domains": ["*"], + "routes": [ + { + "timeout_ms": 0, + "prefix": "/", + "cluster": "service1", + "opaque_config": { + "mixer_control": "on", + "mixer_forward": "off" + } + } + ] + } + ] + }, + "access_log": [ + { + "path": "/dev/stdout" + } + ], + "filters": [ + { + "type": "decoder", + "name": "mixer", + "config": { + "mixer_server": "{{.MixerServer}}", +{{.ServerConfig}} + } + }, + { + "type": "decoder", + "name": "router", + "config": {} + } + ] + } + } + ] + }, + { + "address": "tcp://0.0.0.0:{{.ClientPort}}", + "bind_to_port": true, + "filters": [ + { + "type": "read", + "name": "http_connection_manager", + "config": { + "codec_type": "auto", + "stat_prefix": "ingress_http", + "route_config": { + "virtual_hosts": [ + { + "name": "backend", + "domains": ["*"], + "routes": [ + { + "timeout_ms": 0, + "prefix": "/", + "cluster": "service2" + } + ] + } + ] + }, + "access_log": [ + { + "path": "/dev/stdout" + } + ], + "filters": [ + { + "type": "decoder", + "name": "mixer", + "config": { + "mixer_server": "{{.MixerServer}}", +{{.ClientConfig}} + } + }, + { + "type": "decoder", + "name": "router", + "config": {} + } + ] + } + } + ] + } + ], + "admin": { + "access_log_path": "/dev/stdout", + "address": "tcp://0.0.0.0:{{.AdminPort}}" + }, + "cluster_manager": { + "clusters": [ + { + "name": "service1", + "connect_timeout_ms": 5000, + "type": "strict_dns", + "lb_type": "round_robin", + "hosts": [ + { + "url": "tcp://{{.Backend}}" + } + ] + }, + { + "name": "service2", + "connect_timeout_ms": 5000, + "type": "strict_dns", + "lb_type": "round_robin", + "hosts": [ + { + "url": "tcp://localhost:{{.ServerPort}}" + } + ] + } + ] + } +} +` + +func (c *ConfParam) write(path string) error { + tmpl, err := template.New("test").Parse(envoyConfTempl) + if err != nil { + return fmt.Errorf("Failed to parse config template: %v", err) + } + + f, err := os.Create(path) + if err != nil { + return fmt.Errorf("Failed to create file %v: %v", path, err) + } + defer f.Close() + return tmpl.Execute(f, *c) +} + +func getConf() ConfParam { + return ConfParam{ + ClientPort: ClientProxyPort, + ServerPort: ServerProxyPort, + AdminPort: AdminPort, + MixerServer: fmt.Sprintf("localhost:%d", MixerPort), + Backend: fmt.Sprintf("localhost:%d", BackendPort), + ClientConfig: defaultClientMixerConfig, + } +} + +func CreateEnvoyConf(path string, conf string) error { + c := getConf() + c.ServerConfig = conf + return c.write(path) +} diff --git a/src/envoy/mixer/integration_test/failed_request_test.go b/src/envoy/mixer/integration_test/failed_request_test.go new file mode 100644 index 00000000000..011227afa6f --- /dev/null +++ b/src/envoy/mixer/integration_test/failed_request_test.go @@ -0,0 +1,161 @@ +// Copyright 2017 Istio Authors +// +// 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. + +package test + +import ( + "fmt" + "testing" + + rpc "github.com/googleapis/googleapis/google/rpc" +) + +const ( + mixerAuthFailMessage = "Unauthenticated by mixer." +) + +// Check attributes from a fail GET request from mixer +const checkAttributesMixerFail = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "GET", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + } +} +` + +// Report attributes from a fail GET request from mixer +const reportAttributesMixerFail = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "GET", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + }, + "request.size": 0, + "response.time": "*", + "response.size": 41, + "response.duration": "*", + "response.code": 401, + "response.headers": { + "date": "*", + "content-type": "text/plain", + "content-length": "41", + ":status": "401", + "server": "envoy" + } +} +` + +// Report attributes from a fail GET request from backend +const reportAttributesBackendFail = ` +{ + "request.host": "localhost:27070", + "request.path": "/echo", + "request.time": "*", + "source.uid": "POD11", + "source.namespace": "XYZ11", + "target.uid": "POD222", + "target.namespace": "XYZ222", + "request.headers": { + ":method": "GET", + ":path": "/echo", + ":authority": "localhost:27070", + "x-forwarded-proto": "http", + "x-istio-attributes": "-", + "x-request-id": "*" + }, + "request.size": 0, + "response.time": "*", + "response.size": 25, + "response.duration": "*", + "response.code": 400, + "response.headers": { + "date": "*", + "content-type": "text/plain; charset=utf-8", + "content-length": "25", + ":status": "400", + "server": "envoy" + } +} +` + +func TestFailedRequest(t *testing.T) { + s, err := SetUp(t, basicConfig) + if err != nil { + t.Fatalf("Failed to setup test: %v", err) + } + defer s.TearDown() + + url := fmt.Sprintf("http://localhost:%d/echo", ClientProxyPort) + + tag := "MixerFail" + s.mixer.check.r_status = rpc.Status{ + Code: int32(rpc.UNAUTHENTICATED), + Message: mixerAuthFailMessage, + } + code, resp_body, err := HTTPGet(url) + // Make sure to restore r_status for next request. + s.mixer.check.r_status = rpc.Status{} + if err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + if code != 401 { + t.Errorf("Status code 401 is expected.") + } + if resp_body != "UNAUTHENTICATED:"+mixerAuthFailMessage { + t.Errorf("Error response body is not expected.") + } + s.VerifyCheck(tag, checkAttributesMixerFail) + s.VerifyReport(tag, reportAttributesMixerFail) + + // Issues a failed request caused by backend + tag = "BackendFail" + headers := map[string]string{} + headers[FailHeader] = "Yes" + code, resp_body, err = HTTPGetWithHeaders(url, headers) + if err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + if code != 400 { + t.Errorf("Status code 400 is expected.") + } + if resp_body != FailBody { + t.Errorf("Error response body is not expected.") + } + // Same Check attributes as the first one. + s.VerifyCheck(tag, checkAttributesMixerFail) + s.VerifyReport(tag, reportAttributesBackendFail) +} diff --git a/src/envoy/mixer/integration_test/http_client.go b/src/envoy/mixer/integration_test/http_client.go new file mode 100644 index 00000000000..0212ddf775e --- /dev/null +++ b/src/envoy/mixer/integration_test/http_client.go @@ -0,0 +1,80 @@ +// Copyright 2017 Istio Authors +// +// 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. + +package test + +import ( + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" +) + +func HTTPGet(url string) (code int, resp_body string, err error) { + log.Println("HTTP GET", url) + client := &http.Client{} + resp, err := client.Get(url) + if err != nil { + log.Println(err) + return 0, "", err + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + resp_body = string(body) + code = resp.StatusCode + log.Println(resp_body) + return code, resp_body, nil +} + +func HTTPPost(url string, content_type string, req_body string) (code int, resp_body string, err error) { + log.Println("HTTP POST", url) + client := &http.Client{} + resp, err := client.Post(url, content_type, strings.NewReader(req_body)) + if err != nil { + log.Println(err) + return 0, "", err + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + resp_body = string(body) + code = resp.StatusCode + log.Println(resp_body) + return code, resp_body, nil +} + +func HTTPGetWithHeaders(l string, headers map[string]string) (code int, resp_body string, err error) { + log.Println("HTTP GET with headers: ", l) + client := &http.Client{} + req := http.Request{} + + req.Header = map[string][]string{} + for k, v := range headers { + req.Header[k] = []string{v} + } + req.Method = http.MethodGet + req.URL, _ = url.Parse(l) + + resp, err := client.Do(&req) + if err != nil { + log.Println(err) + return 0, "", err + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + resp_body = string(body) + code = resp.StatusCode + log.Println(resp_body) + return code, resp_body, nil +} diff --git a/src/envoy/mixer/integration_test/http_server.go b/src/envoy/mixer/integration_test/http_server.go new file mode 100644 index 00000000000..77677286b58 --- /dev/null +++ b/src/envoy/mixer/integration_test/http_server.go @@ -0,0 +1,111 @@ +// Copyright 2017 Istio Authors +// +// 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. + +package test + +import ( + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "strings" + "time" +) + +const ( + FailHeader = "x-istio-backend-fail" + FailBody = "Bad request from backend." +) + +type HttpServer struct { + port uint16 + lis net.Listener +} + +func handler(w http.ResponseWriter, r *http.Request) { + log.Printf("%v %v %v %v\n", r.Method, r.URL, r.Proto, r.RemoteAddr) + for name, headers := range r.Header { + for _, h := range headers { + log.Printf("%v: %v\n", name, h) + } + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Fail if there is such header. + if r.Header.Get(FailHeader) != "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(FailBody)) + return + } + + // echo back the Content-Type and Content-Length in the response + for _, k := range []string{"Content-Type", "Content-Length"} { + if v := r.Header.Get(k); v != "" { + w.Header().Set(k, v) + } + } + w.WriteHeader(http.StatusOK) + w.Write(body) +} + +func NewHttpServer(port uint16) (*HttpServer, error) { + log.Printf("Http server listening on port %v\n", port) + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + log.Fatal(err) + return nil, err + } + return &HttpServer{ + port: port, + lis: lis, + }, nil +} + +func (s *HttpServer) Start() { + go func() { + http.HandleFunc("/", handler) + http.Serve(s.lis, nil) + }() + + addr := fmt.Sprintf("http://localhost:%s", s.port) + + const maxAttempts = 10 + for i := 0; i < maxAttempts; i++ { + time.Sleep(time.Second) + client := http.Client{} + log.Println("Pinging the server...") + rsp, err := client.Post( + addr+"/echo", "text/plain", strings.NewReader("PING")) + if err == nil && rsp.StatusCode == http.StatusOK { + log.Println("Got a response...") + png, err := ioutil.ReadAll(rsp.Body) + if err == nil && string(png) == "PING" { + log.Println("Server is up and running...") + return + } + } + log.Println("Will wait a second and try again.") + } +} + +func (s *HttpServer) Stop() { + log.Printf("Close HTTP server\n") + s.lis.Close() + log.Printf("Close HTTP server -- Done\n") +} diff --git a/src/envoy/mixer/integration_test/mixer_server.go b/src/envoy/mixer/integration_test/mixer_server.go new file mode 100644 index 00000000000..6e3db085e5d --- /dev/null +++ b/src/envoy/mixer/integration_test/mixer_server.go @@ -0,0 +1,127 @@ +// Copyright 2017 Istio Authors +// +// 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. + +package test + +import ( + "context" + "fmt" + "log" + "net" + + rpc "github.com/googleapis/googleapis/google/rpc" + "google.golang.org/grpc" + + mixerpb "istio.io/api/mixer/v1" + "istio.io/mixer/pkg/api" + "istio.io/mixer/pkg/attribute" + "istio.io/mixer/pkg/pool" + "istio.io/mixer/pkg/tracing" +) + +type Handler struct { + bag *attribute.MutableBag + ch chan int + count int + r_status rpc.Status +} + +func newHandler() *Handler { + return &Handler{ + bag: nil, + ch: make(chan int, 10), // Allow maximum 10 requests + count: 0, + r_status: rpc.Status{}, + } +} + +func (h *Handler) run(bag *attribute.MutableBag) rpc.Status { + h.bag = attribute.CopyBag(bag) + h.ch <- 1 + h.count++ + return h.r_status +} + +type MixerServer struct { + lis net.Listener + gs *grpc.Server + gp *pool.GoroutinePool + s mixerpb.MixerServer + + check *Handler + report *Handler + quota *Handler + quota_request *mixerpb.QuotaRequest +} + +func (ts *MixerServer) Check(ctx context.Context, bag *attribute.MutableBag, + request *mixerpb.CheckRequest, response *mixerpb.CheckResponse) { + response.RequestIndex = request.RequestIndex + response.Result = ts.check.run(bag) +} + +func (ts *MixerServer) Report(ctx context.Context, bag *attribute.MutableBag, + request *mixerpb.ReportRequest, response *mixerpb.ReportResponse) { + response.RequestIndex = request.RequestIndex + response.Result = ts.report.run(bag) +} + +func (ts *MixerServer) Quota(ctx context.Context, bag *attribute.MutableBag, + request *mixerpb.QuotaRequest, response *mixerpb.QuotaResponse) { + response.RequestIndex = request.RequestIndex + ts.quota_request = request + response.Result = ts.quota.run(bag) + response.Amount = 0 +} + +func NewMixerServer(port uint16) (*MixerServer, error) { + log.Printf("Mixer server listening on port %v\n", port) + s := &MixerServer{ + check: newHandler(), + report: newHandler(), + quota: newHandler(), + } + + var err error + s.lis, err = net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + return nil, err + } + + var opts []grpc.ServerOption + opts = append(opts, grpc.MaxConcurrentStreams(32)) + opts = append(opts, grpc.MaxMsgSize(1024*1024)) + s.gs = grpc.NewServer(opts...) + + s.gp = pool.NewGoroutinePool(128, false) + s.gp.AddWorkers(32) + + s.s = api.NewGRPCServer(s, tracing.DisabledTracer(), s.gp) + mixerpb.RegisterMixerServer(s.gs, s.s) + return s, nil +} + +func (s *MixerServer) Start() { + go func() { + _ = s.gs.Serve(s.lis) + log.Printf("Mixer server exited\n") + }() +} + +func (s *MixerServer) Stop() { + log.Printf("Stop Mixer server\n") + s.gs.Stop() + log.Printf("Stop Mixer server -- Done\n") +} diff --git a/src/envoy/mixer/integration_test/quota_test.go b/src/envoy/mixer/integration_test/quota_test.go new file mode 100644 index 00000000000..e99afb8e426 --- /dev/null +++ b/src/envoy/mixer/integration_test/quota_test.go @@ -0,0 +1,64 @@ +// Copyright 2017 Istio Authors +// +// 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. + +package test + +import ( + "fmt" + "testing" + + rpc "github.com/googleapis/googleapis/google/rpc" +) + +const ( + mixerQuotaFailMessage = "Not enough quota by mixer." +) + +func TestQuotaCall(t *testing.T) { + s, err := SetUp(t, basicConfig+","+quotaConfig) + if err != nil { + t.Fatalf("Failed to setup test: %v", err) + } + defer s.TearDown() + + url := fmt.Sprintf("http://localhost:%d/echo", ClientProxyPort) + + // Issues a GET echo request with 0 size body + tag := "OKGet" + if _, _, err := HTTPGet(url); err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + s.VerifyQuota(tag, "RequestCount", 5) + + // Issues a failed POST request caused by Mixer Quota + tag = "QuotaFail" + s.mixer.quota_request = nil + s.mixer.quota.r_status = rpc.Status{ + Code: int32(rpc.RESOURCE_EXHAUSTED), + Message: mixerQuotaFailMessage, + } + code, resp_body, err := HTTPPost(url, "text/plain", "Hello World!") + // Make sure to restore r_status for next request. + s.mixer.quota.r_status = rpc.Status{} + if err != nil { + t.Errorf("Failed in request %s: %v", tag, err) + } + if code != 429 { + t.Errorf("Status code 429 is expected.") + } + if resp_body != "RESOURCE_EXHAUSTED:"+mixerQuotaFailMessage { + t.Errorf("Error response body is not expected.") + } + s.VerifyQuota(tag, "RequestCount", 5) +} diff --git a/src/envoy/mixer/integration_test/repositories.bzl b/src/envoy/mixer/integration_test/repositories.bzl new file mode 100644 index 00000000000..7eec527fa39 --- /dev/null +++ b/src/envoy/mixer/integration_test/repositories.bzl @@ -0,0 +1,272 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_repositories", "new_go_repository", "go_repository") +load("@org_pubref_rules_protobuf//protobuf:rules.bzl", "proto_repositories") + +load("@org_pubref_rules_protobuf//gogo:rules.bzl", "gogo_proto_repositories") +load("@org_pubref_rules_protobuf//cpp:rules.bzl", "cpp_proto_repositories") + +def go_istio_api_repositories(use_local=False): + ISTIO_API_BUILD_FILE = """ +# build protos from istio.io/api repo + +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_go//go:def.bzl", "go_prefix") + +go_prefix("istio.io/api") + +load("@org_pubref_rules_protobuf//gogo:rules.bzl", "gogoslick_proto_library") + +gogoslick_proto_library( + name = "mixer/v1", + importmap = { + "gogoproto/gogo.proto": "github.com/gogo/protobuf/gogoproto", + "google/rpc/status.proto": "github.com/googleapis/googleapis/google/rpc", + "google/protobuf/timestamp.proto": "github.com/gogo/protobuf/types", + "google/protobuf/duration.proto": "github.com/gogo/protobuf/types", + }, + imports = [ + "../../external/com_github_gogo_protobuf", + "../../external/com_github_google_protobuf/src", + "../../external/com_github_googleapis_googleapis", + ], + inputs = [ + "@com_github_google_protobuf//:well_known_protos", + "@com_github_googleapis_googleapis//:status_proto", + "@com_github_gogo_protobuf//gogoproto:go_default_library_protos", + ], + protos = [ + "mixer/v1/attributes.proto", + "mixer/v1/check.proto", + "mixer/v1/quota.proto", + "mixer/v1/report.proto", + "mixer/v1/service.proto", + ], + verbose = 0, + visibility = ["//visibility:public"], + with_grpc = True, + deps = [ + "@com_github_gogo_protobuf//gogoproto:go_default_library", + "@com_github_gogo_protobuf//sortkeys:go_default_library", + "@com_github_gogo_protobuf//types:go_default_library", + "@com_github_googleapis_googleapis//:google/rpc", + ], +) + +DESCRIPTOR_FILE_GROUP = [ + "mixer/v1/config/descriptor/attribute_descriptor.proto", + "mixer/v1/config/descriptor/label_descriptor.proto", + "mixer/v1/config/descriptor/log_entry_descriptor.proto", + "mixer/v1/config/descriptor/metric_descriptor.proto", + "mixer/v1/config/descriptor/monitored_resource_descriptor.proto", + "mixer/v1/config/descriptor/principal_descriptor.proto", + "mixer/v1/config/descriptor/quota_descriptor.proto", + "mixer/v1/config/descriptor/value_type.proto", +] + +gogoslick_proto_library( + name = "mixer/v1/config", + importmap = { + "google/protobuf/struct.proto": "github.com/gogo/protobuf/types", + "mixer/v1/config/descriptor/log_entry_descriptor.proto": "istio.io/api/mixer/v1/config/descriptor", + "mixer/v1/config/descriptor/metric_descriptor.proto": "istio.io/api/mixer/v1/config/descriptor", + "mixer/v1/config/descriptor/monitored_resource_descriptor.proto": "istio.io/api/mixer/v1/config/descriptor", + "mixer/v1/config/descriptor/principal_descriptor.proto": "istio.io/api/mixer/v1/config/descriptor", + "mixer/v1/config/descriptor/quota_descriptor.proto": "istio.io/api/mixer/v1/config/descriptor", + }, + imports = [ + "../../external/com_github_google_protobuf/src", + ], + inputs = DESCRIPTOR_FILE_GROUP + [ + "@com_github_google_protobuf//:well_known_protos", + ], + protos = [ + "mixer/v1/config/cfg.proto", + ], + verbose = 0, + visibility = ["//visibility:public"], + with_grpc = False, + deps = [ + ":mixer/v1/config/descriptor", + "@com_github_gogo_protobuf//sortkeys:go_default_library", + "@com_github_gogo_protobuf//types:go_default_library", + "@com_github_googleapis_googleapis//:google/rpc", + ], +) + +gogoslick_proto_library( + name = "mixer/v1/config/descriptor", + importmap = { + "google/protobuf/duration.proto": "github.com/gogo/protobuf/types", + }, + imports = [ + "../../external/com_github_google_protobuf/src", + ], + inputs = [ + "@com_github_google_protobuf//:well_known_protos", + ], + protos = DESCRIPTOR_FILE_GROUP, + verbose = 0, + visibility = ["//visibility:public"], + with_grpc = False, + deps = [ + "@com_github_gogo_protobuf//types:go_default_library", + ], +) +""" + if use_local: + native.new_local_repository( + name = "com_github_istio_api", + build_file_content = ISTIO_API_BUILD_FILE, + path = "../api", + ) + else: + native.new_git_repository( + name = "com_github_istio_api", + build_file_content = ISTIO_API_BUILD_FILE, + commit = "2cb09827d7f09a6e88eac2c2249dcb45c5419f09", # Mar. 14, 2017 (no releases) + remote = "https://github.com/istio/api.git", + ) + +def go_googleapis_repositories(): + GOOGLEAPIS_BUILD_FILE = """ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_go//go:def.bzl", "go_prefix") +go_prefix("github.com/googleapis/googleapis") + +load("@org_pubref_rules_protobuf//gogo:rules.bzl", "gogoslick_proto_library") + +gogoslick_proto_library( + name = "google/rpc", + protos = [ + "google/rpc/code.proto", + "google/rpc/error_details.proto", + "google/rpc/status.proto", + ], + importmap = { + "google/protobuf/any.proto": "github.com/gogo/protobuf/types", + "google/protobuf/duration.proto": "github.com/gogo/protobuf/types", + }, + imports = [ + "../../external/com_github_google_protobuf/src", + ], + inputs = [ + "@com_github_google_protobuf//:well_known_protos", + ], + deps = [ + "@com_github_gogo_protobuf//types:go_default_library", + ], + verbose = 0, +) + +load("@org_pubref_rules_protobuf//cpp:rules.bzl", "cc_proto_library") + +cc_proto_library( + name = "cc_status_proto", + protos = [ + "google/rpc/status.proto", + ], + imports = [ + "../../external/com_github_google_protobuf/src", + ], + verbose = 0, +) + +filegroup( + name = "status_proto", + srcs = [ "google/rpc/status.proto" ], +) + +filegroup( + name = "code_proto", + srcs = [ "google/rpc/code.proto" ], +) +""" + native.new_git_repository( + name = "com_github_googleapis_googleapis", + build_file_content = GOOGLEAPIS_BUILD_FILE, + commit = "13ac2436c5e3d568bd0e938f6ed58b77a48aba15", # Oct 21, 2016 (only release pre-dates sha) + remote = "https://github.com/googleapis/googleapis.git", + ) + +def go_mixer_repositories(use_local_api=False): + go_istio_api_repositories(use_local_api) + go_googleapis_repositories() + + go_repositories() + proto_repositories() + + gogo_proto_repositories() + + new_go_repository( + name = "com_github_golang_glog", + commit = "23def4e6c14b4da8ac2ed8007337bc5eb5007998", # Jan 26, 2016 (no releases) + importpath = "github.com/golang/glog", + ) + + new_go_repository( + name = "com_github_ghodss_yaml", + commit = "04f313413ffd65ce25f2541bfd2b2ceec5c0908c", # Dec 6, 2016 (no releases) + importpath = "github.com/ghodss/yaml", + ) + + new_go_repository( + name = "in_gopkg_yaml_v2", + commit = "14227de293ca979cf205cd88769fe71ed96a97e2", # Jan 24, 2017 (no releases) + importpath = "gopkg.in/yaml.v2", + ) + + new_go_repository( + name = "com_github_golang_protobuf", + commit = "8ee79997227bf9b34611aee7946ae64735e6fd93", # Nov 16, 2016 (no releases) + importpath = "github.com/golang/protobuf", + ) + + new_go_repository( + name = "org_golang_google_grpc", + commit = "708a7f9f3283aa2d4f6132d287d78683babe55c8", # Dec 5, 2016 (v1.0.5) + importpath = "google.golang.org/grpc", + ) + + new_go_repository( + name = "com_github_spf13_cobra", + commit = "35136c09d8da66b901337c6e86fd8e88a1a255bd", # Jan 30, 2017 (no releases) + importpath = "github.com/spf13/cobra", + ) + + new_go_repository( + name = "com_github_spf13_pflag", + commit = "9ff6c6923cfffbcd502984b8e0c80539a94968b7", # Jan 30, 2017 (no releases) + importpath = "github.com/spf13/pflag", + ) + + new_go_repository( + name = "com_github_hashicorp_go_multierror", + commit = "ed905158d87462226a13fe39ddf685ea65f1c11f", # Dec 16, 2016 (no releases) + importpath = "github.com/hashicorp/go-multierror", + ) + + new_go_repository( + name = "com_github_hashicorp_errwrap", + commit = "7554cd9344cec97297fa6649b055a8c98c2a1e55", # Oct 27, 2014 (no releases) + importpath = "github.com/hashicorp/errwrap", + ) + + new_go_repository( + name = "com_github_opentracing_opentracing_go", + commit = "0c3154a3c2ce79d3271985848659870599dfb77c", # Sep 26, 2016 (v1.0.0) + importpath = "github.com/opentracing/opentracing-go", + ) + + new_go_repository( + name = "com_github_opentracing_basictracer", + commit = "1b32af207119a14b1b231d451df3ed04a72efebf", # Sep 29, 2016 (no releases) + importpath = "github.com/opentracing/basictracer-go", + ) + + go_repository( + name = "com_github_istio_mixer", + commit = "064001053b51f73adc3a80ff87ef41a15316c300", + importpath = "github.com/istio/mixer", + ) + diff --git a/src/envoy/mixer/integration_test/setup.go b/src/envoy/mixer/integration_test/setup.go new file mode 100644 index 00000000000..52bfdd2f711 --- /dev/null +++ b/src/envoy/mixer/integration_test/setup.go @@ -0,0 +1,93 @@ +// Copyright 2017 Istio Authors +// +// 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. + +package test + +import ( + "log" + "testing" +) + +type TestSetup struct { + envoy *Envoy + mixer *MixerServer + backend *HttpServer + t *testing.T +} + +func SetUp(t *testing.T, conf string) (s TestSetup, err error) { + s.t = t + s.envoy, err = NewEnvoy(conf) + if err != nil { + log.Printf("unable to create Envoy %v", err) + } else { + s.envoy.Start() + } + + s.mixer, err = NewMixerServer(MixerPort) + if err != nil { + log.Printf("unable to create mixer server %v", err) + } else { + s.mixer.Start() + } + + s.backend, err = NewHttpServer(BackendPort) + if err != nil { + log.Printf("unable to create HTTP server %v", err) + } else { + s.backend.Start() + } + return s, err +} + +func (s *TestSetup) TearDown() { + s.envoy.Stop() + s.mixer.Stop() + s.backend.Stop() +} + +func (s *TestSetup) VerifyCheckCount(tag string, expected int) { + if s.mixer.check.count != expected { + s.t.Fatalf("%s check count doesn't match: %v\n, expected: %+v", + tag, s.mixer.check.count, expected) + } +} + +func (s *TestSetup) VerifyCheck(tag string, result string) { + _ = <-s.mixer.check.ch + if err := Verify(s.mixer.check.bag, result); err != nil { + s.t.Fatalf("Failed to verify %s check: %v\n, Attributes: %+v", + tag, err, s.mixer.check.bag) + } +} + +func (s *TestSetup) VerifyReport(tag string, result string) { + _ = <-s.mixer.report.ch + if err := Verify(s.mixer.report.bag, result); err != nil { + s.t.Fatalf("Failed to verify %s report: %v\n, Attributes: %+v", + tag, err, s.mixer.report.bag) + } +} + +func (s *TestSetup) VerifyQuota(tag string, name string, amount int64) { + _ = <-s.mixer.quota.ch + if s.mixer.quota_request.Quota != name { + s.t.Fatalf("Failed to verify %s quota name: %v, expected: %v\n", + tag, s.mixer.quota_request.Quota, name) + } + if s.mixer.quota_request.Amount != amount { + s.t.Fatalf("Failed to verify %s quota amount: %v, expected: %v\n", + tag, s.mixer.quota_request.Amount, amount) + } +} diff --git a/src/envoy/mixer/integration_test/test_suite.bzl b/src/envoy/mixer/integration_test/test_suite.bzl new file mode 100644 index 00000000000..afa984f4773 --- /dev/null +++ b/src/envoy/mixer/integration_test/test_suite.bzl @@ -0,0 +1,29 @@ +# Copyright 2017 Istio Authors. 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. +# +################################################################################ +# + +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +def go_test_suite(tests, library, data, tags, size="small"): + for test in tests: + go_test( + name = test.split(".")[0], + size = size, + srcs = [test], + data = data, + library = library, + tags = tags, + ) diff --git a/src/envoy/mixer/repositories.bzl b/src/envoy/mixer/repositories.bzl index 473e2650882..4382e71f921 100644 --- a/src/envoy/mixer/repositories.bzl +++ b/src/envoy/mixer/repositories.bzl @@ -15,7 +15,7 @@ ################################################################################ # -MIXER_CLIENT = "92a305961bcea90ed349ffdbb0ea299c6f6bacad" +MIXER_CLIENT = "c5d857e28bfcc53f20f59466b464f99526737545" def mixer_client_repositories(bind=True): native.git_repository( diff --git a/src/envoy/mixer/utils.cc b/src/envoy/mixer/utils.cc index 6dc3a78356d..32b09667208 100644 --- a/src/envoy/mixer/utils.cc +++ b/src/envoy/mixer/utils.cc @@ -21,20 +21,6 @@ namespace Utils { const LowerCaseString kIstioAttributeHeader("x-istio-attributes"); -StringMap ExtractStringMap(const Json::Object& json, const std::string& name) { - StringMap map; - if (json.hasObject(name)) { - Json::ObjectPtr json_obj = json.getObject(name); - Json::Object* raw_obj = json_obj.get(); - json_obj->iterate( - [&map, raw_obj](const std::string& key, const Json::Object&) -> bool { - map[key] = raw_obj->getString(key); - return true; - }); - } - return map; -} - std::string SerializeStringMap(const StringMap& string_map) { ::istio::proxy::mixer::StringMap pb; ::google::protobuf::Map* map_pb = pb.mutable_map(); diff --git a/src/envoy/mixer/utils.h b/src/envoy/mixer/utils.h index 9273a9a2d8b..e94ed91706c 100644 --- a/src/envoy/mixer/utils.h +++ b/src/envoy/mixer/utils.h @@ -29,9 +29,6 @@ extern const LowerCaseString kIstioAttributeHeader; // The string map. typedef std::map StringMap; -// Extracts name/value attributes from a json object. -StringMap ExtractStringMap(const Json::Object& json, const std::string& name); - // Serialize a string map to string. std::string SerializeStringMap(const StringMap& map); diff --git a/src/envoy/repositories.bzl b/src/envoy/repositories.bzl index dffa32438a0..bad332e188d 100644 --- a/src/envoy/repositories.bzl +++ b/src/envoy/repositories.bzl @@ -235,25 +235,6 @@ def lightstep_repositories(bind=True): BUILD = """ load("@protobuf_git//:protobuf.bzl", "cc_proto_library") -genrule( - name = "envoy_carrier_pb", - srcs = ["src/c++11/envoy/envoy_carrier.proto"], - outs = ["lightstep/envoy_carrier.proto"], - cmd = "cp $(SRCS) $@", -) - -cc_proto_library( - name = "envoy_carrier_proto", - srcs = ["lightstep/envoy_carrier.proto"], - include = ".", - deps = [ - "//external:cc_wkt_protos", - ], - protoc = "//external:protoc", - default_runtime = "//external:protobuf", - visibility = ["//visibility:public"], -) - cc_library( name = "lightstep_core", srcs = [ @@ -266,7 +247,7 @@ cc_library( "src/c++11/lightstep/impl.h", "src/c++11/lightstep/options.h", "src/c++11/lightstep/propagation.h", - "src/c++11/lightstep/envoy.h", + "src/c++11/lightstep/carrier.h", "src/c++11/lightstep/span.h", "src/c++11/lightstep/tracer.h", "src/c++11/lightstep/util.h", @@ -275,7 +256,7 @@ cc_library( "src/c++11/mapbox_variant/variant.hpp", ], copts = [ - "-DPACKAGE_VERSION='\\"0.19\\"'", + "-DPACKAGE_VERSION='\\"0.36\\"'", "-Iexternal/lightstep_git/src/c++11/lightstep", "-Iexternal/lightstep_git/src/c++11/mapbox_variant", ], @@ -285,7 +266,7 @@ cc_library( visibility = ["//visibility:public"], deps = [ "@lightstep_common_git//:collector_proto", - ":envoy_carrier_proto", + "@lightstep_common_git//:lightstep_carrier_proto", "//external:protobuf", ], )""" @@ -293,16 +274,21 @@ cc_library( COMMON_BUILD = """ load("@protobuf_git//:protobuf.bzl", "cc_proto_library") -genrule( - name = "collector_pb", +cc_proto_library( + name = "collector_proto", srcs = ["collector.proto"], - outs = ["lightstep/collector.proto"], - cmd = "cp $(SRCS) $@", + include = ".", + deps = [ + "//external:cc_wkt_protos", + ], + protoc = "//external:protoc", + default_runtime = "//external:protobuf", + visibility = ["//visibility:public"], ) cc_proto_library( - name = "collector_proto", - srcs = ["lightstep/collector.proto"], + name = "lightstep_carrier_proto", + srcs = ["lightstep_carrier.proto"], include = ".", deps = [ "//external:cc_wkt_protos", @@ -310,19 +296,20 @@ cc_proto_library( protoc = "//external:protoc", default_runtime = "//external:protobuf", visibility = ["//visibility:public"], -)""" +) +""" native.new_git_repository( name = "lightstep_common_git", remote = "https://github.com/lightstep/lightstep-tracer-common.git", - commit = "8d932f7f76cd286691e6179621d0012b0ff1e6aa", + commit = "cbbecd671c1ae1f20ae873c5da688c8c14d04ec3", build_file_content = COMMON_BUILD, ) native.new_git_repository( name = "lightstep_git", remote = "https://github.com/lightstep/lightstep-tracer-cpp.git", - commit = "5a71d623cac17a059041b04fabca4ed86ffff7cc", + commit = "f1dc8f3dfd529350e053fd21273e627f409ae428", # 0.36 build_file_content = BUILD, ) @@ -572,185 +559,14 @@ def envoy_repositories(bind=True): rapidjson_repositories(bind) nghttp2_repositories(bind) ares_repositories(bind) - - BUILD = """ -load("@protobuf_git//:protobuf.bzl", "cc_proto_library") - -exports_files(["source/precompiled/precompiled.h"]) - -package(default_visibility = ["//visibility:public"]) - -genrule( - name = "envoy-ratelimit-proto", - srcs = [ - "source/common/ratelimit/ratelimit.proto", - ], - outs = [ - "source/common/generated/ratelimit.proto", - ], - cmd = "cp $(SRCS) $@", -) - -cc_proto_library( - name = "envoy-ratelimit-pb", - srcs = [ - "source/common/generated/ratelimit.proto", - ], - default_runtime = "//external:protobuf", - protoc = "//external:protoc", - include = "source", -) - -genrule( - name = "envoy-test-proto", - srcs = [ - "test/proto/helloworld.proto", - ], - outs = [ - "test/generated/helloworld.proto", - ], - cmd = "cp $(SRCS) $@", -) - -cc_proto_library( - name = "envoy-test-pb", - srcs = [ - "test/generated/helloworld.proto", - ], - default_runtime = "//external:protobuf", - protoc = "//external:protoc", - include = "test", -) - -genrule( - name = "envoy-version", - srcs = glob([ - ".git/**", - ]), - tools = [ - "tools/gen_git_sha.sh", - ], - outs = [ - "source/common/version_generated.cc", - ], - cmd = "touch $@ && $(location tools/gen_git_sha.sh) $$(dirname $(location tools/gen_git_sha.sh)) $@", - local = 1, -) - -cc_library( - name = "envoy-common", - srcs = glob([ - "source/**/*.cc", - "source/**/*.h", - "include/**/*.h", - ], exclude=["source/exe/main.cc"]) + [ - "source/common/version_generated.cc", - ], - copts = [ - "-I./external/envoy_git/source", - "-include ./external/envoy_git/source/precompiled/precompiled.h", - ], - includes = [ - "include", - ], - linkopts = [ - "-lpthread", - "-lanl", - "-lrt", - ], - linkstatic=1, - alwayslink=1, - deps = [ - ":envoy-ratelimit-pb", - "//external:ares", - "//external:libssl", - "//external:nghttp2", - "//external:spdlog", - "//external:tclap", - "//external:lightstep", - "//external:event", - "//external:protobuf", - "//external:http_parser", - "//external:rapidjson", - "//external:event_pthreads", - ], -) - -cc_library( - name = "envoy-main", - srcs = [ - "source/exe/main.cc", - ], - copts = [ - "-I./external/envoy_git/source", - "-include ./external/envoy_git/source/precompiled/precompiled.h", - ], - deps = [ - ":envoy-common", - ], - linkstatic=1, -) - -cc_binary( - name = "envoy", - srcs = [ - "source/exe/main.cc", - ], - copts = [ - "-I./external/envoy_git/source", - "-include ./external/envoy_git/source/precompiled/precompiled.h", - ], - deps = [ - ":envoy-common", - ], - linkstatic=1, -) - -cc_library( - name = "envoy-test-lib", - srcs = glob([ - "test/**/*.cc", - "test/**/*.h", - ]), - copts = [ - "-I./external/envoy_git/source", - "-include ./external/envoy_git/test/precompiled/precompiled_test.h", - ], - includes = [ - "include", - ], - deps = [ - ":envoy-common", - ":envoy-test-pb", - "//external:googletest", - ], - alwayslink=1, -) - -filegroup( - name = "envoy-testdata", - srcs = glob([ - "generated/**/*", - "test/**/*", - ]), -) - -cc_test( - name = "envoy-test", - data = [ - ":envoy-testdata", - ], - deps = [ - ":envoy-test-lib", - ":envoy-test-pb", - "//external:googletest", - ], - linkstatic=1, -)""" - - native.new_git_repository( - name = "envoy_git", + # @boringssl is defined in //:repositories.bzl, but bound to libssl for + # grpc. Rebind to what envoy expects here. + native.bind( + name = "ssl", + actual = "@boringssl//:ssl", + ) + native.git_repository( + name = "envoy", remote = "https://github.com/lyft/envoy.git", - commit = "9dcac8ca111ecc8da059d1f8d42eb766b44bacd6", # https://github.com/lyft/envoy/pull/553 - build_file_content = BUILD, + commit = "bf3f23ad439ee83b91015dc4d0d7cb53b14bf1bc", ) diff --git a/src/envoy/transcoding/BUILD b/src/envoy/transcoding/BUILD new file mode 100644 index 00000000000..8e15a45eadb --- /dev/null +++ b/src/envoy/transcoding/BUILD @@ -0,0 +1,40 @@ +# 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. +# +################################################################################ +# +cc_library( + name = "envoy_input_stream", + srcs = [ + "envoy_input_stream.cc", + ], + hdrs = [ + "envoy_input_stream.h", + ], + deps = [ + "//contrib/endpoints/src/grpc/transcoding:transcoder_input_stream", + "@envoy//source/exe:envoy_common_lib", + ], +) + +cc_test( + name = "envoy_input_stream_test", + srcs = [ + "envoy_input_stream_test.cc", + ], + deps = [ + ":envoy_input_stream", + "@googletest_git//:googletest_main", + ], +) diff --git a/src/envoy/transcoding/envoy_input_stream.cc b/src/envoy/transcoding/envoy_input_stream.cc new file mode 100644 index 00000000000..81334303a7a --- /dev/null +++ b/src/envoy/transcoding/envoy_input_stream.cc @@ -0,0 +1,63 @@ +/* Copyright 2017 Istio Authors. 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 "src/envoy/transcoding/envoy_input_stream.h" + +namespace Grpc { + +void EnvoyInputStream::Move(Buffer::Instance &instance) { + if (!finished_) { + buffer_.move(instance); + } +} + +bool EnvoyInputStream::Next(const void **data, int *size) { + if (position_ != 0) { + buffer_.drain(position_); + position_ = 0; + } + + Buffer::RawSlice slice; + uint64_t num_slices = buffer_.getRawSlices(&slice, 1); + + if (num_slices) { + *data = slice.mem_; + *size = slice.len_; + position_ = slice.len_; + byte_count_ += slice.len_; + return true; + } + + if (!finished_) { + *data = nullptr; + *size = 0; + return true; + } + return false; +} + +void EnvoyInputStream::BackUp(int count) { + GOOGLE_CHECK_GE(count, 0); + GOOGLE_CHECK_LE(count, position_); + + position_ -= count; + byte_count_ -= count; +} + +int64_t EnvoyInputStream::BytesAvailable() const { + return buffer_.length() - position_; +} + +} // namespace Grpc \ No newline at end of file diff --git a/src/envoy/transcoding/envoy_input_stream.h b/src/envoy/transcoding/envoy_input_stream.h new file mode 100644 index 00000000000..3e204e21eec --- /dev/null +++ b/src/envoy/transcoding/envoy_input_stream.h @@ -0,0 +1,51 @@ +/* Copyright 2017 Istio Authors. 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. + */ + +#pragma once + +#include "precompiled/precompiled.h" + +#include "common/buffer/buffer_impl.h" +#include "contrib/endpoints/src/grpc/transcoding/transcoder_input_stream.h" + +namespace Grpc { + +class EnvoyInputStream + : public google::api_manager::transcoding::TranscoderInputStream { + public: + // Add a buffer to input stream, will consume all buffer from parameter + // if the stream is not finished + void Move(Buffer::Instance &instance); + + // Mark the buffer is finished + void Finish() { finished_ = true; } + + // TranscoderInputStream + virtual bool Next(const void **data, int *size) override; + virtual void BackUp(int count) override; + virtual bool Skip(int count) override { return false; } // Not implemented + virtual google::protobuf::int64 ByteCount() const override { + return byte_count_; + } + virtual int64_t BytesAvailable() const override; + + private: + Buffer::OwnedImpl buffer_; + int position_{0}; + int64_t byte_count_{0}; + bool finished_{false}; +}; + +} // namespace Grpc \ No newline at end of file diff --git a/src/envoy/transcoding/envoy_input_stream_test.cc b/src/envoy/transcoding/envoy_input_stream_test.cc new file mode 100644 index 00000000000..e8604367b91 --- /dev/null +++ b/src/envoy/transcoding/envoy_input_stream_test.cc @@ -0,0 +1,97 @@ +/* Copyright 2017 Istio Authors. 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 "src/envoy/transcoding/envoy_input_stream.h" + +#include "gtest/gtest.h" + +namespace Grpc { +namespace { + +class EnvoyInputStreamTest : public testing::Test { + public: + EnvoyInputStreamTest() { + Buffer::OwnedImpl buffer{"abcd"}; + stream_.Move(buffer); + } + + std::string slice_data_{"abcd"}; + EnvoyInputStream stream_; + + const void *data_; + int size_; +}; + +TEST_F(EnvoyInputStreamTest, Move) { + Buffer::OwnedImpl buffer{"abcd"}; + stream_.Move(buffer); + + EXPECT_EQ(0, buffer.length()); + EXPECT_EQ(8, stream_.BytesAvailable()); +} + +TEST_F(EnvoyInputStreamTest, Next) { + EXPECT_TRUE(stream_.Next(&data_, &size_)); + EXPECT_EQ(4, size_); + EXPECT_EQ(0, memcmp(slice_data_.data(), data_, size_)); +} + +TEST_F(EnvoyInputStreamTest, TwoSlices) { + Buffer::OwnedImpl buffer("efgh"); + + stream_.Move(buffer); + + EXPECT_TRUE(stream_.Next(&data_, &size_)); + EXPECT_EQ(4, size_); + EXPECT_EQ(0, memcmp(slice_data_.data(), data_, size_)); + EXPECT_TRUE(stream_.Next(&data_, &size_)); + EXPECT_EQ(4, size_); + EXPECT_EQ(0, memcmp("efgh", data_, size_)); +} + +TEST_F(EnvoyInputStreamTest, BackUp) { + EXPECT_TRUE(stream_.Next(&data_, &size_)); + EXPECT_EQ(4, size_); + + stream_.BackUp(3); + EXPECT_EQ(3, stream_.BytesAvailable()); + EXPECT_EQ(1, stream_.ByteCount()); + + EXPECT_TRUE(stream_.Next(&data_, &size_)); + EXPECT_EQ(3, size_); + EXPECT_EQ(0, memcmp("bcd", data_, size_)); + EXPECT_EQ(4, stream_.ByteCount()); +} + +TEST_F(EnvoyInputStreamTest, ByteCount) { + EXPECT_EQ(0, stream_.ByteCount()); + EXPECT_TRUE(stream_.Next(&data_, &size_)); + EXPECT_EQ(4, stream_.ByteCount()); +} + +TEST_F(EnvoyInputStreamTest, Finish) { + EXPECT_TRUE(stream_.Next(&data_, &size_)); + EXPECT_TRUE(stream_.Next(&data_, &size_)); + EXPECT_EQ(0, size_); + stream_.Finish(); + EXPECT_FALSE(stream_.Next(&data_, &size_)); + + Buffer::OwnedImpl buffer("efgh"); + stream_.Move(buffer); + + EXPECT_EQ(4, buffer.length()); +} +} +} \ No newline at end of file diff --git a/.bazelrc b/tools/bazel.rc similarity index 100% rename from .bazelrc rename to tools/bazel.rc diff --git a/tools/bazel.rc.jenkins b/tools/bazel.rc.jenkins new file mode 100644 index 00000000000..664f7b13380 --- /dev/null +++ b/tools/bazel.rc.jenkins @@ -0,0 +1,8 @@ +# This is from Bazel's former travis setup, to avoid blowing up the RAM usage. +startup --host_jvm_args=-Xmx8192m +startup --host_jvm_args=-Xms8192m +startup --batch + +# This is so we understand failures better +build --verbose_failures + diff --git a/.bazelrc.travis b/tools/bazel.rc.travis similarity index 100% rename from .bazelrc.travis rename to tools/bazel.rc.travis