diff --git a/api/contrib/envoy/extensions/filters/http/peer_metadata/v3alpha/BUILD b/api/contrib/envoy/extensions/filters/http/peer_metadata/v3alpha/BUILD new file mode 100644 index 0000000000000..d4e65a26105e1 --- /dev/null +++ b/api/contrib/envoy/extensions/filters/http/peer_metadata/v3alpha/BUILD @@ -0,0 +1,10 @@ +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "@com_github_cncf_xds//xds/annotations/v3:pkg", + "@com_github_cncf_xds//udpa/annotations:pkg", + ], +) diff --git a/api/contrib/envoy/extensions/filters/http/peer_metadata/v3alpha/peer_metadata.proto b/api/contrib/envoy/extensions/filters/http/peer_metadata/v3alpha/peer_metadata.proto new file mode 100644 index 0000000000000..2e0f1fdd75d2b --- /dev/null +++ b/api/contrib/envoy/extensions/filters/http/peer_metadata/v3alpha/peer_metadata.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.peer_metadata.v3alpha; + +import "xds/annotations/v3/status.proto"; +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.peer_metadata.v3alpha"; +option java_outer_classname = "PeerMetadataProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/filters/http/peer_metadata/v3alpha"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; +option (xds.annotations.v3.file_status).work_in_progress = true; + +// [#protodoc-title: Peer Metadata HTTP filter] +// +// Peer metadata provider filter. This filter encapsulates the discovery of the +// peer telemetry attributes for consumption by the telemetry filters. +// [#extension: envoy.filters.http.peer_metadata] + +message PeerMetadataConfig { + // DEPRECATED. + // This method uses `baggage` header encoding. + message Baggage { + } + + // This method uses the workload metadata xDS. Requires that the bootstrap extension is enabled. + // For downstream discovery, the remote address is the lookup key in xDS. + // For upstream discovery: + // + // * If the upstream host address is an IP, this IP is used as the lookup key; + // + // * If the upstream host address is internal, uses the + // "filter_metadata.tunnel.destination" dynamic metadata value as the lookup key. + message WorkloadDiscovery { + } + + // This method uses Istio HTTP metadata exchange headers, e.g. `x-envoy-peer-metadata`. Removes these headers if found. + message IstioHeaders { + // Strip x-envoy-peer-metadata and x-envoy-peer-metadata-id headers on HTTP requests to services outside the mesh. + // Detects upstream clusters with `istio` and `external` filter metadata fields + bool skip_external_clusters = 1; + } + + // An exhaustive list of the derivation methods. + message DiscoveryMethod { + oneof method_specifier { + Baggage baggage = 1; + WorkloadDiscovery workload_discovery = 2; + IstioHeaders istio_headers = 3; + } + } + + // The order of the derivation of the downstream peer metadata, in the precedence order. + // First successful lookup wins. + repeated DiscoveryMethod downstream_discovery = 1; + + // The order of the derivation of the upstream peer metadata, in the precedence order. + // First successful lookup wins. + repeated DiscoveryMethod upstream_discovery = 2; + + // An exhaustive list of the metadata propagation methods. + message PropagationMethod { + oneof method_specifier { + IstioHeaders istio_headers = 1; + } + } + + // Downstream injection of the metadata via a response header. + repeated PropagationMethod downstream_propagation = 3; + + // Upstream injection of the metadata via a request header. + repeated PropagationMethod upstream_propagation = 4; + + // True to enable sharing with the upstream. + bool shared_with_upstream = 5; + + // Additional labels to be added to the peer metadata to help your understand the traffic. + // e.g. `role`, `location` etc. + repeated string additional_labels = 6; +} diff --git a/contrib/common/metadata_object/source/BUILD b/contrib/common/metadata_object/source/BUILD new file mode 100644 index 0000000000000..57192495ddb71 --- /dev/null +++ b/contrib/common/metadata_object/source/BUILD @@ -0,0 +1,25 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_library( + name = "metadata_object_lib", + srcs = ["metadata_object.cc"], + hdrs = ["metadata_object.h"], + visibility = ["//visibility:public"], + deps = [ + "//envoy/common:hashable_interface", + "//envoy/registry", + "//envoy/stream_info:filter_state_interface", + "//source/common/common:hash_lib", + "//source/common/protobuf:utility_lib", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:optional", + ], +) diff --git a/contrib/common/metadata_object/source/metadata_object.cc b/contrib/common/metadata_object/source/metadata_object.cc new file mode 100644 index 0000000000000..c6fc47f64467a --- /dev/null +++ b/contrib/common/metadata_object/source/metadata_object.cc @@ -0,0 +1,375 @@ +// Copyright 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 "contrib/common/metadata_object/source/metadata_object.h" + +#include "envoy/registry/registry.h" +#include "source/common/common/hash.h" +#include "source/common/protobuf/utility.h" + +#include "absl/strings/str_join.h" + +namespace Istio { +namespace Common { + +namespace { +static absl::flat_hash_map ALL_BAGGAGE_TOKENS = { + {NamespaceNameToken, BaggageToken::NamespaceName}, + {ClusterNameToken, BaggageToken::ClusterName}, + {ServiceNameToken, BaggageToken::ServiceName}, + {ServiceVersionToken, BaggageToken::ServiceVersion}, + {AppNameToken, BaggageToken::AppName}, + {AppVersionToken, BaggageToken::AppVersion}, + {WorkloadNameToken, BaggageToken::WorkloadName}, + {WorkloadTypeToken, BaggageToken::WorkloadType}, + {InstanceNameToken, BaggageToken::InstanceName}, +}; + +static absl::flat_hash_map ALL_WORKLOAD_TOKENS = { + {PodSuffix, WorkloadType::Pod}, + {DeploymentSuffix, WorkloadType::Deployment}, + {JobSuffix, WorkloadType::Job}, + {CronJobSuffix, WorkloadType::CronJob}, +}; + +absl::optional toSuffix(WorkloadType workload_type) { + switch (workload_type) { + case WorkloadType::Deployment: + return DeploymentSuffix; + case WorkloadType::CronJob: + return CronJobSuffix; + case WorkloadType::Job: + return JobSuffix; + case WorkloadType::Pod: + return PodSuffix; + case WorkloadType::Unknown: + default: + return {}; + } +} + +} // namespace + +Envoy::ProtobufTypes::MessagePtr WorkloadMetadataObject::serializeAsProto() const { + auto message = std::make_unique(); + const auto suffix = toSuffix(workload_type_); + if (suffix) { + (*message->mutable_fields())[WorkloadTypeToken].set_string_value(*suffix); + } + if (!workload_name_.empty()) { + (*message->mutable_fields())[WorkloadNameToken].set_string_value(workload_name_); + } + if (!instance_name_.empty()) { + (*message->mutable_fields())[InstanceNameToken].set_string_value(instance_name_); + } + if (!cluster_name_.empty()) { + (*message->mutable_fields())[ClusterNameToken].set_string_value(cluster_name_); + } + if (!namespace_name_.empty()) { + (*message->mutable_fields())[NamespaceNameToken].set_string_value(namespace_name_); + } + if (!canonical_name_.empty()) { + (*message->mutable_fields())[ServiceNameToken].set_string_value(canonical_name_); + } + if (!canonical_revision_.empty()) { + (*message->mutable_fields())[ServiceVersionToken].set_string_value(canonical_revision_); + } + if (!app_name_.empty()) { + (*message->mutable_fields())[AppNameToken].set_string_value(app_name_); + } + if (!app_version_.empty()) { + (*message->mutable_fields())[AppVersionToken].set_string_value(app_version_); + } + if (!identity_.empty()) { + (*message->mutable_fields())[IdentityToken].set_string_value(identity_); + } + + if (!labels_.empty()) { + auto* labels = (*message->mutable_fields())[LabelsToken].mutable_struct_value(); + for (const auto& l : labels_) { + (*labels->mutable_fields())[std::string(l.first)].set_string_value(std::string(l.second)); + } + } + + return message; +} + +std::vector> +WorkloadMetadataObject::serializeAsPairs() const { + std::vector> parts; + const auto suffix = toSuffix(workload_type_); + if (suffix) { + parts.push_back({WorkloadTypeToken, *suffix}); + } + if (!workload_name_.empty()) { + parts.push_back({WorkloadNameToken, workload_name_}); + } + if (!instance_name_.empty()) { + parts.push_back({InstanceNameToken, instance_name_}); + } + if (!cluster_name_.empty()) { + parts.push_back({ClusterNameToken, cluster_name_}); + } + if (!namespace_name_.empty()) { + parts.push_back({NamespaceNameToken, namespace_name_}); + } + if (!canonical_name_.empty()) { + parts.push_back({ServiceNameToken, canonical_name_}); + } + if (!canonical_revision_.empty()) { + parts.push_back({ServiceVersionToken, canonical_revision_}); + } + if (!app_name_.empty()) { + parts.push_back({AppNameToken, app_name_}); + } + if (!app_version_.empty()) { + parts.push_back({AppVersionToken, app_version_}); + } + if (!labels_.empty()) { + for (const auto& l : labels_) { + parts.push_back({absl::StrCat("labels[]", l.first), absl::string_view(l.second)}); + } + } + return parts; +} + +absl::optional WorkloadMetadataObject::serializeAsString() const { + const auto parts = serializeAsPairs(); + return absl::StrJoin(parts, ",", absl::PairFormatter("=")); +} + +absl::optional WorkloadMetadataObject::hash() const { + return Envoy::HashUtil::xxHash64(*serializeAsString()); +} + +absl::optional WorkloadMetadataObject::owner() const { + const auto suffix = toSuffix(workload_type_); + if (suffix) { + return absl::StrCat(OwnerPrefix, namespace_name_, "/", *suffix, "s/", workload_name_); + } + return {}; +} + +WorkloadType fromSuffix(absl::string_view suffix) { + const auto it = ALL_WORKLOAD_TOKENS.find(suffix); + if (it != ALL_WORKLOAD_TOKENS.end()) { + return it->second; + } + return WorkloadType::Unknown; +} + +WorkloadType parseOwner(absl::string_view owner, absl::string_view workload) { + // Strip "s/workload_name" and check for workload type. + if (owner.size() > workload.size() + 2) { + owner.remove_suffix(workload.size() + 2); + size_t last = owner.rfind('/'); + if (last != absl::string_view::npos) { + return fromSuffix(owner.substr(last + 1)); + } + } + return WorkloadType::Unknown; +} + +google::protobuf::Struct convertWorkloadMetadataToStruct(const WorkloadMetadataObject& obj) { + google::protobuf::Struct metadata; + if (!obj.instance_name_.empty()) { + (*metadata.mutable_fields())[InstanceMetadataField].set_string_value(obj.instance_name_); + } + if (!obj.namespace_name_.empty()) { + (*metadata.mutable_fields())[NamespaceMetadataField].set_string_value(obj.namespace_name_); + } + if (!obj.workload_name_.empty()) { + (*metadata.mutable_fields())[WorkloadMetadataField].set_string_value(obj.workload_name_); + } + if (!obj.cluster_name_.empty()) { + (*metadata.mutable_fields())[ClusterMetadataField].set_string_value(obj.cluster_name_); + } + auto* labels = (*metadata.mutable_fields())[LabelsMetadataField].mutable_struct_value(); + if (!obj.canonical_name_.empty()) { + (*labels->mutable_fields())[CanonicalNameLabel].set_string_value(obj.canonical_name_); + } + if (!obj.canonical_revision_.empty()) { + (*labels->mutable_fields())[CanonicalRevisionLabel].set_string_value(obj.canonical_revision_); + } + if (!obj.app_name_.empty()) { + (*labels->mutable_fields())[AppNameLabel].set_string_value(obj.app_name_); + } + if (!obj.app_version_.empty()) { + (*labels->mutable_fields())[AppVersionLabel].set_string_value(obj.app_version_); + } + if (!obj.getLabels().empty()) { + for (const auto& lbl : obj.getLabels()) { + (*labels->mutable_fields())[std::string(lbl.first)].set_string_value(std::string(lbl.second)); + } + } + if (const auto owner = obj.owner(); owner.has_value()) { + (*metadata.mutable_fields())[OwnerMetadataField].set_string_value(*owner); + } + return metadata; +} + +// Convert struct to a metadata object. +std::unique_ptr +convertStructToWorkloadMetadata(const google::protobuf::Struct& metadata) { + return convertStructToWorkloadMetadata(metadata, {}); +} + +std::unique_ptr +convertStructToWorkloadMetadata(const google::protobuf::Struct& metadata, + const absl::flat_hash_set& additional_labels) { + absl::string_view instance, namespace_name, owner, workload, cluster, canonical_name, + canonical_revision, app_name, app_version; + std::vector> labels; + for (const auto& it : metadata.fields()) { + if (it.first == InstanceMetadataField) { + instance = it.second.string_value(); + } else if (it.first == NamespaceMetadataField) { + namespace_name = it.second.string_value(); + } else if (it.first == OwnerMetadataField) { + owner = it.second.string_value(); + } else if (it.first == WorkloadMetadataField) { + workload = it.second.string_value(); + } else if (it.first == ClusterMetadataField) { + cluster = it.second.string_value(); + } else if (it.first == LabelsMetadataField) { + for (const auto& labels_it : it.second.struct_value().fields()) { + if (labels_it.first == CanonicalNameLabel) { + canonical_name = labels_it.second.string_value(); + } else if (labels_it.first == CanonicalRevisionLabel) { + canonical_revision = labels_it.second.string_value(); + } else if (labels_it.first == AppNameLabel) { + app_name = labels_it.second.string_value(); + } else if (labels_it.first == AppVersionLabel) { + app_version = labels_it.second.string_value(); + } else if (additional_labels.contains(std::string(labels_it.first))) { + labels.push_back( + {std::string(labels_it.first), std::string(labels_it.second.string_value())}); + } + } + } + } + auto obj = std::make_unique(instance, cluster, namespace_name, workload, + canonical_name, canonical_revision, app_name, + app_version, parseOwner(owner, workload), ""); + obj->setLabels(labels); + return obj; +} + +absl::optional +convertEndpointMetadata(const std::string& endpoint_encoding) { + std::vector parts = absl::StrSplit(endpoint_encoding, ';'); + if (parts.size() < 5) { + return {}; + } + return absl::make_optional("", parts[4], parts[1], parts[0], parts[2], + parts[3], "", "", WorkloadType::Unknown, ""); +} + +std::string serializeToStringDeterministic(const google::protobuf::Struct& metadata) { + std::string out; + { + google::protobuf::io::StringOutputStream md(&out); + google::protobuf::io::CodedOutputStream mcs(&md); + mcs.SetSerializationDeterministic(true); + if (!metadata.SerializeToCodedStream(&mcs)) { + out.clear(); + } + } + return out; +} + +WorkloadMetadataObject::FieldType +WorkloadMetadataObject::getField(absl::string_view field_name) const { + const auto it = ALL_BAGGAGE_TOKENS.find(field_name); + if (it != ALL_BAGGAGE_TOKENS.end()) { + switch (it->second) { + case BaggageToken::NamespaceName: + return namespace_name_; + case BaggageToken::ClusterName: + return cluster_name_; + case BaggageToken::ServiceName: + return canonical_name_; + case BaggageToken::ServiceVersion: + return canonical_revision_; + case BaggageToken::AppName: + return app_name_; + case BaggageToken::AppVersion: + return app_version_; + case BaggageToken::WorkloadName: + return workload_name_; + case BaggageToken::WorkloadType: + if (const auto value = toSuffix(workload_type_); value.has_value()) { + return *value; + } + return "unknown"; + case BaggageToken::InstanceName: + return instance_name_; + } + } + return {}; +} + +std::unique_ptr convertBaggageToWorkloadMetadata(absl::string_view data) { + absl::string_view instance; + absl::string_view cluster; + absl::string_view workload; + absl::string_view namespace_name; + absl::string_view canonical_name; + absl::string_view canonical_revision; + absl::string_view app_name; + absl::string_view app_version; + WorkloadType workload_type = WorkloadType::Unknown; + std::vector properties = absl::StrSplit(data, ','); + for (absl::string_view property : properties) { + std::pair parts = absl::StrSplit(property, '='); + const auto it = ALL_BAGGAGE_TOKENS.find(parts.first); + if (it != ALL_BAGGAGE_TOKENS.end()) { + switch (it->second) { + case BaggageToken::NamespaceName: + namespace_name = parts.second; + break; + case BaggageToken::ClusterName: + cluster = parts.second; + break; + case BaggageToken::ServiceName: + canonical_name = parts.second; + break; + case BaggageToken::ServiceVersion: + canonical_revision = parts.second; + break; + case BaggageToken::AppName: + app_name = parts.second; + break; + case BaggageToken::AppVersion: + app_version = parts.second; + break; + case BaggageToken::WorkloadName: + workload = parts.second; + break; + case BaggageToken::WorkloadType: + workload_type = fromSuffix(parts.second); + break; + case BaggageToken::InstanceName: + instance = parts.second; + break; + } + } + } + return std::make_unique(instance, cluster, namespace_name, workload, + canonical_name, canonical_revision, app_name, + app_version, workload_type, ""); +} + +} // namespace Common +} // namespace Istio diff --git a/contrib/common/metadata_object/source/metadata_object.h b/contrib/common/metadata_object/source/metadata_object.h new file mode 100644 index 0000000000000..2a0e65a8e349d --- /dev/null +++ b/contrib/common/metadata_object/source/metadata_object.h @@ -0,0 +1,158 @@ +// Copyright 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 "envoy/common/hashable.h" +#include "envoy/stream_info/filter_state.h" + +#include "source/common/protobuf/protobuf.h" + +#include "absl/types/optional.h" + +#include "google/protobuf/struct.pb.h" + +namespace Istio { +namespace Common { + +// Filter state key to store the peer metadata under. +constexpr absl::string_view DownstreamPeer = "downstream_peer"; +constexpr absl::string_view UpstreamPeer = "upstream_peer"; + +// Special filter state key to indicate the filter is done looking for peer metadata. +// This is used by network metadata exchange on failure. +constexpr absl::string_view NoPeer = "peer_not_found"; + +// Special labels used in the peer metadata. +constexpr absl::string_view CanonicalNameLabel = "service.istio.io/canonical-name"; +constexpr absl::string_view CanonicalRevisionLabel = "service.istio.io/canonical-revision"; +constexpr absl::string_view AppNameLabel = "app"; +constexpr absl::string_view AppVersionLabel = "version"; + +enum class WorkloadType { + Unknown, + Pod, + Deployment, + Job, + CronJob, +}; + +constexpr absl::string_view OwnerPrefix = "kubernetes://apis/apps/v1/namespaces/"; + +constexpr absl::string_view PodSuffix = "pod"; +constexpr absl::string_view DeploymentSuffix = "deployment"; +constexpr absl::string_view JobSuffix = "job"; +constexpr absl::string_view CronJobSuffix = "cronjob"; + +enum class BaggageToken { + NamespaceName, + ClusterName, + ServiceName, + ServiceVersion, + AppName, + AppVersion, + WorkloadName, + WorkloadType, + InstanceName, +}; + +constexpr absl::string_view NamespaceNameToken = "namespace"; +constexpr absl::string_view ClusterNameToken = "cluster"; +constexpr absl::string_view ServiceNameToken = "service"; +constexpr absl::string_view ServiceVersionToken = "revision"; +constexpr absl::string_view AppNameToken = "app"; +constexpr absl::string_view AppVersionToken = "version"; +constexpr absl::string_view WorkloadNameToken = "workload"; +constexpr absl::string_view WorkloadTypeToken = "type"; +constexpr absl::string_view InstanceNameToken = "name"; +constexpr absl::string_view LabelsToken = "labels"; +constexpr absl::string_view IdentityToken = "identity"; + +constexpr absl::string_view InstanceMetadataField = "NAME"; +constexpr absl::string_view NamespaceMetadataField = "NAMESPACE"; +constexpr absl::string_view ClusterMetadataField = "CLUSTER_ID"; +constexpr absl::string_view OwnerMetadataField = "OWNER"; +constexpr absl::string_view WorkloadMetadataField = "WORKLOAD_NAME"; +constexpr absl::string_view LabelsMetadataField = "LABELS"; + +class WorkloadMetadataObject : public Envoy::StreamInfo::FilterState::Object, + public Envoy::Hashable { +public: + explicit WorkloadMetadataObject(absl::string_view instance_name, absl::string_view cluster_name, + absl::string_view namespace_name, absl::string_view workload_name, + absl::string_view canonical_name, + absl::string_view canonical_revision, absl::string_view app_name, + absl::string_view app_version, WorkloadType workload_type, + absl::string_view identity) + : instance_name_(instance_name), cluster_name_(cluster_name), namespace_name_(namespace_name), + workload_name_(workload_name), canonical_name_(canonical_name), + canonical_revision_(canonical_revision), app_name_(app_name), app_version_(app_version), + workload_type_(workload_type), identity_(identity) {} + + absl::optional hash() const override; + Envoy::ProtobufTypes::MessagePtr serializeAsProto() const override; + std::vector> serializeAsPairs() const; + absl::optional serializeAsString() const override; + absl::optional owner() const; + bool hasFieldSupport() const override { return true; } + using Envoy::StreamInfo::FilterState::Object::FieldType; + FieldType getField(absl::string_view) const override; + void setLabels(std::vector> labels) { labels_ = labels; } + std::vector> getLabels() const { return labels_; } + + const std::string instance_name_; + const std::string cluster_name_; + const std::string namespace_name_; + const std::string workload_name_; + const std::string canonical_name_; + const std::string canonical_revision_; + const std::string app_name_; + const std::string app_version_; + const WorkloadType workload_type_; + const std::string identity_; + std::vector> labels_; +}; + +// Parse string workload type. +WorkloadType fromSuffix(absl::string_view suffix); + +// Parse owner field from kubernetes to detect the workload type. +WorkloadType parseOwner(absl::string_view owner, absl::string_view workload); + +// Convert a metadata object to a struct. +google::protobuf::Struct convertWorkloadMetadataToStruct(const WorkloadMetadataObject& obj); + +// Convert struct to a metadata object. +std::unique_ptr +convertStructToWorkloadMetadata(const google::protobuf::Struct& metadata); + +std::unique_ptr +convertStructToWorkloadMetadata(const google::protobuf::Struct& metadata, + const absl::flat_hash_set& additional_labels); + +// Convert endpoint metadata string to a metadata object. +// Telemetry metadata is compressed into a semicolon separated string: +// workload-name;namespace;canonical-service-name;canonical-service-revision;cluster-id. +// Telemetry metadata is stored as a string under "istio", "workload" field +// path. +absl::optional +convertEndpointMetadata(const std::string& endpoint_encoding); + +std::string serializeToStringDeterministic(const google::protobuf::Struct& metadata); + +// Convert from baggage encoding. +std::unique_ptr convertBaggageToWorkloadMetadata(absl::string_view data); + +} // namespace Common +} // namespace Istio diff --git a/contrib/common/workload_discovery/source/BUILD b/contrib/common/workload_discovery/source/BUILD new file mode 100644 index 0000000000000..42cb9d4be40df --- /dev/null +++ b/contrib/common/workload_discovery/source/BUILD @@ -0,0 +1,22 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_library( + name = "api_lib", + srcs = ["api.cc"], + hdrs = ["api.h"], + visibility = ["//visibility:public"], + deps = [ + "//contrib/common/metadata_object/source:metadata_object_lib", + "//envoy/network:address_interface", + "//envoy/server:factory_context_interface", + "//envoy/singleton:manager_interface", + ], +) diff --git a/contrib/common/workload_discovery/source/api.cc b/contrib/common/workload_discovery/source/api.cc new file mode 100644 index 0000000000000..3f3f1a0689ddb --- /dev/null +++ b/contrib/common/workload_discovery/source/api.cc @@ -0,0 +1,30 @@ +// Copyright 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 "contrib/common/workload_discovery/source/api.h" + +#include "envoy/singleton/manager.h" + +namespace Envoy::Extensions::Common::WorkloadDiscovery { + +// Stub implementation that returns null provider +// In a full implementation, this would be registered via a bootstrap extension +WorkloadMetadataProviderSharedPtr +GetProvider(Server::Configuration::ServerFactoryContext& context) { + UNREFERENCED_PARAMETER(context); + // Return null - workload discovery is optional + return nullptr; +} + +} // namespace Envoy::Extensions::Common::WorkloadDiscovery diff --git a/contrib/common/workload_discovery/source/api.h b/contrib/common/workload_discovery/source/api.h new file mode 100644 index 0000000000000..79f578f094014 --- /dev/null +++ b/contrib/common/workload_discovery/source/api.h @@ -0,0 +1,35 @@ +// Copyright 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 "envoy/network/address.h" +#include "envoy/server/factory_context.h" + +#include "contrib/common/metadata_object/source/metadata_object.h" + +namespace Envoy::Extensions::Common::WorkloadDiscovery { + +class WorkloadMetadataProvider { +public: + virtual ~WorkloadMetadataProvider() = default; + virtual std::optional + GetMetadata(const Network::Address::InstanceConstSharedPtr& address) PURE; +}; + +using WorkloadMetadataProviderSharedPtr = std::shared_ptr; + +WorkloadMetadataProviderSharedPtr GetProvider(Server::Configuration::ServerFactoryContext& context); + +} // namespace Envoy::Extensions::Common::WorkloadDiscovery diff --git a/contrib/contrib_build_config.bzl b/contrib/contrib_build_config.bzl index e7f5129722233..7b17996e89e8d 100644 --- a/contrib/contrib_build_config.bzl +++ b/contrib/contrib_build_config.bzl @@ -15,6 +15,7 @@ CONTRIB_EXTENSIONS = { "envoy.filters.http.golang": "//contrib/golang/filters/http/source:config", "envoy.filters.http.language": "//contrib/language/filters/http/source:config_lib", "envoy.filters.http.peak_ewma": "//contrib/peak_ewma/filters/http/source:config", + "envoy.filters.http.peer_metadata": "//contrib/peer_metadata/filters/http/source:config", "envoy.filters.http.sxg": "//contrib/sxg/filters/http/source:config", # diff --git a/contrib/extensions_metadata.yaml b/contrib/extensions_metadata.yaml index 757449f1fe3f1..58bb2f3bc8c90 100644 --- a/contrib/extensions_metadata.yaml +++ b/contrib/extensions_metadata.yaml @@ -151,6 +151,13 @@ envoy.filters.http.peak_ewma: status: alpha type_urls: - envoy.extensions.filters.http.peak_ewma.v3alpha.PeakEwmaConfig +envoy.filters.http.peer_metadata: + categories: + - envoy.filters.http + security_posture: requires_trusted_downstream_and_upstream + status: alpha + type_urls: + - envoy.extensions.filters.http.peer_metadata.v3alpha.PeerMetadataConfig envoy.load_balancing_policies.peak_ewma: categories: - envoy.load_balancing_policies diff --git a/contrib/peer_metadata/filters/http/source/BUILD b/contrib/peer_metadata/filters/http/source/BUILD new file mode 100644 index 0000000000000..de456c0df3376 --- /dev/null +++ b/contrib/peer_metadata/filters/http/source/BUILD @@ -0,0 +1,46 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_contrib_extension", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +# Peer Metadata L7 HTTP filter + +envoy_cc_library( + name = "filter_lib", + srcs = ["filter.cc"], + hdrs = ["filter.h"], + deps = [ + "//contrib/common/metadata_object/source:metadata_object_lib", + "//contrib/common/workload_discovery/source:api_lib", + "//envoy/http:filter_interface", + "//envoy/registry", + "//envoy/server:factory_context_interface", + "//source/common/common:base64_lib", + "//source/common/common:hash_lib", + "//source/common/http:header_utility_lib", + "//source/common/http:utility_lib", + "//source/common/network:utility_lib", + "//source/common/singleton:const_singleton", + "//source/extensions/filters/common/expr:cel_state_lib", + "//source/extensions/filters/http/common:pass_through_filter_lib", + "@envoy_api//contrib/envoy/extensions/filters/http/peer_metadata/v3alpha:pkg_cc_proto", + ], +) + +envoy_cc_contrib_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":filter_lib", + "//envoy/registry", + "//source/extensions/filters/http/common:factory_base_lib", + "@envoy_api//contrib/envoy/extensions/filters/http/peer_metadata/v3alpha:pkg_cc_proto", + ], +) diff --git a/contrib/peer_metadata/filters/http/source/config.cc b/contrib/peer_metadata/filters/http/source/config.cc new file mode 100644 index 0000000000000..8ec721c190c8b --- /dev/null +++ b/contrib/peer_metadata/filters/http/source/config.cc @@ -0,0 +1,39 @@ +// Copyright 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 "contrib/peer_metadata/filters/http/source/config.h" + +#include "envoy/registry/registry.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace PeerMetadata { + +Http::FilterFactoryCb FilterConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig& config, + const std::string&, Server::Configuration::FactoryContext& factory_context) { + auto filter_config = std::make_shared(config, factory_context); + return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) { + auto filter = std::make_shared(filter_config); + callbacks.addStreamFilter(filter); + }; +} + +REGISTER_FACTORY(FilterConfigFactory, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace PeerMetadata +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/peer_metadata/filters/http/source/config.h b/contrib/peer_metadata/filters/http/source/config.h new file mode 100644 index 0000000000000..a0f49f323fb60 --- /dev/null +++ b/contrib/peer_metadata/filters/http/source/config.h @@ -0,0 +1,44 @@ +// Copyright 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 "envoy/extensions/filters/http/peer_metadata/v3alpha/peer_metadata.pb.h" +#include "envoy/extensions/filters/http/peer_metadata/v3alpha/peer_metadata.pb.validate.h" + +#include "source/extensions/filters/http/common/factory_base.h" + +#include "contrib/peer_metadata/filters/http/source/filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace PeerMetadata { + +class FilterConfigFactory + : public Common::FactoryBase< + envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig> { +public: + FilterConfigFactory() : FactoryBase("envoy.filters.http.peer_metadata") {} + +private: + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig& config, + const std::string&, Server::Configuration::FactoryContext& factory_context) override; +}; + +} // namespace PeerMetadata +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/peer_metadata/filters/http/source/filter.cc b/contrib/peer_metadata/filters/http/source/filter.cc new file mode 100644 index 0000000000000..16699e734a397 --- /dev/null +++ b/contrib/peer_metadata/filters/http/source/filter.cc @@ -0,0 +1,359 @@ +// Copyright 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 "contrib/peer_metadata/filters/http/source/filter.h" + +#include "envoy/registry/registry.h" +#include "envoy/server/factory_context.h" + +#include "source/common/common/base64.h" +#include "source/common/common/hash.h" +#include "source/common/http/header_utility.h" +#include "source/common/http/utility.h" +#include "source/common/network/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace PeerMetadata { + +using ::Envoy::Extensions::Filters::Common::Expr::CelState; + +class XDSMethod : public DiscoveryMethod { +public: + XDSMethod(bool downstream, Server::Configuration::ServerFactoryContext& factory_context) + : downstream_(downstream), + metadata_provider_(Extensions::Common::WorkloadDiscovery::GetProvider(factory_context)) {} + absl::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, + Context&) const override; + +private: + const bool downstream_; + Extensions::Common::WorkloadDiscovery::WorkloadMetadataProviderSharedPtr metadata_provider_; +}; + +absl::optional XDSMethod::derivePeerInfo(const StreamInfo::StreamInfo& info, + Http::HeaderMap&, Context&) const { + if (!metadata_provider_) { + return {}; + } + Network::Address::InstanceConstSharedPtr peer_address; + if (downstream_) { + peer_address = info.downstreamAddressProvider().remoteAddress(); + } else { + if (info.upstreamInfo().has_value()) { + auto upstream_host = info.upstreamInfo().value().get().upstreamHost(); + if (upstream_host) { + const auto address = upstream_host->address(); + switch (address->type()) { + case Network::Address::Type::Ip: + peer_address = upstream_host->address(); + break; + case Network::Address::Type::EnvoyInternal: + if (upstream_host->metadata()) { + const auto& filter_metadata = upstream_host->metadata()->filter_metadata(); + const auto& it = filter_metadata.find("envoy.filters.listener.original_dst"); + if (it != filter_metadata.end()) { + const auto& destination_it = it->second.fields().find("local"); + if (destination_it != it->second.fields().end()) { + peer_address = Network::Utility::parseInternetAddressAndPortNoThrow( + destination_it->second.string_value(), /*v6only=*/false); + } + } + } + break; + default: + break; + } + } + } + } + if (!peer_address) { + return {}; + } + ENVOY_LOG_MISC(debug, "Peer address: {}", peer_address->asString()); + return metadata_provider_->GetMetadata(peer_address); +} + +MXMethod::MXMethod(bool downstream, const absl::flat_hash_set additional_labels, + Server::Configuration::ServerFactoryContext& factory_context) + : downstream_(downstream), tls_(factory_context.threadLocal()), + additional_labels_(additional_labels) { + tls_.set([](Event::Dispatcher&) { return std::make_shared(); }); +} + +absl::optional MXMethod::derivePeerInfo(const StreamInfo::StreamInfo&, + Http::HeaderMap& headers, Context& ctx) const { + const auto peer_id_header = headers.get(Headers::get().ExchangeMetadataHeaderId); + if (downstream_) { + ctx.request_peer_id_received_ = !peer_id_header.empty(); + } + absl::string_view peer_id = + peer_id_header.empty() ? "" : peer_id_header[0]->value().getStringView(); + const auto peer_info_header = headers.get(Headers::get().ExchangeMetadataHeader); + if (downstream_) { + ctx.request_peer_received_ = !peer_info_header.empty(); + } + absl::string_view peer_info = + peer_info_header.empty() ? "" : peer_info_header[0]->value().getStringView(); + if (!peer_info.empty()) { + return lookup(peer_id, peer_info); + } + return {}; +} + +void MXMethod::remove(Http::HeaderMap& headers) const { + headers.remove(Headers::get().ExchangeMetadataHeaderId); + headers.remove(Headers::get().ExchangeMetadataHeader); +} + +absl::optional MXMethod::lookup(absl::string_view id, absl::string_view value) const { + // This code is copied from: + // https://github.com/istio/proxy/blob/release-1.18/extensions/metadata_exchange/plugin.cc#L116 + auto& cache = tls_->cache_; + if (max_peer_cache_size_ > 0 && !id.empty()) { + auto it = cache.find(id); + if (it != cache.end()) { + return it->second; + } + } + const auto bytes = Base64::decodeWithoutPadding(value); + google::protobuf::Struct metadata; + if (!metadata.ParseFromString(bytes)) { + return {}; + } + auto out = Istio::Common::convertStructToWorkloadMetadata(metadata, additional_labels_); + if (max_peer_cache_size_ > 0 && !id.empty()) { + // do not let the cache grow beyond max cache size. + if (static_cast(cache.size()) > max_peer_cache_size_) { + cache.erase(cache.begin(), std::next(cache.begin(), max_peer_cache_size_ / 4)); + } + cache.emplace(id, *out); + } + return *out; +} + +MXPropagationMethod::MXPropagationMethod( + bool downstream, Server::Configuration::ServerFactoryContext& factory_context, + const absl::flat_hash_set& additional_labels, + const envoy::extensions::filters::http::peer_metadata::v3alpha:: + PeerMetadataConfig_IstioHeaders& istio_headers) + : downstream_(downstream), id_(factory_context.localInfo().node().id()), + value_(computeValue(additional_labels, factory_context)), + skip_external_clusters_(istio_headers.skip_external_clusters()) {} + +std::string MXPropagationMethod::computeValue( + const absl::flat_hash_set& additional_labels, + Server::Configuration::ServerFactoryContext& factory_context) const { + const auto obj = Istio::Common::convertStructToWorkloadMetadata( + factory_context.localInfo().node().metadata(), additional_labels); + const google::protobuf::Struct metadata = Istio::Common::convertWorkloadMetadataToStruct(*obj); + const std::string metadata_bytes = Istio::Common::serializeToStringDeterministic(metadata); + return Base64::encode(metadata_bytes.data(), metadata_bytes.size()); +} + +void MXPropagationMethod::inject(const StreamInfo::StreamInfo& info, Http::HeaderMap& headers, + Context& ctx) const { + if (skipMXHeaders(skip_external_clusters_, info)) { + return; + } + if (!downstream_ || ctx.request_peer_id_received_) { + headers.setReference(Headers::get().ExchangeMetadataHeaderId, id_); + } + if (!downstream_ || ctx.request_peer_received_) { + headers.setReference(Headers::get().ExchangeMetadataHeader, value_); + } +} + +FilterConfig::FilterConfig( + const envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig& config, + Server::Configuration::FactoryContext& factory_context) + : shared_with_upstream_(config.shared_with_upstream()), + downstream_discovery_(buildDiscoveryMethods(config.downstream_discovery(), + buildAdditionalLabels(config.additional_labels()), + true, factory_context)), + upstream_discovery_(buildDiscoveryMethods(config.upstream_discovery(), + buildAdditionalLabels(config.additional_labels()), + false, factory_context)), + downstream_propagation_(buildPropagationMethods( + config.downstream_propagation(), buildAdditionalLabels(config.additional_labels()), true, + factory_context)), + upstream_propagation_(buildPropagationMethods( + config.upstream_propagation(), buildAdditionalLabels(config.additional_labels()), false, + factory_context)) {} + +std::vector FilterConfig::buildDiscoveryMethods( + const Protobuf::RepeatedPtrField< + envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig:: + DiscoveryMethod>& config, + const absl::flat_hash_set& additional_labels, bool downstream, + Server::Configuration::FactoryContext& factory_context) const { + std::vector methods; + methods.reserve(config.size()); + for (const auto& method : config) { + switch (method.method_specifier_case()) { + case envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig:: + DiscoveryMethod::MethodSpecifierCase::kWorkloadDiscovery: + methods.push_back( + std::make_unique(downstream, factory_context.serverFactoryContext())); + break; + case envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig:: + DiscoveryMethod::MethodSpecifierCase::kIstioHeaders: + methods.push_back(std::make_unique(downstream, additional_labels, + factory_context.serverFactoryContext())); + break; + default: + break; + } + } + return methods; +} + +std::vector FilterConfig::buildPropagationMethods( + const Protobuf::RepeatedPtrField< + envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig:: + PropagationMethod>& config, + const absl::flat_hash_set& additional_labels, bool downstream, + Server::Configuration::FactoryContext& factory_context) const { + std::vector methods; + methods.reserve(config.size()); + for (const auto& method : config) { + switch (method.method_specifier_case()) { + case envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig:: + PropagationMethod::MethodSpecifierCase::kIstioHeaders: + methods.push_back( + std::make_unique(downstream, factory_context.serverFactoryContext(), + additional_labels, method.istio_headers())); + break; + default: + break; + } + } + return methods; +} + +absl::flat_hash_set +FilterConfig::buildAdditionalLabels(const Protobuf::RepeatedPtrField& labels) const { + absl::flat_hash_set result; + for (const auto& label : labels) { + result.emplace(label); + } + return result; +} + +void FilterConfig::discoverDownstream(StreamInfo::StreamInfo& info, Http::RequestHeaderMap& headers, + Context& ctx) const { + discover(info, true, headers, ctx); +} + +void FilterConfig::discoverUpstream(StreamInfo::StreamInfo& info, Http::ResponseHeaderMap& headers, + Context& ctx) const { + discover(info, false, headers, ctx); +} + +void FilterConfig::discover(StreamInfo::StreamInfo& info, bool downstream, Http::HeaderMap& headers, + Context& ctx) const { + for (const auto& method : downstream ? downstream_discovery_ : upstream_discovery_) { + const auto result = method->derivePeerInfo(info, headers, ctx); + if (result) { + setFilterState(info, downstream, *result); + break; + } + } + for (const auto& method : downstream ? downstream_discovery_ : upstream_discovery_) { + method->remove(headers); + } +} + +void FilterConfig::injectDownstream(const StreamInfo::StreamInfo& info, + Http::ResponseHeaderMap& headers, Context& ctx) const { + for (const auto& method : downstream_propagation_) { + method->inject(info, headers, ctx); + } +} + +void FilterConfig::injectUpstream(const StreamInfo::StreamInfo& info, + Http::RequestHeaderMap& headers, Context& ctx) const { + for (const auto& method : upstream_propagation_) { + method->inject(info, headers, ctx); + } +} + +void FilterConfig::setFilterState(StreamInfo::StreamInfo& info, bool downstream, + const PeerInfo& value) const { + const absl::string_view key = + downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer; + if (!info.filterState()->hasDataWithName(key)) { + // Use CelState to allow operation filter_state.upstream_peer.labels['role'] + auto pb = value.serializeAsProto(); + auto peer_info = std::make_unique(FilterConfig::peerInfoPrototype()); + peer_info->setValue(absl::string_view(pb->SerializeAsString())); + info.filterState()->setData( + key, std::move(peer_info), StreamInfo::FilterState::StateType::Mutable, + StreamInfo::FilterState::LifeSpan::FilterChain, sharedWithUpstream()); + } else { + ENVOY_LOG(debug, "Duplicate peer metadata, skipping"); + } +} + +Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool) { + config_->discoverDownstream(decoder_callbacks_->streamInfo(), headers, ctx_); + config_->injectUpstream(decoder_callbacks_->streamInfo(), headers, ctx_); + return Http::FilterHeadersStatus::Continue; +} + +bool MXPropagationMethod::skipMXHeaders(const bool skip_external_clusters, + const StreamInfo::StreamInfo& info) const { + // We skip metadata in two cases. + // 1. skip_external_clusters is enabled, and we detect the upstream as external. + const auto& cluster_info = info.upstreamClusterInfo(); + if (cluster_info && cluster_info.value()) { + const auto& cluster_name = cluster_info.value()->name(); + // PassthroughCluster is always considered external + if (skip_external_clusters && cluster_name == "PassthroughCluster") { + return true; + } + const auto& filter_metadata = cluster_info.value()->metadata().filter_metadata(); + const auto& it = filter_metadata.find("istio"); + // Otherwise, cluster must be tagged as external + if (it != filter_metadata.end()) { + if (skip_external_clusters) { + const auto& skip_mx = it->second.fields().find("external"); + if (skip_mx != it->second.fields().end()) { + if (skip_mx->second.bool_value()) { + return true; + } + } + } + const auto& skip_mx = it->second.fields().find("disable_mx"); + if (skip_mx != it->second.fields().end()) { + if (skip_mx->second.bool_value()) { + return true; + } + } + } + } + return false; +} + +Http::FilterHeadersStatus Filter::encodeHeaders(Http::ResponseHeaderMap& headers, bool) { + config_->discoverUpstream(decoder_callbacks_->streamInfo(), headers, ctx_); + config_->injectDownstream(decoder_callbacks_->streamInfo(), headers, ctx_); + return Http::FilterHeadersStatus::Continue; +} + +} // namespace PeerMetadata +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/peer_metadata/filters/http/source/filter.h b/contrib/peer_metadata/filters/http/source/filter.h new file mode 100644 index 0000000000000..6a787892e451d --- /dev/null +++ b/contrib/peer_metadata/filters/http/source/filter.h @@ -0,0 +1,164 @@ +// Copyright 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 "envoy/extensions/filters/http/peer_metadata/v3alpha/peer_metadata.pb.h" + +#include "source/common/singleton/const_singleton.h" +#include "source/extensions/filters/common/expr/cel_state.h" +#include "source/extensions/filters/http/common/pass_through_filter.h" + +#include "contrib/common/metadata_object/source/metadata_object.h" +#include "contrib/common/workload_discovery/source/api.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace PeerMetadata { + +using ::Envoy::Extensions::Filters::Common::Expr::CelStatePrototype; +using ::Envoy::Extensions::Filters::Common::Expr::CelStateType; + +struct HeaderValues { + const Http::LowerCaseString ExchangeMetadataHeader{"x-envoy-peer-metadata"}; + const Http::LowerCaseString ExchangeMetadataHeaderId{"x-envoy-peer-metadata-id"}; +}; + +using Headers = ConstSingleton; + +using PeerInfo = Istio::Common::WorkloadMetadataObject; + +struct Context { + bool request_peer_id_received_{false}; + bool request_peer_received_{false}; +}; + +// Base class for the discovery methods. First derivation wins but all methods perform removal. +class DiscoveryMethod { +public: + virtual ~DiscoveryMethod() = default; + virtual absl::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, + Context&) const PURE; + virtual void remove(Http::HeaderMap&) const {} +}; + +using DiscoveryMethodPtr = std::unique_ptr; + +class MXMethod : public DiscoveryMethod { +public: + MXMethod(bool downstream, const absl::flat_hash_set additional_labels, + Server::Configuration::ServerFactoryContext& factory_context); + absl::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, + Context&) const override; + void remove(Http::HeaderMap&) const override; + +private: + absl::optional lookup(absl::string_view id, absl::string_view value) const; + const bool downstream_; + struct MXCache : public ThreadLocal::ThreadLocalObject { + absl::flat_hash_map cache_; + }; + mutable ThreadLocal::TypedSlot tls_; + const absl::flat_hash_set additional_labels_; + const int64_t max_peer_cache_size_{500}; +}; + +// Base class for the propagation methods. +class PropagationMethod { +public: + virtual ~PropagationMethod() = default; + virtual void inject(const StreamInfo::StreamInfo&, Http::HeaderMap&, Context&) const PURE; +}; + +using PropagationMethodPtr = std::unique_ptr; + +class MXPropagationMethod : public PropagationMethod { +public: + MXPropagationMethod(bool downstream, Server::Configuration::ServerFactoryContext& factory_context, + const absl::flat_hash_set& additional_labels, + const envoy::extensions::filters::http::peer_metadata::v3alpha:: + PeerMetadataConfig_IstioHeaders&); + void inject(const StreamInfo::StreamInfo&, Http::HeaderMap&, Context&) const override; + +private: + const bool downstream_; + std::string computeValue(const absl::flat_hash_set&, + Server::Configuration::ServerFactoryContext&) const; + const std::string id_; + const std::string value_; + const bool skip_external_clusters_; + bool skipMXHeaders(const bool, const StreamInfo::StreamInfo&) const; +}; + +class FilterConfig : public Logger::Loggable { +public: + FilterConfig(const envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig&, + Server::Configuration::FactoryContext&); + void discoverDownstream(StreamInfo::StreamInfo&, Http::RequestHeaderMap&, Context&) const; + void discoverUpstream(StreamInfo::StreamInfo&, Http::ResponseHeaderMap&, Context&) const; + void injectDownstream(const StreamInfo::StreamInfo&, Http::ResponseHeaderMap&, Context&) const; + void injectUpstream(const StreamInfo::StreamInfo&, Http::RequestHeaderMap&, Context&) const; + + static const CelStatePrototype& peerInfoPrototype() { + static const CelStatePrototype* const prototype = new CelStatePrototype( + true, CelStateType::Protobuf, "type.googleapis.com/google.protobuf.Struct", + StreamInfo::FilterState::LifeSpan::FilterChain); + return *prototype; + } + +private: + std::vector buildDiscoveryMethods( + const Protobuf::RepeatedPtrField&, + const absl::flat_hash_set& additional_labels, bool downstream, + Server::Configuration::FactoryContext&) const; + std::vector buildPropagationMethods( + const Protobuf::RepeatedPtrField&, + const absl::flat_hash_set& additional_labels, bool downstream, + Server::Configuration::FactoryContext&) const; + absl::flat_hash_set + buildAdditionalLabels(const Protobuf::RepeatedPtrField&) const; + StreamInfo::StreamSharingMayImpactPooling sharedWithUpstream() const { + return shared_with_upstream_ + ? StreamInfo::StreamSharingMayImpactPooling::SharedWithUpstreamConnectionOnce + : StreamInfo::StreamSharingMayImpactPooling::None; + } + void discover(StreamInfo::StreamInfo&, bool downstream, Http::HeaderMap&, Context&) const; + void setFilterState(StreamInfo::StreamInfo&, bool downstream, const PeerInfo& value) const; + const bool shared_with_upstream_; + const std::vector downstream_discovery_; + const std::vector upstream_discovery_; + const std::vector downstream_propagation_; + const std::vector upstream_propagation_; +}; + +using FilterConfigSharedPtr = std::shared_ptr; + +class Filter : public Http::PassThroughFilter { +public: + Filter(const FilterConfigSharedPtr& config) : config_(config) {} + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap&, bool) override; + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap&, bool) override; + +private: + FilterConfigSharedPtr config_; + Context ctx_; +}; + +} // namespace PeerMetadata +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/peer_metadata/filters/http/test/BUILD b/contrib/peer_metadata/filters/http/test/BUILD new file mode 100644 index 0000000000000..d6f198be8fa2a --- /dev/null +++ b/contrib/peer_metadata/filters/http/test/BUILD @@ -0,0 +1,32 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_test( + name = "filter_test", + srcs = ["filter_test.cc"], + deps = [ + "//contrib/peer_metadata/filters/http/source:filter_lib", + "//source/common/network:address_lib", + "//test/common/stream_info:test_util", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/stream_info:stream_info_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + deps = [ + "//contrib/peer_metadata/filters/http/source:config", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/contrib/peer_metadata/filters/http/test/config_test.cc b/contrib/peer_metadata/filters/http/test/config_test.cc new file mode 100644 index 0000000000000..5e2d5ef136ffa --- /dev/null +++ b/contrib/peer_metadata/filters/http/test/config_test.cc @@ -0,0 +1,48 @@ +// Copyright 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 "contrib/peer_metadata/filters/http/source/config.h" + +#include "test/mocks/server/factory_context.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace PeerMetadata { + +TEST(PeerMetadataConfigTest, PeerMetadataFilter) { + NiceMock context; + FilterConfigFactory factory; + const std::string yaml_string = R"EOF( + downstream_discovery: + - istio_headers: {} + )EOF"; + + envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig proto_config; + TestUtility::loadFromYaml(yaml_string, proto_config); + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProtoTyped(proto_config, "", context); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + cb(filter_callback); +} + +} // namespace PeerMetadata +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/peer_metadata/filters/http/test/filter_test.cc b/contrib/peer_metadata/filters/http/test/filter_test.cc new file mode 100644 index 0000000000000..3ed14ff2db2e2 --- /dev/null +++ b/contrib/peer_metadata/filters/http/test/filter_test.cc @@ -0,0 +1,221 @@ +// Copyright 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 "contrib/peer_metadata/filters/http/source/filter.h" + +#include "source/common/network/address_impl.h" + +#include "test/common/stream_info/test_util.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/stream_info/mocks.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using Istio::Common::WorkloadMetadataObject; +using testing::HasSubstr; +using testing::Invoke; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace PeerMetadata { +namespace { + +class MockSingletonManager : public Singleton::Manager { +public: + MockSingletonManager() {} + ~MockSingletonManager() override {} + MOCK_METHOD(Singleton::InstanceSharedPtr, get, + (const std::string& name, Singleton::SingletonFactoryCb cb, bool pin)); +}; + +class MockWorkloadMetadataProvider + : public Extensions::Common::WorkloadDiscovery::WorkloadMetadataProvider, + public Singleton::Instance { +public: + MockWorkloadMetadataProvider() {} + ~MockWorkloadMetadataProvider() override {} + MOCK_METHOD(std::optional, GetMetadata, + (const Network::Address::InstanceConstSharedPtr& address)); +}; + +class PeerMetadataTest : public testing::Test { +protected: + PeerMetadataTest() { + ON_CALL(context_.server_factory_context_, singletonManager()) + .WillByDefault(ReturnRef(singleton_manager_)); + metadata_provider_ = std::make_shared>(); + ON_CALL(singleton_manager_, get(HasSubstr("workload_metadata_provider"), _, _)) + .WillByDefault(Return(metadata_provider_)); + } + void initialize(const std::string& yaml_config) { + TestUtility::loadFromYaml(yaml_config, config_); + FilterConfigFactory factory; + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProtoTyped(config_, "", context_); + Http::MockFilterChainFactoryCallbacks filter_callback; + ON_CALL(filter_callback, addStreamFilter(_)).WillByDefault(testing::SaveArg<0>(&filter_)); + EXPECT_CALL(filter_callback, addStreamFilter(_)); + cb(filter_callback); + ON_CALL(decoder_callbacks_, streamInfo()).WillByDefault(testing::ReturnRef(stream_info_)); + filter_->setDecoderFilterCallbacks(decoder_callbacks_); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, true)); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, true)); + } + void checkNoPeer(bool downstream) { + EXPECT_FALSE(stream_info_.filterState()->hasDataWithName( + downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer)); + } + void checkPeerNamespace(bool downstream, const std::string& expected) { + const auto* cel_state = + stream_info_.filterState() + ->getDataReadOnly( + downstream ? Istio::Common::DownstreamPeer : Istio::Common::UpstreamPeer); + Protobuf::Struct obj; + ASSERT_TRUE(obj.ParseFromString(cel_state->value().data())); + EXPECT_EQ(expected, extractString(obj, "namespace")); + } + + absl::string_view extractString(const Protobuf::Struct& metadata, absl::string_view key) { + const auto& it = metadata.fields().find(key); + if (it == metadata.fields().end()) { + return {}; + } + return it->second.string_value(); + } + + void checkShared(bool expected) { + EXPECT_EQ(expected, + stream_info_.filterState()->objectsSharedWithUpstreamConnection()->size() > 0); + } + NiceMock context_; + NiceMock singleton_manager_; + std::shared_ptr> metadata_provider_; + NiceMock stream_info_; + NiceMock decoder_callbacks_; + Http::TestRequestHeaderMapImpl request_headers_; + Http::TestResponseHeaderMapImpl response_headers_; + envoy::extensions::filters::http::peer_metadata::v3alpha::PeerMetadataConfig config_; + Http::StreamFilterSharedPtr filter_; +}; + +TEST_F(PeerMetadataTest, None) { + initialize("{}"); + EXPECT_EQ(0, request_headers_.size()); + EXPECT_EQ(0, response_headers_.size()); + checkNoPeer(true); + checkNoPeer(false); +} + +TEST_F(PeerMetadataTest, DownstreamXDSNone) { + EXPECT_CALL(*metadata_provider_, GetMetadata(_)).WillRepeatedly(Return(std::nullopt)); + initialize(R"EOF( + downstream_discovery: + - workload_discovery: {} + )EOF"); + EXPECT_EQ(0, request_headers_.size()); + EXPECT_EQ(0, response_headers_.size()); + checkNoPeer(true); + checkNoPeer(false); +} + +TEST_F(PeerMetadataTest, DownstreamXDS) { + const WorkloadMetadataObject pod("pod-foo-1234", "my-cluster", "default", "foo", "foo-service", + "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, ""); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)) + .WillRepeatedly(Invoke([&](const Network::Address::InstanceConstSharedPtr& address) + -> std::optional { + if (absl::StartsWith(address->asStringView(), "127.0.0.1")) { + return {pod}; + } + return {}; + })); + initialize(R"EOF( + downstream_discovery: + - workload_discovery: {} + )EOF"); + EXPECT_EQ(0, request_headers_.size()); + EXPECT_EQ(0, response_headers_.size()); + checkPeerNamespace(true, "default"); + checkNoPeer(false); + checkShared(false); +} + +TEST_F(PeerMetadataTest, DownstreamMXEmpty) { + initialize(R"EOF( + downstream_discovery: + - istio_headers: {} + )EOF"); + EXPECT_EQ(0, request_headers_.size()); + EXPECT_EQ(0, response_headers_.size()); + checkNoPeer(true); + checkNoPeer(false); +} + +constexpr absl::string_view SampleIstioHeader = + "ChIKBWlzdGlvEgkaB3NpZGVjYXIKDgoIU1RTX1BPUlQSAhoAChEKB01FU0hfSUQSBhoEbWVzaAocChZTVEFDS0RSSVZFUl" + "9UT0tFTl9GSUxFEgIaAAowCihTVEFDS0RSSVZFUl9MT0dHSU5HX0VYUE9SVF9JTlRFUlZBTF9TRUNTEgQaAjIwCjYKDElO" + "U1RBTkNFX0lQUxImGiQxMC41Mi4wLjM0LGZlODA6OmEwNzU6MTFmZjpmZTVlOmYxY2QKFAoDYXBwEg0aC3Byb2R1Y3RwYW" + "dlCisKG1NFQ1VSRV9TVEFDS0RSSVZFUl9FTkRQT0lOVBIMGgpsb2NhbGhvc3Q6Cl0KGmt1YmVybmV0ZXMuaW8vbGltaXQt" + "cmFuZ2VyEj8aPUxpbWl0UmFuZ2VyIHBsdWdpbiBzZXQ6IGNwdSByZXF1ZXN0IGZvciBjb250YWluZXIgcHJvZHVjdHBhZ2" + "UKIQoNV09SS0xPQURfTkFNRRIQGg5wcm9kdWN0cGFnZS12MQofChFJTlRFUkNFUFRJT05fTU9ERRIKGghSRURJUkVDVAoe" + "CgpDTFVTVEVSX0lEEhAaDmNsaWVudC1jbHVzdGVyCkkKD0lTVElPX1BST1hZX1NIQRI2GjRpc3Rpby1wcm94eTo0N2U0NT" + "U5YjhlNGYwZDUxNmMwZDE3YjIzM2QxMjdhM2RlYjNkN2NlClIKBU9XTkVSEkkaR2t1YmVybmV0ZXM6Ly9hcGlzL2FwcHMv" + "djEvbmFtZXNwYWNlcy9kZWZhdWx0L2RlcGxveW1lbnRzL3Byb2R1Y3RwYWdlLXYxCsEBCgZMQUJFTFMStgEqswEKFAoDYX" + "BwEg0aC3Byb2R1Y3RwYWdlCiEKEXBvZC10ZW1wbGF0ZS1oYXNoEgwaCjg0OTc1YmM3NzgKMwofc2VydmljZS5pc3Rpby5p" + "by9jYW5vbmljYWwtbmFtZRIQGg5wcm9kdWN0cGFnZS12MQoyCiNzZXJ2aWNlLmlzdGlvLmlvL2Nhbm9uaWNhbC1yZXZpc2" + "lvbhILGgl2ZXJzaW9uLTEKDwoHdmVyc2lvbhIEGgJ2MQopCgROQU1FEiEaH3Byb2R1Y3RwYWdlLXYxLTg0OTc1YmM3Nzgt" + "cHh6MncKLQoIUE9EX05BTUUSIRofcHJvZHVjdHBhZ2UtdjEtODQ5NzViYzc3OC1weHoydwoaCg1JU1RJT19WRVJTSU9OEg" + "kaBzEuNS1kZXYKHwoVSU5DTFVERV9JTkJPVU5EX1BPUlRTEgYaBDkwODAKmwEKEVBMQVRGT1JNX01FVEFEQVRBEoUBKoIB" + "CiYKFGdjcF9na2VfY2x1c3Rlcl9uYW1lEg4aDHRlc3QtY2x1c3RlcgocCgxnY3BfbG9jYXRpb24SDBoKdXMtZWFzdDQtYg" + "odCgtnY3BfcHJvamVjdBIOGgx0ZXN0LXByb2plY3QKGwoSZ2NwX3Byb2plY3RfbnVtYmVyEgUaAzEyMwopCg9TRVJWSUNF" + "X0FDQ09VTlQSFhoUYm9va2luZm8tcHJvZHVjdHBhZ2UKHQoQQ09ORklHX05BTUVTUEFDRRIJGgdkZWZhdWx0Cg8KB3Zlcn" + "Npb24SBBoCdjEKHgoYU1RBQ0tEUklWRVJfUk9PVF9DQV9GSUxFEgIaAAohChFwb2QtdGVtcGxhdGUtaGFzaBIMGgo4NDk3" + "NWJjNzc4Ch8KDkFQUF9DT05UQUlORVJTEg0aC3Rlc3QsYm9uemFpChYKCU5BTUVTUEFDRRIJGgdkZWZhdWx0CjMKK1NUQU" + "NLRFJJVkVSX01PTklUT1JJTkdfRVhQT1JUX0lOVEVSVkFMX1NFQ1MSBBoCMjA"; + +TEST_F(PeerMetadataTest, DownstreamMX) { + request_headers_.setReference(Headers::get().ExchangeMetadataHeaderId, "test-pod"); + request_headers_.setReference(Headers::get().ExchangeMetadataHeader, SampleIstioHeader); + initialize(R"EOF( + downstream_discovery: + - istio_headers: {} + )EOF"); + EXPECT_EQ(0, request_headers_.size()); + EXPECT_EQ(0, response_headers_.size()); + checkPeerNamespace(true, "default"); + checkNoPeer(false); + checkShared(false); +} + +TEST_F(PeerMetadataTest, UpstreamMXPropagation) { + initialize(R"EOF( + upstream_propagation: + - istio_headers: + skip_external_clusters: false + )EOF"); + EXPECT_EQ(2, request_headers_.size()); + EXPECT_EQ(0, response_headers_.size()); + checkNoPeer(true); + checkNoPeer(false); +} + +} // namespace +} // namespace PeerMetadata +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy