diff --git a/src/envoy/mixer/BUILD b/src/envoy/mixer/BUILD new file mode 100644 index 00000000000..428c87101b9 --- /dev/null +++ b/src/envoy/mixer/BUILD @@ -0,0 +1,41 @@ +# 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. +# +################################################################################ +# + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "filter_lib", + srcs = [ + "http_control.cc", + "http_control.h", + "http_filter.cc", + ], + deps = [ + "//external:mixer_client_lib", + "@envoy_git//:envoy-common", + ], + alwayslink = 1, +) + +cc_binary( + name = "envoy_esp", + linkstatic = 1, + deps = [ + ":filter_lib", + "@envoy_git//:envoy-main", + ], +) diff --git a/src/envoy/mixer/README.md b/src/envoy/mixer/README.md new file mode 100644 index 00000000000..94276e37826 --- /dev/null +++ b/src/envoy/mixer/README.md @@ -0,0 +1,49 @@ + +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: + +``` + cd $(ISTIO)/mixer + bazel build ...:all +``` + +## Build Envoy proxy + +* Build target envoy_esp: + +``` + bazel build //src/envoy/mixer:envoy_esp +``` + +## How to run it + +* Start mixer server. In mixer folder run: + +``` + bazel-bin/cmd/server/mixs server +``` + + The server will run at port 9091 + +* Start backend Echo server. + +``` + cd test/backend/echo + go run echo.go +``` + +* Start Envoy proxy, run + +``` + bazel-bin/src/envoy/mixer/envoy_esp -c src/envoy/prototype/envoy-mixer.conf +``` + +* Then issue HTTP request to proxy. + +``` + curl http://localhost:9090/echo -d "hello world" +``` + diff --git a/src/envoy/mixer/envoy-mixer.conf b/src/envoy/mixer/envoy-mixer.conf new file mode 100644 index 00000000000..3b494c96b59 --- /dev/null +++ b/src/envoy/mixer/envoy-mixer.conf @@ -0,0 +1,71 @@ +{ + "listeners": [ + { + "port": 9090, + "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" + } + ] + } + ] + }, + "access_log": [ + { + "path": "/dev/stdout" + } + ], + "filters": [ + { + "type": "both", + "name": "mixer", + "config": { + "mixer_server": "localhost:9091" + } + }, + { + "type": "decoder", + "name": "router", + "config": {} + } + ] + } + } + ] + } + ], + "admin": { + "access_log_path": "/dev/stdout", + "port": 9001 + }, + "cluster_manager": { + "clusters": [ + { + "name": "service1", + "connect_timeout_ms": 5000, + "type": "strict_dns", + "lb_type": "round_robin", + "hosts": [ + { + "url": "tcp://localhost:8080" + } + ] + } + ] + } +} diff --git a/src/envoy/mixer/http_control.cc b/src/envoy/mixer/http_control.cc new file mode 100644 index 00000000000..39e96ecbd3c --- /dev/null +++ b/src/envoy/mixer/http_control.cc @@ -0,0 +1,184 @@ +/* + * 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/http_control.h" +#include "common/common/utility.h" +#include "common/http/utility.h" + +using ::google::protobuf::util::Status; +using ::istio::mixer_client::Attributes; +using ::istio::mixer_client::DoneFunc; + +namespace Http { +namespace Mixer { +namespace { + +const std::string kProxyPeerID = "Istio/Proxy"; +const std::string kEnvNameTargetService = "TARGET_SERVICE"; + +// Define lower case string for X-Forwarded-Host. +const LowerCaseString kHeaderNameXFH("x-forwarded-host", false); + +const std::string kRequestHeaderPrefix = "requestHeader:"; +const std::string kRequestParamPrefix = "requestParameter:"; +const std::string kResponseHeaderPrefix = "responseHeader:"; + +// Define attribute names +const std::string kAttrNamePeerId = "peerId"; +const std::string kAttrNameURL = "url"; +const std::string kAttrNameHttpMethod = "httpMethod"; +const std::string kAttrNameRequestSize = "requestSize"; +const std::string kAttrNameResponseSize = "responseSize"; +const std::string kAttrNameLogMessage = "logMessage"; +const std::string kAttrNameResponseTime = "responseTime"; +const std::string kAttrNameOriginIp = "originIp"; +const std::string kAttrNameOriginHost = "originHost"; +const std::string kAttrNameTargetService = "targetService"; + +Attributes::Value StringValue(const std::string& str) { + Attributes::Value v; + v.type = Attributes::Value::STRING; + v.str_v = str; + return v; +} + +Attributes::Value Int64Value(int64_t value) { + Attributes::Value v; + v.type = Attributes::Value::INT64; + v.value.int64_v = value; + return v; +} + +void SetStringAttribute(const std::string& name, const std::string& value, + Attributes* attr) { + if (!value.empty()) { + attr->attributes[name] = StringValue(value); + } +} + +std::string GetFirstForwardedFor(const HeaderMap& header_map) { + if (!header_map.ForwardedFor()) { + return ""; + } + std::vector xff_address_list = + StringUtil::split(header_map.ForwardedFor()->value().c_str(), ','); + if (xff_address_list.empty()) { + return ""; + } + return xff_address_list.front(); +} + +std::string GetLastForwardedHost(const HeaderMap& header_map) { + const HeaderEntry* entry = header_map.get(kHeaderNameXFH); + if (entry == nullptr) { + return ""; + } + auto xff_list = StringUtil::split(entry->value().c_str(), ','); + if (xff_list.empty()) { + return ""; + } + return xff_list.back(); +} + +void FillRequestHeaderAttributes(const HeaderMap& header_map, + Attributes* attr) { + // Pass in all headers + header_map.iterate( + [](const HeaderEntry& header, void* context) { + auto attr = static_cast(context); + attr->attributes[kRequestHeaderPrefix + header.key().c_str()] = + StringValue(header.value().c_str()); + }, + attr); + + // Pass in all Query parameters. + auto path = header_map.Path(); + if (path != nullptr) { + for (const auto& it : Utility::parseQueryString(path->value().c_str())) { + attr->attributes[kRequestParamPrefix + it.first] = StringValue(it.second); + } + } + + SetStringAttribute(kAttrNameOriginIp, GetFirstForwardedFor(header_map), attr); + SetStringAttribute(kAttrNameOriginHost, GetLastForwardedHost(header_map), + attr); +} + +void FillResponseHeaderAttributes(const HeaderMap& header_map, + Attributes* attr) { + header_map.iterate( + [](const HeaderEntry& header, void* context) { + auto attr = static_cast(context); + attr->attributes[kResponseHeaderPrefix + header.key().c_str()] = + StringValue(header.value().c_str()); + }, + attr); +} + +void FillRequestInfoAttributes(const AccessLog::RequestInfo& info, + Attributes* attr) { + if (info.bytesReceived() >= 0) { + attr->attributes[kAttrNameRequestSize] = Int64Value(info.bytesReceived()); + } + if (info.bytesSent() >= 0) { + attr->attributes[kAttrNameResponseSize] = Int64Value(info.bytesSent()); + } + if (info.duration().count() >= 0) { + attr->attributes[kAttrNameResponseTime] = + Int64Value(info.duration().count()); + } +} + +} // namespace + +HttpControl::HttpControl(const std::string& mixer_server) { + ::istio::mixer_client::MixerClientOptions options; + options.mixer_server = mixer_server; + mixer_client_ = ::istio::mixer_client::CreateMixerClient(options); + + auto target_service = getenv(kEnvNameTargetService.c_str()); + if (target_service) { + target_service_ = target_service; + } +} + +void HttpControl::FillCheckAttributes(const HeaderMap& header_map, + Attributes* attr) { + FillRequestHeaderAttributes(header_map, attr); + + SetStringAttribute(kAttrNameTargetService, target_service_, attr); + attr->attributes[kAttrNamePeerId] = StringValue(kProxyPeerID); +} + +void HttpControl::Check(HttpRequestDataPtr request_data, HeaderMap& headers, + DoneFunc on_done) { + FillCheckAttributes(headers, &request_data->attributes); + log().debug("Send Check: {}", request_data->attributes.DebugString()); + mixer_client_->Check(request_data->attributes, on_done); +} + +void HttpControl::Report(HttpRequestDataPtr request_data, + const HeaderMap* response_headers, + const AccessLog::RequestInfo& request_info, + DoneFunc on_done) { + // Use all Check attributes for Report. + // Add additional Report attributes. + FillResponseHeaderAttributes(*response_headers, &request_data->attributes); + FillRequestInfoAttributes(request_info, &request_data->attributes); + log().debug("Send Report: {}", request_data->attributes.DebugString()); + mixer_client_->Report(request_data->attributes, on_done); +} + +} // namespace Mixer +} // namespace Http diff --git a/src/envoy/mixer/http_control.h b/src/envoy/mixer/http_control.h new file mode 100644 index 00000000000..71c7e5620d6 --- /dev/null +++ b/src/envoy/mixer/http_control.h @@ -0,0 +1,63 @@ +/* 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. + */ + +#pragma once + +#include "precompiled/precompiled.h" + +#include "common/common/logger.h" +#include "common/http/headers.h" +#include "envoy/http/access_log.h" +#include "include/client.h" + +namespace Http { +namespace Mixer { + +// Store data from Check to report +struct HttpRequestData { + ::istio::mixer_client::Attributes attributes; +}; +typedef std::shared_ptr HttpRequestDataPtr; + +// The mixer client class to control HTTP requests. +// It has Check() to validate if a request can be processed. +// At the end of request, call Report(). +class HttpControl final : public Logger::Loggable { + public: + // The constructor. + HttpControl(const std::string& mixer_server); + + // Make mixer check call. + void Check(HttpRequestDataPtr request_data, HeaderMap& headers, + ::istio::mixer_client::DoneFunc on_done); + + // Make mixer report call. + void Report(HttpRequestDataPtr request_data, + const HeaderMap* response_headers, + const AccessLog::RequestInfo& request_info, + ::istio::mixer_client::DoneFunc on_done); + + private: + void FillCheckAttributes(const HeaderMap& header_map, + ::istio::mixer_client::Attributes* attr); + + // The mixer client + std::unique_ptr<::istio::mixer_client::MixerClient> mixer_client_; + // Target service + std::string target_service_; +}; + +} // namespace Mixer +} // namespace Http diff --git a/src/envoy/mixer/http_filter.cc b/src/envoy/mixer/http_filter.cc new file mode 100644 index 00000000000..ec209b1ff98 --- /dev/null +++ b/src/envoy/mixer/http_filter.cc @@ -0,0 +1,267 @@ +/* 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 "precompiled/precompiled.h" + +#include "common/common/logger.h" +#include "common/http/headers.h" +#include "common/http/utility.h" +#include "envoy/server/instance.h" +#include "server/config/network/http_connection_manager.h" +#include "src/envoy/mixer/http_control.h" + +using ::google::protobuf::util::Status; +using StatusCode = ::google::protobuf::util::error::Code; +using ::istio::mixer_client::DoneFunc; + +namespace Http { +namespace Mixer { +namespace { + +// Define lower case string for X-Forwarded-Host. +const LowerCaseString kHeaderNameXFH("x-forwarded-host", false); + +// Convert Status::code to HTTP code +int HttpCode(int code) { + // Map Canonical codes to HTTP status codes. This is based on the mapping + // defined by the protobuf http error space. + switch (code) { + case StatusCode::OK: + return 200; + case StatusCode::CANCELLED: + return 499; + case StatusCode::UNKNOWN: + return 500; + case StatusCode::INVALID_ARGUMENT: + return 400; + case StatusCode::DEADLINE_EXCEEDED: + return 504; + case StatusCode::NOT_FOUND: + return 404; + case StatusCode::ALREADY_EXISTS: + return 409; + case StatusCode::PERMISSION_DENIED: + return 403; + case StatusCode::RESOURCE_EXHAUSTED: + return 429; + case StatusCode::FAILED_PRECONDITION: + return 400; + case StatusCode::ABORTED: + return 409; + case StatusCode::OUT_OF_RANGE: + return 400; + case StatusCode::UNIMPLEMENTED: + return 501; + case StatusCode::INTERNAL: + return 500; + case StatusCode::UNAVAILABLE: + return 503; + case StatusCode::DATA_LOSS: + return 500; + case StatusCode::UNAUTHENTICATED: + return 401; + default: + return 500; + } +} + +} // namespace + +class Config : public Logger::Loggable { + private: + std::shared_ptr http_control_; + Upstream::ClusterManager& cm_; + + public: + Config(const Json::Object& config, Server::Instance& server) + : cm_(server.clusterManager()) { + std::string mixer_server; + if (config.hasObject("mixer_server")) { + mixer_server = config.getString("mixer_server"); + } else { + log().error( + "mixer_server is required but not specified in the config: {}", + __func__); + } + + http_control_ = std::make_shared(mixer_server); + log().debug("Called Mixer::Config contructor with mixer_server: ", + mixer_server); + } + + std::shared_ptr& http_control() { return http_control_; } +}; + +typedef std::shared_ptr ConfigPtr; + +class Instance : public Http::StreamFilter, public Http::AccessLog::Instance { + private: + std::shared_ptr http_control_; + std::shared_ptr request_data_; + + enum State { NotStarted, Calling, Complete, Responded }; + State state_; + + StreamDecoderFilterCallbacks* decoder_callbacks_; + StreamEncoderFilterCallbacks* encoder_callbacks_; + + bool initiating_call_; + + public: + Instance(ConfigPtr config) + : http_control_(config->http_control()), + state_(NotStarted), + initiating_call_(false) { + Log().debug("Called Mixer::Instance : {}", __func__); + } + + // Jump thread; on_done will be called at the dispatcher thread. + DoneFunc wrapper(DoneFunc on_done) { + auto& dispatcher = decoder_callbacks_->dispatcher(); + return [&dispatcher, on_done](const Status& status) { + dispatcher.post([status, on_done]() { on_done(status); }); + }; + } + + FilterHeadersStatus decodeHeaders(HeaderMap& headers, + bool end_stream) override { + Log().debug("Called Mixer::Instance : {}", __func__); + state_ = Calling; + initiating_call_ = true; + request_data_ = std::make_shared(); + http_control_->Check( + request_data_, headers, + wrapper([this](const Status& status) { completeCheck(status); })); + initiating_call_ = false; + + if (state_ == Complete) { + return FilterHeadersStatus::Continue; + } + Log().debug("Called Mixer::Instance : {} Stop", __func__); + return FilterHeadersStatus::StopIteration; + } + + FilterDataStatus decodeData(Buffer::Instance& data, + bool end_stream) override { + Log().debug("Called Mixer::Instance : {} ({}, {})", __func__, data.length(), + end_stream); + if (state_ == Calling) { + return FilterDataStatus::StopIterationAndBuffer; + } + return FilterDataStatus::Continue; + } + + FilterTrailersStatus decodeTrailers(HeaderMap& trailers) override { + Log().debug("Called Mixer::Instance : {}", __func__); + if (state_ == Calling) { + return FilterTrailersStatus::StopIteration; + } + return FilterTrailersStatus::Continue; + } + void setDecoderFilterCallbacks( + StreamDecoderFilterCallbacks& callbacks) override { + Log().debug("Called Mixer::Instance : {}", __func__); + decoder_callbacks_ = &callbacks; + decoder_callbacks_->addResetStreamCallback( + [this]() { state_ = Responded; }); + } + void completeCheck(const Status& status) { + Log().debug("Called Mixer::Instance : check complete {}", + status.ToString()); + if (!status.ok() && state_ != Responded) { + state_ = Responded; + Utility::sendLocalReply(*decoder_callbacks_, + Code(HttpCode(status.error_code())), + 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 { + Log().debug("Called Mixer::Instance : {}", __func__); + // Make sure not to use any class members at the callback. + // The class may be gone when it is called. + // Log() is a static function so it is OK. + http_control_->Report(request_data_, response_headers, request_info, + [](const Status& status) { + Log().debug("Report returns status: {}", + status.ToString()); + }); + } + + static spdlog::logger& Log() { + static spdlog::logger& instance = + Logger::Registry::getLog(Logger::Id::http); + return instance; + } +}; + +} // namespace Mixer +} // namespace Http + +namespace Server { +namespace Configuration { + +class MixerConfig : public HttpFilterConfigFactory { + public: + 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") { + return nullptr; + } + + Http::Mixer::ConfigPtr mixer_config( + new Http::Mixer::Config(config, server)); + return + [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)); + }; + } +}; + +static RegisterHttpFilterConfigFactory register_; + +} // namespace Configuration +} // namespace server