diff --git a/src/envoy/mixer/README.md b/src/envoy/mixer/README.md index 018de6ab206..5cfb5f57fcb 100644 --- a/src/envoy/mixer/README.md +++ b/src/envoy/mixer/README.md @@ -66,7 +66,8 @@ This filter will intercept all HTTP requests and call Mixer. Here is its config: "mixer_server": "${MIXER_SERVER}", "mixer_attributes" : { "attribute_name1": "attribute_value1", - "attribute_name2": "attribute_value2" + "attribute_name2": "attribute_value2", + "quota.name": "RequestCount" }, "forward_attributes" : { "attribute_name1": "attribute_value1", @@ -79,6 +80,7 @@ Notes: * mixer_server is required * mixer_attributes: these attributes will be send to the mixer * forward_attributes: these attributes will be forwarded to the upstream istio/proxy. +* "quota.name" and "quota.amount" are used for quota call. "quota.amount" is default to 1 if missing. By default, mixer filter forwards attributes and does not invoke mixer server. You can customize this behavior per HTTP route by supplying an opaque config: diff --git a/src/envoy/mixer/envoy.conf.template b/src/envoy/mixer/envoy.conf.template index 86aa0e1dc33..1ca8ffc62b3 100644 --- a/src/envoy/mixer/envoy.conf.template +++ b/src/envoy/mixer/envoy.conf.template @@ -42,7 +42,8 @@ "mixer_server": "${MIXER_SERVER}", "mixer_attributes": { "target.uid": "POD222", - "target.namespace": "XYZ222" + "target.namespace": "XYZ222", + "quota.name": "RequestCount" } } }, diff --git a/src/envoy/mixer/http_control.cc b/src/envoy/mixer/http_control.cc index 50a637ca356..0e015b58ba6 100644 --- a/src/envoy/mixer/http_control.cc +++ b/src/envoy/mixer/http_control.cc @@ -114,6 +114,23 @@ HttpControl::HttpControl(const std::string& mixer_server, ::istio::mixer_client::MixerClientOptions options; options.mixer_server = mixer_server; mixer_client_ = ::istio::mixer_client::CreateMixerClient(options); + + // Extract quota attributes + auto it = config_attributes_.find(::istio::mixer_client::kQuotaName); + if (it != config_attributes_.end()) { + quota_attributes_.attributes[ ::istio::mixer_client::kQuotaName] = + Attributes::StringValue(it->second); + config_attributes_.erase(it); + + int64_t amount = 1; // default amount to 1. + it = config_attributes_.find(::istio::mixer_client::kQuotaAmount); + if (it != config_attributes_.end()) { + amount = std::stoi(it->second); + config_attributes_.erase(it); + } + quota_attributes_.attributes[ ::istio::mixer_client::kQuotaAmount] = + Attributes::Int64Value(amount); + } } void HttpControl::FillCheckAttributes(HeaderMap& header_map, Attributes* attr) { @@ -141,7 +158,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..9405945d714 100644 --- a/src/envoy/mixer/http_control.h +++ b/src/envoy/mixer/http_control.h @@ -58,6 +58,8 @@ class HttpControl final : public Logger::Loggable { std::unique_ptr<::istio::mixer_client::MixerClient> mixer_client_; // The attributes read from the config file. std::map config_attributes_; + // Quota attributes; extracted from envoy filter config. + ::istio::mixer_client::Attributes quota_attributes_; }; } // namespace Mixer diff --git a/src/envoy/mixer/integration_test/envoy.conf b/src/envoy/mixer/integration_test/envoy.conf index 2083d7f4e5a..4d993753f8b 100644 --- a/src/envoy/mixer/integration_test/envoy.conf +++ b/src/envoy/mixer/integration_test/envoy.conf @@ -42,7 +42,9 @@ "mixer_server": "localhost:29091", "mixer_attributes": { "target.uid": "POD222", - "target.namespace": "XYZ222" + "target.namespace": "XYZ222", + "quota.name": "RequestCount", + "quota.amount": "5" } } }, diff --git a/src/envoy/mixer/integration_test/mixer_server.go b/src/envoy/mixer/integration_test/mixer_server.go index e36f04b484a..fbe5a5bf9c5 100644 --- a/src/envoy/mixer/integration_test/mixer_server.go +++ b/src/envoy/mixer/integration_test/mixer_server.go @@ -56,9 +56,10 @@ type MixerServer struct { gp *pool.GoroutinePool s mixerpb.MixerServer - check *Handler - report *Handler - quota *Handler + check *Handler + report *Handler + quota *Handler + quota_request *mixerpb.QuotaRequest } func (ts *MixerServer) Check(ctx context.Context, bag *attribute.MutableBag, @@ -76,6 +77,7 @@ func (ts *MixerServer) Report(ctx context.Context, bag *attribute.MutableBag, 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 } diff --git a/src/envoy/mixer/integration_test/mixer_test.go b/src/envoy/mixer/integration_test/mixer_test.go index b8f382bd58c..862391f873a 100644 --- a/src/envoy/mixer/integration_test/mixer_test.go +++ b/src/envoy/mixer/integration_test/mixer_test.go @@ -22,7 +22,8 @@ import ( ) const ( - mixerFailMessage = "Unauthenticated by mixer." + mixerAuthFailMessage = "Unauthenticated by mixer." + mixerQuotaFailMessage = "Not enough quota by mixer." ) // Attributes verification rules @@ -134,14 +135,14 @@ const reportAttributesOkPost = ` }, "request.size": 12, "response.time": "*", - "response.size": 12, + "response.size": 45, "response.latency": "*", - "response.http.code": 200, + "response.http.code": 429, "response.headers": { "date": "*", "content-type": "text/plain", - "content-length": "12", - ":status": "200", + "content-length": "45", + ":status": "429", "server": "envoy" } } @@ -262,6 +263,7 @@ func verifyAttributes( t.Fatalf("Failed to verify %s check: %v\n, Attributes: %+v", tag, err, s.mixer.check.bag) } + _ = <-s.mixer.report.ch if err := Verify(s.mixer.report.bag, report); err != nil { t.Fatalf("Failed to verify %s report: %v\n, Attributes: %+v", @@ -269,6 +271,18 @@ func verifyAttributes( } } +func verifyQuota(s *TestSetup, tag string, t *testing.T) { + _ = <-s.mixer.quota.ch + if s.mixer.quota_request.Quota != "RequestCount" { + t.Fatalf("Failed to verify %s quota name (=RequestCount): %v\n", + tag, s.mixer.quota_request.Quota) + } + if s.mixer.quota_request.Amount != 5 { + t.Fatalf("Failed to verify %s quota amount (=5): %v\n", + tag, s.mixer.quota_request.Amount) + } +} + func TestMixer(t *testing.T) { s, err := SetUp() if err != nil { @@ -288,20 +302,36 @@ func TestMixer(t *testing.T) { } verifyAttributes(&s, "OkGet", checkAttributesOkGet, reportAttributesOkGet, t) + verifyQuota(&s, "OkGet", t) - // Issues a POST echo request with - if _, _, err := HTTPPost(url, "text/plain", "Hello World!"); err != nil { + // Issues a failed POST request caused by Mixer Quota + 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 POST request: %v", 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.") + } verifyAttributes(&s, "OkPost", checkAttributesOkPost, reportAttributesOkPost, t) + verifyQuota(&s, "OkPost", t) // Issues a failed request caused by mixer s.mixer.check.r_status = rpc.Status{ Code: int32(rpc.UNAUTHENTICATED), - Message: mixerFailMessage, + Message: mixerAuthFailMessage, } - code, resp_body, err := HTTPGet(url) + 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 GET request: error: %v", err) @@ -309,11 +339,12 @@ func TestMixer(t *testing.T) { if code != 401 { t.Errorf("Status code 401 is expected.") } - if resp_body != "UNAUTHENTICATED:"+mixerFailMessage { + if resp_body != "UNAUTHENTICATED:"+mixerAuthFailMessage { t.Errorf("Error response body is not expected.") } verifyAttributes(&s, "MixerFail", checkAttributesMixerFail, reportAttributesMixerFail, t) + // Not quota call due to Mixer failure. // Issues a failed request caused by backend headers := map[string]string{} @@ -330,4 +361,5 @@ func TestMixer(t *testing.T) { } verifyAttributes(&s, "BackendFail", checkAttributesBackendFail, reportAttributesBackendFail, t) + verifyQuota(&s, "BackendFail", t) } diff --git a/src/envoy/mixer/repositories.bzl b/src/envoy/mixer/repositories.bzl index 9d06b24a14b..7edd7e89834 100644 --- a/src/envoy/mixer/repositories.bzl +++ b/src/envoy/mixer/repositories.bzl @@ -15,7 +15,7 @@ ################################################################################ # -MIXER_CLIENT = "1d6b587755846fe1b3a44fb53e3ab9ce0534af2c" +MIXER_CLIENT = "b76b5131c4650cefff4af7e4267883a33d66bca1" def mixer_client_repositories(bind=True): native.git_repository(