mirror of https://github.com/linkerd/linkerd2.git
policy: add `policy.linkerd.io` `HTTPRoute` CRD (#8949)
Our use of the `gateway.networking.k8s.io` types is not compliant with the gateway API spec in at least a few ways: 1. We do not support the `Gateway` types. This is considered a "core" feature of the `HTTPRoute` type. 2. We do not currently update `HTTPRoute` status fields as dictated by the spec. 3. Our use of Linkerd-specific `parentRef` types may not work well with the gateway project's admission controller (untested). Issue #8944 proposes solving this by replacing our use of `gateway.networking.k8s.io`'s `HTTPRoute` type with our own `policy.linkerd.io` version of the same type. That issue suggests that the new `policy.linkerd.io` types be added separately from the change that removes support for the `gateway.networking.k8s.io` versions, so that the migration can be done incrementally. This branch does the following: * Add new `HTTPRoute` CRDs. These are based on the `gateway.networking.k8s.io` CRDs, with the following changes: - The group is `policy.linkerd.io`, - The API version is `v1alpha1`, - `backendRefs` fields are removed, as Linkerd does not support them, - filter types Linkerd does not support (`RequestMirror` and `ExtensionRef`), are removed. * Add Rust bindings for the new `policy.linkerd.io` versions of `HTTPRoute` types in `linkerd-policy-controller-k8s-api`. The Rust bindings define their own versions of the `HttpRoute`, `HttpRouteRule`, and `HttpRouteFilter` types, because these types' structures are changed from the Gateway API versions (due to the removal of unsupported filter types and fields). For other types, which are identical to the upstream Gateway API versions (such as the various match types and filter types), we re-export the existing bindings from the `k8s-gateway-api`crate to minimize duplication. * Add conversions to `InboundRouteBinding` from the `policy.linkerd.io` `HTTPRoute` types. When possible, I tried to factor out the code that was shared between the conversions for Linkerd's `HTTPRoute` types and the upstream Gateway API versions. * Implement `kubert`'s `IndexNamespacedResource` trait for `linkerd_policy_controller_k8s_api::policy::HttpRoute`, so that the policy controller can index both versions of the `HTTPRoute` CRD. * Adds validation for `policy.linkerd.io` `HTTPRoute`s to the policy controller's validating admission webhook. * Updated the policy controller tests to test both versions of `HTTPRoute`. ## Notes A couple questions I had about this approach: - Is re-using bindings from the `k8s-gateway-api` crate appropriate here, when the type has not changed from the Gateway API version? If not, I can change this PR to vendor those types as well, but it will result in a lot more code duplication. - Right now, the indexer stores all `HTTPRoute`s in the same index. This means that applying a `policy.linkerd.io` version of `HTTPRoute` and then applying the Gateway API version with the same ns/name will update the same value in the index. Is this what we want? I wasn't entirely sure... See #8944.
This commit is contained in:
parent
7e98376f20
commit
753c73e0a0
|
|
@ -1009,6 +1009,7 @@ name = "linkerd-policy-controller-k8s-api"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ipnet",
|
||||
"k8s-gateway-api",
|
||||
"k8s-openapi",
|
||||
"kube",
|
||||
"schemars",
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -199,6 +200,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -46,6 +46,7 @@ var (
|
|||
templatesCrdFiles = []string{
|
||||
"templates/gateway.networking.k8s.io/httproute.yaml",
|
||||
"templates/policy/authorization-policy.yaml",
|
||||
"templates/policy/httproute.yaml",
|
||||
"templates/policy/meshtls-authentication.yaml",
|
||||
"templates/policy/network-authentication.yaml",
|
||||
"templates/policy/server-authorization.yaml",
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -194,6 +195,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -194,6 +195,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -162,6 +162,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -194,6 +195,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -194,6 +195,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -199,6 +200,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ webhooks:
|
|||
apiVersions: ["v1alpha1", "v1beta1"]
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- networkauthentications
|
||||
- meshtlsauthentications
|
||||
- serverauthorizations
|
||||
|
|
@ -202,6 +203,7 @@ rules:
|
|||
- policy.linkerd.io
|
||||
resources:
|
||||
- authorizationpolicies
|
||||
- httproutes
|
||||
- meshtlsauthentications
|
||||
- networkauthentications
|
||||
- servers
|
||||
|
|
|
|||
1
justfile
1
justfile
|
|
@ -156,6 +156,7 @@ test-cluster-install-crds: _test-cluster-exists && _test-cluster-crds-ready
|
|||
_test-cluster-crds-ready:
|
||||
{{ _kubectl }} wait --for condition=established --timeout=60s crd \
|
||||
authorizationpolicies.policy.linkerd.io \
|
||||
httproutes.policy.linkerd.io \
|
||||
meshtlsauthentications.policy.linkerd.io \
|
||||
networkauthentications.policy.linkerd.io \
|
||||
serverauthorizations.policy.linkerd.io \
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
k8s-openapi = { version = "0.15", default-features = false, features = ["v1_20"] }
|
||||
k8s-gateway-api = "0.6.0"
|
||||
kube = { version = "0.74", default-features = false, features = ["client", "derive", "runtime"] }
|
||||
ipnet = { version = "2.5", features = ["json"] }
|
||||
schemars = "0.8"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod authorization_policy;
|
||||
pub mod httproute;
|
||||
pub mod meshtls_authentication;
|
||||
mod network;
|
||||
pub mod network_authentication;
|
||||
|
|
@ -8,6 +9,7 @@ pub mod target_ref;
|
|||
|
||||
pub use self::{
|
||||
authorization_policy::{AuthorizationPolicy, AuthorizationPolicySpec},
|
||||
httproute::{HttpRoute, HttpRouteSpec},
|
||||
meshtls_authentication::{MeshTLSAuthentication, MeshTLSAuthenticationSpec},
|
||||
network::Network,
|
||||
network_authentication::{NetworkAuthentication, NetworkAuthenticationSpec},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
pub use k8s_gateway_api::{
|
||||
CommonRouteSpec, Hostname, HttpHeaderMatch, HttpHeaderName, HttpMethod, HttpPathMatch,
|
||||
HttpQueryParamMatch, HttpRequestHeaderFilter, HttpRequestRedirectFilter, HttpRouteMatch,
|
||||
LocalObjectReference, ParentReference, RouteStatus,
|
||||
};
|
||||
|
||||
/// HTTPRoute provides a way to route HTTP requests. This includes the
|
||||
/// capability to match requests by hostname, path, header, or query param.
|
||||
/// Filters can be used to specify additional processing steps. Backends specify
|
||||
/// where matching requests should be routed.
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
Default,
|
||||
kube::CustomResource,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
schemars::JsonSchema,
|
||||
)]
|
||||
#[kube(
|
||||
group = "policy.linkerd.io",
|
||||
version = "v1alpha1",
|
||||
kind = "HTTPRoute",
|
||||
struct = "HttpRoute",
|
||||
status = "HttpRouteStatus",
|
||||
namespaced
|
||||
)]
|
||||
pub struct HttpRouteSpec {
|
||||
/// Common route information.
|
||||
#[serde(flatten)]
|
||||
pub inner: CommonRouteSpec,
|
||||
|
||||
/// Hostnames defines a set of hostname that should match against the HTTP
|
||||
/// Host header to select a HTTPRoute to process the request. This matches
|
||||
/// the RFC 1123 definition of a hostname with 2 notable exceptions:
|
||||
///
|
||||
/// 1. IPs are not allowed.
|
||||
/// 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard
|
||||
/// label must appear by itself as the first label.
|
||||
pub hostnames: Option<Vec<Hostname>>,
|
||||
|
||||
/// Rules are a list of HTTP matchers, filters and actions.
|
||||
pub rules: Option<Vec<HttpRouteRule>>,
|
||||
}
|
||||
|
||||
/// HTTPRouteRule defines semantics for matching an HTTP request based on
|
||||
/// conditions (matches), processing it (filters), and forwarding the request to
|
||||
/// an API object (backendRefs).
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HttpRouteRule {
|
||||
/// Matches define conditions used for matching the rule against incoming
|
||||
/// HTTP requests. Each match is independent, i.e. this rule will be matched
|
||||
/// if **any** one of the matches is satisfied.
|
||||
///
|
||||
/// For example, take the following matches configuration:
|
||||
///
|
||||
/// ```yaml
|
||||
/// matches:
|
||||
/// - path:
|
||||
/// value: "/foo"
|
||||
/// headers:
|
||||
/// - name: "version"
|
||||
/// value: "v2"
|
||||
/// - path:
|
||||
/// value: "/v2/foo"
|
||||
/// ```
|
||||
///
|
||||
/// For a request to match against this rule, a request must satisfy
|
||||
/// EITHER of the two conditions:
|
||||
///
|
||||
/// - path prefixed with `/foo` AND contains the header `version: v2`
|
||||
/// - path prefix of `/v2/foo`
|
||||
///
|
||||
/// See the documentation for HTTPRouteMatch on how to specify multiple
|
||||
/// match conditions that should be ANDed together.
|
||||
///
|
||||
/// If no matches are specified, the default is a prefix
|
||||
/// path match on "/", which has the effect of matching every
|
||||
/// HTTP request.
|
||||
///
|
||||
/// Proxy or Load Balancer routing configuration generated from HTTPRoutes
|
||||
/// MUST prioritize rules based on the following criteria, continuing on
|
||||
/// ties. Precedence must be given to the the Rule with the largest number
|
||||
/// of:
|
||||
///
|
||||
/// * Characters in a matching non-wildcard hostname.
|
||||
/// * Characters in a matching hostname.
|
||||
/// * Characters in a matching path.
|
||||
/// * Header matches.
|
||||
/// * Query param matches.
|
||||
///
|
||||
/// If ties still exist across multiple Routes, matching precedence MUST be
|
||||
/// determined in order of the following criteria, continuing on ties:
|
||||
///
|
||||
/// * The oldest Route based on creation timestamp.
|
||||
/// * The Route appearing first in alphabetical order by
|
||||
/// "{namespace}/{name}".
|
||||
///
|
||||
/// If ties still exist within the Route that has been given precedence,
|
||||
/// matching precedence MUST be granted to the first matching rule meeting
|
||||
/// the above criteria.
|
||||
///
|
||||
/// When no rules matching a request have been successfully attached to the
|
||||
/// parent a request is coming from, a HTTP 404 status code MUST be returned.
|
||||
pub matches: Option<Vec<HttpRouteMatch>>,
|
||||
|
||||
/// Filters define the filters that are applied to requests that match this
|
||||
/// rule.
|
||||
///
|
||||
/// The effects of ordering of multiple behaviors are currently unspecified.
|
||||
/// This can change in the future based on feedback during the alpha stage.
|
||||
///
|
||||
/// Conformance-levels at this level are defined based on the type of
|
||||
/// filter:
|
||||
///
|
||||
/// - ALL core filters MUST be supported by all implementations.
|
||||
/// - Implementers are encouraged to support extended filters.
|
||||
/// - Implementation-specific custom filters have no API guarantees across
|
||||
/// implementations.
|
||||
///
|
||||
/// Specifying a core filter multiple times has unspecified or custom
|
||||
/// conformance.
|
||||
///
|
||||
/// Support: Core
|
||||
pub filters: Option<Vec<HttpRouteFilter>>,
|
||||
}
|
||||
|
||||
/// HTTPRouteFilter defines processing steps that must be completed during the
|
||||
/// request or response lifecycle. HTTPRouteFilters are meant as an extension
|
||||
/// point to express processing that may be done in Gateway implementations.
|
||||
/// Some examples include request or response modification, implementing
|
||||
/// authentication strategies, rate-limiting, and traffic shaping. API
|
||||
/// guarantee/conformance is defined based on the type of the filter.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
|
||||
#[serde(tag = "type", rename_all = "PascalCase")]
|
||||
pub enum HttpRouteFilter {
|
||||
/// RequestHeaderModifier defines a schema for a filter that modifies request
|
||||
/// headers.
|
||||
///
|
||||
/// Support: Core
|
||||
#[serde(rename_all = "camelCase")]
|
||||
RequestHeaderModifier {
|
||||
request_header_modifier: HttpRequestHeaderFilter,
|
||||
},
|
||||
|
||||
/// RequestRedirect defines a schema for a filter that responds to the
|
||||
/// request with an HTTP redirection.
|
||||
///
|
||||
/// Support: Core
|
||||
#[serde(rename_all = "camelCase")]
|
||||
RequestRedirect {
|
||||
request_redirect: HttpRequestRedirectFilter,
|
||||
},
|
||||
}
|
||||
|
||||
/// HTTPRouteStatus defines the observed state of HTTPRoute.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
|
||||
pub struct HttpRouteStatus {
|
||||
/// Common route status information.
|
||||
#[serde(flatten)]
|
||||
pub inner: RouteStatus,
|
||||
}
|
||||
|
|
@ -60,17 +60,18 @@ impl TryFrom<k8s::policy::AuthorizationPolicySpec> for Spec {
|
|||
}
|
||||
|
||||
fn target(t: LocalTargetRef) -> Result<Target> {
|
||||
if t.targets_kind::<k8s::policy::Server>() {
|
||||
Ok(Target::Server(t.name))
|
||||
} else if t.targets_kind::<k8s::Namespace>() {
|
||||
Ok(Target::Namespace)
|
||||
} else if t.targets_kind::<k8s_gateway_api::HttpRoute>() {
|
||||
Ok(Target::HttpRoute(t.name))
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
match t {
|
||||
t if t.targets_kind::<k8s::policy::Server>() => Ok(Target::Server(t.name)),
|
||||
t if t.targets_kind::<k8s::Namespace>() => Ok(Target::Namespace),
|
||||
t if t.targets_kind::<k8s_gateway_api::HttpRoute>()
|
||||
|| t.targets_kind::<k8s::policy::HttpRoute>() =>
|
||||
{
|
||||
Ok(Target::HttpRoute(t.name))
|
||||
}
|
||||
_ => anyhow::bail!(
|
||||
"unsupported authorization target type: {}",
|
||||
t.canonical_kind()
|
||||
)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use ahash::AHashMap as HashMap;
|
|||
use anyhow::{bail, Error, Result};
|
||||
use k8s_gateway_api as api;
|
||||
use linkerd_policy_controller_core::http_route;
|
||||
use linkerd_policy_controller_k8s_api::policy::httproute as policy;
|
||||
use std::num::NonZeroU16;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
|
@ -34,70 +35,14 @@ impl TryFrom<api::HttpRoute> for InboundRouteBinding {
|
|||
type Error = Error;
|
||||
|
||||
fn try_from(route: api::HttpRoute) -> Result<Self, Self::Error> {
|
||||
let parents = route
|
||||
.spec
|
||||
.inner
|
||||
.parent_refs
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(
|
||||
|api::ParentReference {
|
||||
group,
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
section_name,
|
||||
port,
|
||||
}| {
|
||||
// Ignore parents that are not a Server.
|
||||
if let Some(g) = group {
|
||||
if let Some(k) = kind {
|
||||
if !g.eq_ignore_ascii_case("policy.linkerd.io")
|
||||
|| !k.eq_ignore_ascii_case("server")
|
||||
|| name.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if namespace.is_some() && namespace != route.metadata.namespace {
|
||||
return Some(Err(InvalidParentRef::ServerInAnotherNamespace));
|
||||
}
|
||||
if port.is_some() {
|
||||
return Some(Err(InvalidParentRef::SpecifiesPort));
|
||||
}
|
||||
if section_name.is_some() {
|
||||
return Some(Err(InvalidParentRef::SpecifiesSection));
|
||||
}
|
||||
|
||||
Some(Ok(InboundParentRef::Server(name)))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, InvalidParentRef>>()?;
|
||||
// If there are no valid parents, then the route is invalid.
|
||||
if parents.is_empty() {
|
||||
return Err(InvalidParentRef::DoesNotSelectServer.into());
|
||||
}
|
||||
|
||||
let route_ns = route.metadata.namespace.as_deref();
|
||||
let parents = InboundParentRef::collect_from(route_ns, route.spec.inner.parent_refs)?;
|
||||
let hostnames = route
|
||||
.spec
|
||||
.hostnames
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|hostname| {
|
||||
if hostname.starts_with("*.") {
|
||||
let mut reverse_labels = hostname
|
||||
.split('.')
|
||||
.skip(1)
|
||||
.map(|label| label.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
reverse_labels.reverse();
|
||||
http_route::HostMatch::Suffix { reverse_labels }
|
||||
} else {
|
||||
http_route::HostMatch::Exact(hostname)
|
||||
}
|
||||
})
|
||||
.map(convert::http_match)
|
||||
.collect();
|
||||
|
||||
let rules = route
|
||||
|
|
@ -105,7 +50,48 @@ impl TryFrom<api::HttpRoute> for InboundRouteBinding {
|
|||
.rules
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Self::try_rule)
|
||||
.map(
|
||||
|api::HttpRouteRule {
|
||||
matches,
|
||||
filters,
|
||||
backend_refs: _,
|
||||
}| Self::try_rule(matches, filters, Self::try_gateway_filter),
|
||||
)
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
Ok(InboundRouteBinding {
|
||||
parents,
|
||||
route: http_route::InboundHttpRoute {
|
||||
hostnames,
|
||||
rules,
|
||||
authorizations: HashMap::default(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<policy::HttpRoute> for InboundRouteBinding {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(route: policy::HttpRoute) -> Result<Self, Self::Error> {
|
||||
let route_ns = route.metadata.namespace.as_deref();
|
||||
let parents = InboundParentRef::collect_from(route_ns, route.spec.inner.parent_refs)?;
|
||||
let hostnames = route
|
||||
.spec
|
||||
.hostnames
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(convert::http_match)
|
||||
.collect();
|
||||
|
||||
let rules = route
|
||||
.spec
|
||||
.rules
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|policy::HttpRouteRule { matches, filters }| {
|
||||
Self::try_rule(matches, filters, Self::try_policy_filter)
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
Ok(InboundRouteBinding {
|
||||
|
|
@ -187,71 +173,39 @@ impl InboundRouteBinding {
|
|||
})
|
||||
}
|
||||
|
||||
fn try_rule(rule: api::HttpRouteRule) -> Result<http_route::InboundHttpRouteRule> {
|
||||
let matches = rule
|
||||
.matches
|
||||
fn try_rule<F>(
|
||||
matches: Option<Vec<api::HttpRouteMatch>>,
|
||||
filters: Option<Vec<F>>,
|
||||
try_filter: impl Fn(F) -> Result<http_route::InboundFilter>,
|
||||
) -> Result<http_route::InboundHttpRouteRule> {
|
||||
let matches = matches
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Self::try_match)
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
let filters = rule
|
||||
.filters
|
||||
let filters = filters
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Self::try_filter)
|
||||
.map(try_filter)
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
Ok(http_route::InboundHttpRouteRule { matches, filters })
|
||||
}
|
||||
|
||||
fn try_filter(filter: api::HttpRouteFilter) -> Result<http_route::InboundFilter> {
|
||||
fn try_gateway_filter(filter: api::HttpRouteFilter) -> Result<http_route::InboundFilter> {
|
||||
let filter = match filter {
|
||||
api::HttpRouteFilter::RequestHeaderModifier {
|
||||
request_header_modifier: api::HttpRequestHeaderFilter { set, add, remove },
|
||||
} => http_route::InboundFilter::RequestHeaderModifier(
|
||||
http_route::RequestHeaderModifierFilter {
|
||||
add: add
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|api::HttpHeader { name, value }| Ok((name.parse()?, value.parse()?)))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
set: set
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|api::HttpHeader { name, value }| Ok((name.parse()?, value.parse()?)))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
remove: remove
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(http_route::HeaderName::try_from)
|
||||
.collect::<Result<_, _>>()?,
|
||||
},
|
||||
),
|
||||
request_header_modifier,
|
||||
} => {
|
||||
let filter = convert::req_header_modifier(request_header_modifier)?;
|
||||
http_route::InboundFilter::RequestHeaderModifier(filter)
|
||||
}
|
||||
|
||||
api::HttpRouteFilter::RequestRedirect {
|
||||
request_redirect:
|
||||
api::HttpRequestRedirectFilter {
|
||||
scheme,
|
||||
hostname,
|
||||
path,
|
||||
port,
|
||||
status_code,
|
||||
},
|
||||
} => http_route::InboundFilter::RequestRedirect(http_route::RequestRedirectFilter {
|
||||
scheme: scheme.as_deref().map(TryInto::try_into).transpose()?,
|
||||
host: hostname,
|
||||
path: path.map(|path_mod| match path_mod {
|
||||
api::HttpPathModifier::ReplaceFullPath(s) => http_route::PathModifier::Full(s),
|
||||
api::HttpPathModifier::ReplacePrefixMatch(s) => {
|
||||
http_route::PathModifier::Prefix(s)
|
||||
}
|
||||
}),
|
||||
port: port.and_then(|p| NonZeroU16::try_from(p).ok()),
|
||||
status: status_code
|
||||
.map(http_route::StatusCode::try_from)
|
||||
.transpose()?,
|
||||
}),
|
||||
api::HttpRouteFilter::RequestRedirect { request_redirect } => {
|
||||
let filter = convert::req_redirect(request_redirect)?;
|
||||
http_route::InboundFilter::RequestRedirect(filter)
|
||||
}
|
||||
|
||||
api::HttpRouteFilter::RequestMirror { .. } => {
|
||||
bail!("RequestMirror filter is not supported")
|
||||
|
|
@ -265,4 +219,138 @@ impl InboundRouteBinding {
|
|||
};
|
||||
Ok(filter)
|
||||
}
|
||||
|
||||
fn try_policy_filter(filter: policy::HttpRouteFilter) -> Result<http_route::InboundFilter> {
|
||||
let filter = match filter {
|
||||
policy::HttpRouteFilter::RequestHeaderModifier {
|
||||
request_header_modifier,
|
||||
} => {
|
||||
let filter = convert::req_header_modifier(request_header_modifier)?;
|
||||
http_route::InboundFilter::RequestHeaderModifier(filter)
|
||||
}
|
||||
|
||||
policy::HttpRouteFilter::RequestRedirect { request_redirect } => {
|
||||
let filter = convert::req_redirect(request_redirect)?;
|
||||
http_route::InboundFilter::RequestRedirect(filter)
|
||||
}
|
||||
};
|
||||
Ok(filter)
|
||||
}
|
||||
}
|
||||
|
||||
impl InboundParentRef {
|
||||
fn collect_from(
|
||||
route_ns: Option<&str>,
|
||||
parent_refs: Option<Vec<api::ParentReference>>,
|
||||
) -> Result<Vec<Self>, InvalidParentRef> {
|
||||
let parents = parent_refs
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|parent_ref| Self::from_parent_ref(route_ns, parent_ref))
|
||||
.collect::<Result<Vec<_>, InvalidParentRef>>()?;
|
||||
|
||||
// If there are no valid parents, then the route is invalid.
|
||||
if parents.is_empty() {
|
||||
return Err(InvalidParentRef::DoesNotSelectServer);
|
||||
}
|
||||
|
||||
Ok(parents)
|
||||
}
|
||||
|
||||
fn from_parent_ref(
|
||||
route_ns: Option<&str>,
|
||||
api::ParentReference {
|
||||
group,
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
section_name,
|
||||
port,
|
||||
}: api::ParentReference,
|
||||
) -> Option<Result<Self, InvalidParentRef>> {
|
||||
// Ignore parents that are not a Server.
|
||||
if let Some(g) = group {
|
||||
if let Some(k) = kind {
|
||||
if !g.eq_ignore_ascii_case("policy.linkerd.io")
|
||||
|| !k.eq_ignore_ascii_case("server")
|
||||
|| name.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if namespace.is_some() && namespace.as_deref() != route_ns {
|
||||
return Some(Err(InvalidParentRef::ServerInAnotherNamespace));
|
||||
}
|
||||
if port.is_some() {
|
||||
return Some(Err(InvalidParentRef::SpecifiesPort));
|
||||
}
|
||||
if section_name.is_some() {
|
||||
return Some(Err(InvalidParentRef::SpecifiesSection));
|
||||
}
|
||||
|
||||
Some(Ok(InboundParentRef::Server(name)))
|
||||
}
|
||||
}
|
||||
|
||||
mod convert {
|
||||
use super::*;
|
||||
pub(super) fn http_match(hostname: api::Hostname) -> http_route::HostMatch {
|
||||
if hostname.starts_with("*.") {
|
||||
let mut reverse_labels = hostname
|
||||
.split('.')
|
||||
.skip(1)
|
||||
.map(|label| label.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
reverse_labels.reverse();
|
||||
http_route::HostMatch::Suffix { reverse_labels }
|
||||
} else {
|
||||
http_route::HostMatch::Exact(hostname)
|
||||
}
|
||||
}
|
||||
pub(super) fn req_header_modifier(
|
||||
api::HttpRequestHeaderFilter { set, add, remove }: api::HttpRequestHeaderFilter,
|
||||
) -> Result<http_route::RequestHeaderModifierFilter> {
|
||||
Ok(http_route::RequestHeaderModifierFilter {
|
||||
add: add
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|api::HttpHeader { name, value }| Ok((name.parse()?, value.parse()?)))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
set: set
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|api::HttpHeader { name, value }| Ok((name.parse()?, value.parse()?)))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
remove: remove
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(http_route::HeaderName::try_from)
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn req_redirect(
|
||||
api::HttpRequestRedirectFilter {
|
||||
scheme,
|
||||
hostname,
|
||||
path,
|
||||
port,
|
||||
status_code,
|
||||
}: api::HttpRequestRedirectFilter,
|
||||
) -> Result<http_route::RequestRedirectFilter> {
|
||||
Ok(http_route::RequestRedirectFilter {
|
||||
scheme: scheme.as_deref().map(TryInto::try_into).transpose()?,
|
||||
host: hostname,
|
||||
path: path.map(|path_mod| match path_mod {
|
||||
api::HttpPathModifier::ReplaceFullPath(s) => http_route::PathModifier::Full(s),
|
||||
api::HttpPathModifier::ReplacePrefixMatch(s) => http_route::PathModifier::Prefix(s),
|
||||
}),
|
||||
port: port.and_then(|p| NonZeroU16::try_from(p).ok()),
|
||||
status: status_code
|
||||
.map(http_route::StatusCode::try_from)
|
||||
.transpose()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,6 +185,92 @@ impl Index {
|
|||
ns.reindex(&self.authentications);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_route<R>(&mut self, route: R)
|
||||
where
|
||||
R: ResourceExt,
|
||||
InboundRouteBinding: TryFrom<R>,
|
||||
<InboundRouteBinding as TryFrom<R>>::Error: std::fmt::Display,
|
||||
{
|
||||
let ns = route.namespace().expect("HttpRoute must have a namespace");
|
||||
let name = route.name_unchecked();
|
||||
let _span = info_span!("apply", %ns, %name).entered();
|
||||
|
||||
let route_binding = match route.try_into() {
|
||||
Ok(binding) => binding,
|
||||
Err(error) => {
|
||||
tracing::info!(%ns, %name, %error, "Ignoring HTTPRoute");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.ns_or_default_with_reindex(ns, |ns| ns.policy.update_http_route(name, route_binding))
|
||||
}
|
||||
|
||||
fn reset_route<R>(&mut self, routes: Vec<R>, deleted: HashMap<String, HashSet<String>>)
|
||||
where
|
||||
R: ResourceExt,
|
||||
InboundRouteBinding: TryFrom<R>,
|
||||
<InboundRouteBinding as TryFrom<R>>::Error: std::fmt::Display,
|
||||
{
|
||||
let _span = info_span!("reset").entered();
|
||||
|
||||
// Aggregate all of the updates by namespace so that we only reindex
|
||||
// once per namespace.
|
||||
type Ns = NsUpdate<InboundRouteBinding>;
|
||||
let mut updates_by_ns = HashMap::<String, Ns>::default();
|
||||
for route in routes.into_iter() {
|
||||
let namespace = route.namespace().expect("HttpRoute must be namespaced");
|
||||
let name = route.name_unchecked();
|
||||
let route_binding = match route.try_into() {
|
||||
Ok(binding) => binding,
|
||||
Err(error) => {
|
||||
tracing::info!(ns = %namespace, %name, %error, "Ignoring HTTPRoute");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
updates_by_ns
|
||||
.entry(namespace)
|
||||
.or_default()
|
||||
.added
|
||||
.push((name, route_binding));
|
||||
}
|
||||
for (ns, names) in deleted.into_iter() {
|
||||
updates_by_ns.entry(ns).or_default().removed = names;
|
||||
}
|
||||
|
||||
for (namespace, Ns { added, removed }) in updates_by_ns.into_iter() {
|
||||
if added.is_empty() {
|
||||
// If there are no live resources in the namespace, we do not
|
||||
// want to create a default namespace instance, we just want to
|
||||
// clear out all resources for the namespace (and then drop the
|
||||
// whole namespace, if necessary).
|
||||
self.ns_with_reindex(namespace, |ns| {
|
||||
ns.policy.http_routes.clear();
|
||||
true
|
||||
});
|
||||
} else {
|
||||
// Otherwise, we take greater care to reindex only when the
|
||||
// state actually changed. The vast majority of resets will see
|
||||
// no actual data change.
|
||||
self.ns_or_default_with_reindex(namespace, |ns| {
|
||||
let mut changed = !removed.is_empty();
|
||||
for name in removed.into_iter() {
|
||||
ns.policy.http_routes.remove(&name);
|
||||
}
|
||||
for (name, route_binding) in added.into_iter() {
|
||||
changed = ns.policy.update_http_route(name, route_binding) || changed;
|
||||
}
|
||||
changed
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_route(&mut self, ns: String, name: String) {
|
||||
let _span = info_span!("delete", %ns, %name).entered();
|
||||
self.ns_with_reindex(ns, |ns| ns.policy.http_routes.remove(&name).is_some())
|
||||
}
|
||||
}
|
||||
|
||||
impl kubert::index::IndexNamespacedResource<k8s::Pod> for Index {
|
||||
|
|
@ -612,24 +698,11 @@ impl kubert::index::IndexNamespacedResource<k8s::policy::NetworkAuthentication>
|
|||
|
||||
impl kubert::index::IndexNamespacedResource<k8s_gateway_api::HttpRoute> for Index {
|
||||
fn apply(&mut self, route: k8s_gateway_api::HttpRoute) {
|
||||
let ns = route.namespace().expect("HttpRoute must have a namespace");
|
||||
let name = route.name_unchecked();
|
||||
let _span = info_span!("apply", %ns, %name).entered();
|
||||
|
||||
let route_binding = match route.try_into() {
|
||||
Ok(binding) => binding,
|
||||
Err(error) => {
|
||||
tracing::info!(%ns, %name, %error, "Ignoring HTTPRoute");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.ns_or_default_with_reindex(ns, |ns| ns.policy.update_http_route(name, route_binding))
|
||||
self.apply_route(route)
|
||||
}
|
||||
|
||||
fn delete(&mut self, ns: String, name: String) {
|
||||
let _span = info_span!("delete", %ns, %name).entered();
|
||||
self.ns_with_reindex(ns, |ns| ns.policy.http_routes.remove(&name).is_some())
|
||||
self.delete_route(ns, name)
|
||||
}
|
||||
|
||||
fn reset(
|
||||
|
|
@ -637,58 +710,25 @@ impl kubert::index::IndexNamespacedResource<k8s_gateway_api::HttpRoute> for Inde
|
|||
routes: Vec<k8s_gateway_api::HttpRoute>,
|
||||
deleted: HashMap<String, HashSet<String>>,
|
||||
) {
|
||||
let _span = info_span!("reset").entered();
|
||||
self.reset_route(routes, deleted)
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate all of the updates by namespace so that we only reindex
|
||||
// once per namespace.
|
||||
type Ns = NsUpdate<InboundRouteBinding>;
|
||||
let mut updates_by_ns = HashMap::<String, Ns>::default();
|
||||
for route in routes.into_iter() {
|
||||
let namespace = route.namespace().expect("HttpRoute must be namespaced");
|
||||
let name = route.name_unchecked();
|
||||
let route_binding = match route.try_into() {
|
||||
Ok(binding) => binding,
|
||||
Err(error) => {
|
||||
tracing::info!(ns = %namespace, %name, %error, "Ignoring HTTPRoute");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
updates_by_ns
|
||||
.entry(namespace)
|
||||
.or_default()
|
||||
.added
|
||||
.push((name, route_binding));
|
||||
}
|
||||
for (ns, names) in deleted.into_iter() {
|
||||
updates_by_ns.entry(ns).or_default().removed = names;
|
||||
}
|
||||
impl kubert::index::IndexNamespacedResource<k8s::policy::HttpRoute> for Index {
|
||||
fn apply(&mut self, route: k8s::policy::HttpRoute) {
|
||||
self.apply_route(route)
|
||||
}
|
||||
|
||||
for (namespace, Ns { added, removed }) in updates_by_ns.into_iter() {
|
||||
if added.is_empty() {
|
||||
// If there are no live resources in the namespace, we do not
|
||||
// want to create a default namespace instance, we just want to
|
||||
// clear out all resources for the namespace (and then drop the
|
||||
// whole namespace, if necessary).
|
||||
self.ns_with_reindex(namespace, |ns| {
|
||||
ns.policy.http_routes.clear();
|
||||
true
|
||||
});
|
||||
} else {
|
||||
// Otherwise, we take greater care to reindex only when the
|
||||
// state actually changed. The vast majority of resets will see
|
||||
// no actual data change.
|
||||
self.ns_or_default_with_reindex(namespace, |ns| {
|
||||
let mut changed = !removed.is_empty();
|
||||
for name in removed.into_iter() {
|
||||
ns.policy.http_routes.remove(&name);
|
||||
}
|
||||
for (name, route_binding) in added.into_iter() {
|
||||
changed = ns.policy.update_http_route(name, route_binding) || changed;
|
||||
}
|
||||
changed
|
||||
});
|
||||
}
|
||||
}
|
||||
fn delete(&mut self, ns: String, name: String) {
|
||||
self.delete_route(ns, name)
|
||||
}
|
||||
|
||||
fn reset(
|
||||
&mut self,
|
||||
routes: Vec<k8s::policy::HttpRoute>,
|
||||
deleted: HashMap<String, HashSet<String>>,
|
||||
) {
|
||||
self.reset_route(routes, deleted)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn route_attaches_to_server() {
|
||||
fn gateway_route_attaches_to_server() {
|
||||
let test = TestConfig::default();
|
||||
test_route_attaches_to_server(test, |index, route| index.apply(route.gateway_api()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linkerd_route_attaches_to_server() {
|
||||
let test = TestConfig::default();
|
||||
test_route_attaches_to_server(test, |index, route| index.apply(route.linkerd()))
|
||||
}
|
||||
|
||||
fn test_route_attaches_to_server(test: TestConfig, apply_route: impl FnOnce(&mut Index, MkRoute)) {
|
||||
// Create pod.
|
||||
let mut pod = mk_pod("ns-0", "pod-0", Some(("container-0", None)));
|
||||
pod.labels_mut()
|
||||
|
|
@ -38,9 +47,14 @@ fn route_attaches_to_server() {
|
|||
);
|
||||
|
||||
// Create route.
|
||||
test.index
|
||||
.write()
|
||||
.apply(mk_http_route("ns-0", "route-foo", "srv-8080"));
|
||||
apply_route(
|
||||
&mut test.index.write(),
|
||||
MkRoute {
|
||||
ns: "ns-0".to_string(),
|
||||
name: "route-foo".to_string(),
|
||||
server: "srv-8080".to_string(),
|
||||
},
|
||||
);
|
||||
assert!(rx.has_changed().unwrap());
|
||||
assert_eq!(
|
||||
rx.borrow().reference,
|
||||
|
|
@ -69,43 +83,89 @@ fn route_attaches_to_server() {
|
|||
)));
|
||||
}
|
||||
|
||||
fn mk_http_route(
|
||||
ns: impl ToString,
|
||||
name: impl ToString,
|
||||
server: impl ToString,
|
||||
) -> k8s_gateway_api::HttpRoute {
|
||||
k8s_gateway_api::HttpRoute {
|
||||
metadata: k8s::ObjectMeta {
|
||||
namespace: Some(ns.to_string()),
|
||||
name: Some(name.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s_gateway_api::HttpRouteSpec {
|
||||
inner: k8s_gateway_api::CommonRouteSpec {
|
||||
parent_refs: Some(vec![k8s_gateway_api::ParentReference {
|
||||
group: Some("policy.linkerd.io".to_string()),
|
||||
kind: Some("Server".to_string()),
|
||||
namespace: None,
|
||||
name: server.to_string(),
|
||||
section_name: None,
|
||||
port: None,
|
||||
struct MkRoute {
|
||||
ns: String,
|
||||
name: String,
|
||||
server: String,
|
||||
}
|
||||
|
||||
impl MkRoute {
|
||||
/// Returns the `gateway.networking.k8s.io` version of a `HTTPRoute` with
|
||||
/// the provided namespace, name, and server.
|
||||
fn gateway_api(self) -> k8s_gateway_api::HttpRoute {
|
||||
let Self { ns, name, server } = self;
|
||||
k8s_gateway_api::HttpRoute {
|
||||
metadata: k8s::ObjectMeta {
|
||||
namespace: Some(ns),
|
||||
name: Some(name),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s_gateway_api::HttpRouteSpec {
|
||||
inner: k8s_gateway_api::CommonRouteSpec {
|
||||
parent_refs: Some(vec![k8s_gateway_api::ParentReference {
|
||||
group: Some("policy.linkerd.io".to_string()),
|
||||
kind: Some("Server".to_string()),
|
||||
namespace: None,
|
||||
name: server,
|
||||
section_name: None,
|
||||
port: None,
|
||||
}]),
|
||||
},
|
||||
hostnames: None,
|
||||
rules: Some(vec![k8s_gateway_api::HttpRouteRule {
|
||||
matches: Some(vec![k8s_gateway_api::HttpRouteMatch {
|
||||
path: Some(k8s_gateway_api::HttpPathMatch::PathPrefix {
|
||||
value: "/foo/bar".to_string(),
|
||||
}),
|
||||
headers: None,
|
||||
query_params: None,
|
||||
method: Some("GET".to_string()),
|
||||
}]),
|
||||
filters: None,
|
||||
backend_refs: None,
|
||||
}]),
|
||||
},
|
||||
hostnames: None,
|
||||
rules: Some(vec![k8s_gateway_api::HttpRouteRule {
|
||||
matches: Some(vec![k8s_gateway_api::HttpRouteMatch {
|
||||
path: Some(k8s_gateway_api::HttpPathMatch::PathPrefix {
|
||||
value: "/foo/bar".to_string(),
|
||||
}),
|
||||
headers: None,
|
||||
query_params: None,
|
||||
method: Some("GET".to_string()),
|
||||
status: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `policy.linkerd.io` version of a `HTTPRoute` with
|
||||
/// the provided namespace, name, and server.
|
||||
fn linkerd(self) -> k8s::policy::HttpRoute {
|
||||
use k8s::policy::httproute::*;
|
||||
let Self { ns, name, server } = self;
|
||||
HttpRoute {
|
||||
metadata: k8s::ObjectMeta {
|
||||
namespace: Some(ns),
|
||||
name: Some(name),
|
||||
..Default::default()
|
||||
},
|
||||
spec: HttpRouteSpec {
|
||||
inner: k8s_gateway_api::CommonRouteSpec {
|
||||
parent_refs: Some(vec![k8s_gateway_api::ParentReference {
|
||||
group: Some("policy.linkerd.io".to_string()),
|
||||
kind: Some("Server".to_string()),
|
||||
namespace: None,
|
||||
name: server,
|
||||
section_name: None,
|
||||
port: None,
|
||||
}]),
|
||||
},
|
||||
hostnames: None,
|
||||
rules: Some(vec![HttpRouteRule {
|
||||
matches: Some(vec![HttpRouteMatch {
|
||||
path: Some(HttpPathMatch::PathPrefix {
|
||||
value: "/foo/bar".to_string(),
|
||||
}),
|
||||
headers: None,
|
||||
query_params: None,
|
||||
method: Some("GET".to_string()),
|
||||
}]),
|
||||
filters: None,
|
||||
}]),
|
||||
filters: None,
|
||||
backend_refs: None,
|
||||
}]),
|
||||
},
|
||||
status: None,
|
||||
},
|
||||
status: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
use crate::k8s::{
|
||||
labels,
|
||||
policy::{
|
||||
AuthorizationPolicy, AuthorizationPolicySpec, LocalTargetRef, MeshTLSAuthentication,
|
||||
MeshTLSAuthenticationSpec, NamespacedTargetRef, NetworkAuthentication,
|
||||
NetworkAuthenticationSpec, Server, ServerAuthorization, ServerAuthorizationSpec,
|
||||
ServerSpec,
|
||||
httproute, AuthorizationPolicy, AuthorizationPolicySpec, HttpRoute, HttpRouteSpec,
|
||||
LocalTargetRef, MeshTLSAuthentication, MeshTLSAuthenticationSpec, NamespacedTargetRef,
|
||||
NetworkAuthentication, NetworkAuthenticationSpec, Server, ServerAuthorization,
|
||||
ServerAuthorizationSpec, ServerSpec,
|
||||
},
|
||||
};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use futures::future;
|
||||
use hyper::{body::Buf, http, Body, Request, Response};
|
||||
use k8s_gateway_api::{HttpRoute, HttpRouteFilter, HttpRouteRule, HttpRouteSpec};
|
||||
use k8s_gateway_api as gateway;
|
||||
use k8s_openapi::api::core::v1::{Namespace, ServiceAccount};
|
||||
use kube::{core::DynamicObject, Resource, ResourceExt};
|
||||
use linkerd_policy_controller_k8s_index as index;
|
||||
|
|
@ -123,6 +123,10 @@ impl Admission {
|
|||
return self.admit_spec::<HttpRouteSpec>(req).await;
|
||||
}
|
||||
|
||||
if is_kind::<gateway::HttpRoute>(&req) {
|
||||
return self.admit_spec::<gateway::HttpRouteSpec>(req).await;
|
||||
}
|
||||
|
||||
AdmissionResponse::invalid(format_args!(
|
||||
"unsupported resource type: {}.{}.{}",
|
||||
req.kind.group, req.kind.version, req.kind.kind
|
||||
|
|
@ -205,6 +209,10 @@ fn validate_policy_target(ns: &str, tgt: &LocalTargetRef) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
if tgt.targets_kind::<gateway::HttpRoute>() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if tgt.targets_kind::<Namespace>() {
|
||||
if tgt.name != ns {
|
||||
bail!("cannot target another namespace: {}", tgt.name);
|
||||
|
|
@ -418,51 +426,85 @@ impl Validate<ServerAuthorizationSpec> for Admission {
|
|||
#[async_trait::async_trait]
|
||||
impl Validate<HttpRouteSpec> for Admission {
|
||||
async fn validate(self, _ns: &str, _name: &str, spec: HttpRouteSpec) -> Result<()> {
|
||||
let targets_server = spec.inner.parent_refs.iter().flatten().any(|parent_ref| {
|
||||
if let Some(p) = parent_ref.group.as_deref() {
|
||||
if let Some(k) = parent_ref.kind.as_deref() {
|
||||
return p.eq_ignore_ascii_case("policy.linkerd.io")
|
||||
&& k.eq_ignore_ascii_case("server");
|
||||
}
|
||||
}
|
||||
false
|
||||
});
|
||||
// The validation for the policy.linkerd.io HTTPRoute type is much
|
||||
// simpler than for the Gateway API version: the route must only target
|
||||
// `Server` resources.
|
||||
//
|
||||
// We don't have to do any validation that unsupported filters aren't
|
||||
// present, because Linkerd's HTTPRoute CRD doesn't include those
|
||||
// filters at all.
|
||||
let all_target_servers = spec
|
||||
.inner
|
||||
.parent_refs
|
||||
.iter()
|
||||
.flatten()
|
||||
.all(parent_ref_targets_server);
|
||||
anyhow::ensure!(
|
||||
all_target_servers,
|
||||
"policy.linkerd.io HTTPRoutes must target only Server resources"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Validate<gateway::HttpRouteSpec> for Admission {
|
||||
async fn validate(self, _ns: &str, _name: &str, spec: gateway::HttpRouteSpec) -> Result<()> {
|
||||
// Only validate HttpRoutes which have a Server as a parent_ref.
|
||||
let targets_server = spec
|
||||
.inner
|
||||
.parent_refs
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(parent_ref_targets_server);
|
||||
if !targets_server {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for rule in spec.rules.iter().flatten() {
|
||||
validate_http_route_rule(rule)?;
|
||||
validate_gateway_http_route_rule(rule)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_http_route_rule(rule: &HttpRouteRule) -> Result<()> {
|
||||
fn parent_ref_targets_server(p: &httproute::ParentReference) -> bool {
|
||||
match (p.group.as_deref(), p.kind.as_deref()) {
|
||||
(Some(group), Some(kind)) => {
|
||||
group.eq_ignore_ascii_case("policy.linkerd.io") && kind.eq_ignore_ascii_case("server")
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_gateway_http_route_rule(rule: &gateway::HttpRouteRule) -> Result<()> {
|
||||
if let Some(filters) = &rule.filters {
|
||||
validate_http_route_filters(filters)?;
|
||||
validate_gateway_http_route_filters(filters)?;
|
||||
}
|
||||
|
||||
for backend_ref in rule.backend_refs.iter().flatten() {
|
||||
if let Some(filters) = &backend_ref.filters {
|
||||
validate_http_route_filters(filters)?;
|
||||
validate_gateway_http_route_filters(filters)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_http_route_filters(filters: &[HttpRouteFilter]) -> Result<()> {
|
||||
fn validate_gateway_http_route_filters(filters: &[gateway::HttpRouteFilter]) -> Result<()> {
|
||||
for filter in filters.iter() {
|
||||
match filter {
|
||||
HttpRouteFilter::ExtensionRef { .. } => bail!("ExtensionRef filters are not supported"),
|
||||
HttpRouteFilter::RequestHeaderModifier { .. } => {}
|
||||
HttpRouteFilter::RequestMirror { .. } => {
|
||||
gateway::HttpRouteFilter::ExtensionRef { .. } => {
|
||||
bail!("ExtensionRef filters are not supported")
|
||||
}
|
||||
gateway::HttpRouteFilter::RequestHeaderModifier { .. } => {}
|
||||
gateway::HttpRouteFilter::RequestMirror { .. } => {
|
||||
bail!("RequestMirror filters are not supported")
|
||||
}
|
||||
HttpRouteFilter::RequestRedirect { .. } => {}
|
||||
HttpRouteFilter::URLRewrite { .. } => bail!("URLRewrite filters are not supported"),
|
||||
gateway::HttpRouteFilter::RequestRedirect { .. } => {}
|
||||
gateway::HttpRouteFilter::URLRewrite { .. } => {
|
||||
bail!("URLRewrite filters are not supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -146,9 +146,17 @@ async fn main() -> Result<()> {
|
|||
.instrument(info_span!("networkauthentications")),
|
||||
);
|
||||
|
||||
let http_routes = runtime.watch_all::<k8s_gateway_api::HttpRoute>(ListParams::default());
|
||||
let gateway_http_routes =
|
||||
runtime.watch_all::<k8s_gateway_api::HttpRoute>(ListParams::default());
|
||||
tokio::spawn(
|
||||
kubert::index::namespaced(index.clone(), http_routes).instrument(info_span!("httproutes")),
|
||||
kubert::index::namespaced(index.clone(), gateway_http_routes)
|
||||
.instrument(info_span!("httproutes", group = "networking.k8s.io")),
|
||||
);
|
||||
|
||||
let linkerd_http_routes = runtime.watch_all::<k8s::policy::HttpRoute>(ListParams::default());
|
||||
tokio::spawn(
|
||||
kubert::index::namespaced(index.clone(), linkerd_http_routes)
|
||||
.instrument(info_span!("httproutes", group = "policy.linkerd.io")),
|
||||
);
|
||||
|
||||
// Run the gRPC server, serving results by looking up against the index handle.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
use linkerd_policy_controller_k8s_api::{self as api, policy::httproute::*};
|
||||
use linkerd_policy_test::admission;
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn accepts_valid() {
|
||||
admission::accepts(|ns| HttpRoute {
|
||||
metadata: api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("test".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: HttpRouteSpec {
|
||||
inner: CommonRouteSpec {
|
||||
parent_refs: Some(vec![server_parent_ref(ns)]),
|
||||
},
|
||||
hostnames: None,
|
||||
rules: Some(rules()),
|
||||
},
|
||||
status: None,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn rejects_non_server_parent_ref() {
|
||||
admission::rejects(|ns| HttpRoute {
|
||||
metadata: api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("test".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: HttpRouteSpec {
|
||||
inner: CommonRouteSpec {
|
||||
parent_refs: Some(vec![non_server_parent_ref(ns)]),
|
||||
},
|
||||
hostnames: None,
|
||||
rules: Some(rules()),
|
||||
},
|
||||
status: None,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Tests that an `HTTPRoute` is rejected if it contains *any* parent refs that
|
||||
/// target non-`Server` resources, even if it also targets a `Server` resource.
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn rejects_mixed_parent_ref() {
|
||||
admission::rejects(|ns| HttpRoute {
|
||||
metadata: api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("test".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: HttpRouteSpec {
|
||||
inner: CommonRouteSpec {
|
||||
parent_refs: Some(vec![
|
||||
server_parent_ref(ns.clone()),
|
||||
non_server_parent_ref(ns),
|
||||
]),
|
||||
},
|
||||
hostnames: None,
|
||||
rules: Some(rules()),
|
||||
},
|
||||
status: None,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
fn server_parent_ref(ns: String) -> ParentReference {
|
||||
ParentReference {
|
||||
group: Some("policy.linkerd.io".to_string()),
|
||||
kind: Some("Server".to_string()),
|
||||
namespace: Some(ns),
|
||||
name: "my-server".to_string(),
|
||||
section_name: None,
|
||||
port: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn non_server_parent_ref(ns: String) -> ParentReference {
|
||||
ParentReference {
|
||||
group: Some("foo.bar.bas".to_string()),
|
||||
kind: Some("Gateway".to_string()),
|
||||
namespace: Some(ns),
|
||||
name: "my-gateway".to_string(),
|
||||
section_name: None,
|
||||
port: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn rules() -> Vec<HttpRouteRule> {
|
||||
vec![HttpRouteRule {
|
||||
matches: Some(vec![HttpRouteMatch {
|
||||
path: Some(HttpPathMatch::Exact {
|
||||
value: "/foo".to_string(),
|
||||
}),
|
||||
..HttpRouteMatch::default()
|
||||
}]),
|
||||
filters: None,
|
||||
}]
|
||||
}
|
||||
|
|
@ -285,224 +285,275 @@ async fn server_with_authorization_policy() {
|
|||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn server_with_http_route() {
|
||||
async fn server_with_gateway_http_route() {
|
||||
with_temp_ns(|client, ns| async move {
|
||||
// Create a pod that does nothing. It's injected with a proxy, so we can
|
||||
// attach policies to its admin server.
|
||||
let pod = create_ready_pod(&client, mk_pause(&ns, "pause")).await;
|
||||
|
||||
let mut rx = retry_watch_server(&client, &ns, &pod.name_unchecked()).await;
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an initial config");
|
||||
tracing::trace!(?config);
|
||||
assert_is_default_all_unauthenticated!(config);
|
||||
assert_protocol_detect!(config);
|
||||
|
||||
// Create a server that selects the pod's proxy admin server and ensure
|
||||
// that the update now uses this server, which has no authorizations
|
||||
// and no routes.
|
||||
let _server = create(&client, mk_admin_server(&ns, "linkerd-admin")).await;
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an updated config");
|
||||
tracing::trace!(?config);
|
||||
assert_eq!(
|
||||
config.protocol,
|
||||
Some(grpc::inbound::ProxyProtocol {
|
||||
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
|
||||
grpc::inbound::proxy_protocol::Http1::default()
|
||||
)),
|
||||
}),
|
||||
);
|
||||
assert_eq!(config.authorizations, vec![]);
|
||||
assert_eq!(
|
||||
config.labels,
|
||||
convert_args!(hashmap!(
|
||||
"group" => "policy.linkerd.io",
|
||||
"kind" => "server",
|
||||
"name" => "linkerd-admin"
|
||||
)),
|
||||
);
|
||||
|
||||
// Create an http route that refers to the `linkerd-admin` server (by
|
||||
// name) and ensure that the update now reflects this route.
|
||||
let route = create(
|
||||
&client,
|
||||
k8s_gateway_api::HttpRoute {
|
||||
metadata: kube::api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("metrics-route".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s_gateway_api::HttpRouteSpec {
|
||||
inner: k8s_gateway_api::CommonRouteSpec {
|
||||
parent_refs: Some(vec![k8s_gateway_api::ParentReference {
|
||||
group: Some("policy.linkerd.io".to_string()),
|
||||
kind: Some("Server".to_string()),
|
||||
namespace: None,
|
||||
name: "linkerd-admin".to_string(),
|
||||
section_name: None,
|
||||
port: None,
|
||||
}]),
|
||||
},
|
||||
hostnames: None,
|
||||
rules: Some(vec![k8s_gateway_api::HttpRouteRule {
|
||||
matches: Some(vec![k8s_gateway_api::HttpRouteMatch {
|
||||
path: Some(k8s_gateway_api::HttpPathMatch::Exact {
|
||||
value: "/metrics".to_string(),
|
||||
}),
|
||||
headers: None,
|
||||
query_params: None,
|
||||
method: Some("GET".to_string()),
|
||||
}]),
|
||||
filters: None,
|
||||
backend_refs: None,
|
||||
// name).
|
||||
let route = k8s_gateway_api::HttpRoute {
|
||||
metadata: kube::api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("metrics-route".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s_gateway_api::HttpRouteSpec {
|
||||
inner: k8s_gateway_api::CommonRouteSpec {
|
||||
parent_refs: Some(vec![k8s_gateway_api::ParentReference {
|
||||
group: Some("policy.linkerd.io".to_string()),
|
||||
kind: Some("Server".to_string()),
|
||||
namespace: None,
|
||||
name: "linkerd-admin".to_string(),
|
||||
section_name: None,
|
||||
port: None,
|
||||
}]),
|
||||
},
|
||||
status: None,
|
||||
hostnames: None,
|
||||
rules: Some(vec![k8s_gateway_api::HttpRouteRule {
|
||||
matches: Some(vec![k8s_gateway_api::HttpRouteMatch {
|
||||
path: Some(k8s_gateway_api::HttpPathMatch::Exact {
|
||||
value: "/metrics".to_string(),
|
||||
}),
|
||||
headers: None,
|
||||
query_params: None,
|
||||
method: Some("GET".to_string()),
|
||||
}]),
|
||||
filters: None,
|
||||
backend_refs: None,
|
||||
}]),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an updated config");
|
||||
tracing::trace!(?config);
|
||||
let http1 = if let grpc::inbound::proxy_protocol::Kind::Http1(http1) = config
|
||||
.protocol
|
||||
.expect("must have proxy protocol")
|
||||
.kind
|
||||
.expect("must have kind")
|
||||
{
|
||||
http1
|
||||
} else {
|
||||
panic!("proxy protocol must be HTTP1")
|
||||
status: None,
|
||||
};
|
||||
let h1_route = http1.routes.first().expect("must have route");
|
||||
let rule_match = h1_route
|
||||
.rules
|
||||
.first()
|
||||
.expect("must have rule")
|
||||
.matches
|
||||
.first()
|
||||
.expect("must have match");
|
||||
// Route has no authorizations by default.
|
||||
assert_eq!(h1_route.authorizations, Vec::default());
|
||||
// Route has appropriate metadata.
|
||||
assert_eq!(
|
||||
h1_route
|
||||
.metadata
|
||||
.to_owned()
|
||||
.expect("route must have metadata"),
|
||||
grpc::meta::Metadata {
|
||||
kind: Some(grpc::meta::metadata::Kind::Resource(grpc::meta::Resource {
|
||||
group: "gateway.networking.k8s.io".to_string(),
|
||||
kind: "HTTPRoute".to_string(),
|
||||
name: "metrics-route".to_string(),
|
||||
}))
|
||||
}
|
||||
);
|
||||
// Route has path match.
|
||||
assert_eq!(
|
||||
rule_match
|
||||
.path
|
||||
.to_owned()
|
||||
.expect("must have path match")
|
||||
.kind
|
||||
.expect("must have kind"),
|
||||
grpc::http_route::path_match::Kind::Exact("/metrics".to_string()),
|
||||
);
|
||||
|
||||
// Create a network authentication and an authorization policy that
|
||||
// refers to the `metrics-route` route (by name).
|
||||
let all_nets = create(
|
||||
&client,
|
||||
k8s::policy::NetworkAuthentication {
|
||||
metadata: kube::api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("all-admin".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s::policy::NetworkAuthenticationSpec {
|
||||
networks: vec![
|
||||
k8s::policy::network_authentication::Network {
|
||||
cidr: Ipv4Net::default().into(),
|
||||
except: None,
|
||||
},
|
||||
k8s::policy::network_authentication::Network {
|
||||
cidr: Ipv6Net::default().into(),
|
||||
except: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
.await;
|
||||
create(
|
||||
&client,
|
||||
k8s::policy::AuthorizationPolicy {
|
||||
metadata: kube::api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("all-admin".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s::policy::AuthorizationPolicySpec {
|
||||
target_ref: k8s::policy::LocalTargetRef::from_resource(&route),
|
||||
required_authentication_refs: vec![
|
||||
k8s::policy::NamespacedTargetRef::from_resource(&all_nets),
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an updated config");
|
||||
tracing::trace!(?config);
|
||||
let http1 = if let grpc::inbound::proxy_protocol::Kind::Http1(http1) = config
|
||||
.protocol
|
||||
.expect("must have proxy protocol")
|
||||
.kind
|
||||
.expect("must have kind")
|
||||
{
|
||||
http1
|
||||
} else {
|
||||
panic!("proxy protocol must be HTTP1")
|
||||
};
|
||||
let h1_route = http1.routes.first().expect("must have route");
|
||||
assert_eq!(h1_route.authorizations.len(), 1, "must have authorizations");
|
||||
|
||||
// Delete the `HttpRoute` and ensure that the update reverts to the
|
||||
// default.
|
||||
kube::Api::<k8s_gateway_api::HttpRoute>::namespaced(client.clone(), &ns)
|
||||
.delete("metrics-route", &kube::api::DeleteParams::default())
|
||||
.await
|
||||
.expect("HttpRoute must be deleted");
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an updated config");
|
||||
tracing::trace!(?config);
|
||||
assert_eq!(
|
||||
config.protocol,
|
||||
Some(grpc::inbound::ProxyProtocol {
|
||||
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
|
||||
grpc::inbound::proxy_protocol::Http1::default()
|
||||
)),
|
||||
}),
|
||||
);
|
||||
test_server_with_http_route(client, ns, route).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn server_with_linkerd_http_route() {
|
||||
use k8s::policy::httproute as api;
|
||||
|
||||
with_temp_ns(|client, ns| async move {
|
||||
// Create an http route that refers to the `linkerd-admin` server (by
|
||||
// name).
|
||||
let route = api::HttpRoute {
|
||||
metadata: kube::api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("metrics-route".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: api::HttpRouteSpec {
|
||||
inner: api::CommonRouteSpec {
|
||||
parent_refs: Some(vec![api::ParentReference {
|
||||
group: Some("policy.linkerd.io".to_string()),
|
||||
kind: Some("Server".to_string()),
|
||||
namespace: None,
|
||||
name: "linkerd-admin".to_string(),
|
||||
section_name: None,
|
||||
port: None,
|
||||
}]),
|
||||
},
|
||||
hostnames: None,
|
||||
rules: Some(vec![api::HttpRouteRule {
|
||||
matches: Some(vec![api::HttpRouteMatch {
|
||||
path: Some(api::HttpPathMatch::Exact {
|
||||
value: "/metrics".to_string(),
|
||||
}),
|
||||
headers: None,
|
||||
query_params: None,
|
||||
method: Some("GET".to_string()),
|
||||
}]),
|
||||
filters: None,
|
||||
}]),
|
||||
},
|
||||
status: None,
|
||||
};
|
||||
test_server_with_http_route(client, ns, route).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn test_server_with_http_route<R>(client: kube::Client, ns: String, route: R)
|
||||
where
|
||||
R: kube::Resource + serde::Serialize + serde::de::DeserializeOwned + Clone + std::fmt::Debug,
|
||||
R::DynamicType: Default,
|
||||
{
|
||||
// Create a pod that does nothing. It's injected with a proxy, so we can
|
||||
// attach policies to its admin server.
|
||||
let pod = create_ready_pod(&client, mk_pause(&ns, "pause")).await;
|
||||
|
||||
let mut rx = retry_watch_server(&client, &ns, &pod.name_unchecked()).await;
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an initial config");
|
||||
tracing::trace!(?config);
|
||||
assert_is_default_all_unauthenticated!(config);
|
||||
assert_protocol_detect!(config);
|
||||
|
||||
// Create a server that selects the pod's proxy admin server and ensure
|
||||
// that the update now uses this server, which has no authorizations
|
||||
// and no routes.
|
||||
let _server = create(&client, mk_admin_server(&ns, "linkerd-admin")).await;
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an updated config");
|
||||
tracing::trace!(?config);
|
||||
assert_eq!(
|
||||
config.protocol,
|
||||
Some(grpc::inbound::ProxyProtocol {
|
||||
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
|
||||
grpc::inbound::proxy_protocol::Http1::default()
|
||||
)),
|
||||
}),
|
||||
);
|
||||
assert_eq!(config.authorizations, vec![]);
|
||||
assert_eq!(
|
||||
config.labels,
|
||||
convert_args!(hashmap!(
|
||||
"group" => "policy.linkerd.io",
|
||||
"kind" => "server",
|
||||
"name" => "linkerd-admin"
|
||||
)),
|
||||
);
|
||||
|
||||
// Create an http route that refers to the `linkerd-admin` server (by
|
||||
// name) and ensure that the update now reflects this route.
|
||||
let route = create(&client, route).await;
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an updated config");
|
||||
tracing::trace!(?config);
|
||||
let http1 = if let grpc::inbound::proxy_protocol::Kind::Http1(http1) = config
|
||||
.protocol
|
||||
.expect("must have proxy protocol")
|
||||
.kind
|
||||
.expect("must have kind")
|
||||
{
|
||||
http1
|
||||
} else {
|
||||
panic!("proxy protocol must be HTTP1")
|
||||
};
|
||||
let h1_route = http1.routes.first().expect("must have route");
|
||||
let rule_match = h1_route
|
||||
.rules
|
||||
.first()
|
||||
.expect("must have rule")
|
||||
.matches
|
||||
.first()
|
||||
.expect("must have match");
|
||||
// Route has no authorizations by default.
|
||||
assert_eq!(h1_route.authorizations, Vec::default());
|
||||
// Route has appropriate metadata.
|
||||
assert_eq!(
|
||||
h1_route
|
||||
.metadata
|
||||
.to_owned()
|
||||
.expect("route must have metadata"),
|
||||
grpc::meta::Metadata {
|
||||
kind: Some(grpc::meta::metadata::Kind::Resource(grpc::meta::Resource {
|
||||
group: "gateway.networking.k8s.io".to_string(),
|
||||
kind: "HTTPRoute".to_string(),
|
||||
name: "metrics-route".to_string(),
|
||||
}))
|
||||
}
|
||||
);
|
||||
// Route has path match.
|
||||
assert_eq!(
|
||||
rule_match
|
||||
.path
|
||||
.to_owned()
|
||||
.expect("must have path match")
|
||||
.kind
|
||||
.expect("must have kind"),
|
||||
grpc::http_route::path_match::Kind::Exact("/metrics".to_string()),
|
||||
);
|
||||
|
||||
// Create a network authentication and an authorization policy that
|
||||
// refers to the `metrics-route` route (by name).
|
||||
let all_nets = create(
|
||||
&client,
|
||||
k8s::policy::NetworkAuthentication {
|
||||
metadata: kube::api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("all-admin".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s::policy::NetworkAuthenticationSpec {
|
||||
networks: vec![
|
||||
k8s::policy::network_authentication::Network {
|
||||
cidr: Ipv4Net::default().into(),
|
||||
except: None,
|
||||
},
|
||||
k8s::policy::network_authentication::Network {
|
||||
cidr: Ipv6Net::default().into(),
|
||||
except: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
.await;
|
||||
create(
|
||||
&client,
|
||||
k8s::policy::AuthorizationPolicy {
|
||||
metadata: kube::api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("all-admin".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s::policy::AuthorizationPolicySpec {
|
||||
target_ref: k8s::policy::LocalTargetRef::from_resource(&route),
|
||||
required_authentication_refs: vec![
|
||||
k8s::policy::NamespacedTargetRef::from_resource(&all_nets),
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an updated config");
|
||||
tracing::trace!(?config);
|
||||
let http1 = if let grpc::inbound::proxy_protocol::Kind::Http1(http1) = config
|
||||
.protocol
|
||||
.expect("must have proxy protocol")
|
||||
.kind
|
||||
.expect("must have kind")
|
||||
{
|
||||
http1
|
||||
} else {
|
||||
panic!("proxy protocol must be HTTP1")
|
||||
};
|
||||
let h1_route = http1.routes.first().expect("must have route");
|
||||
assert_eq!(h1_route.authorizations.len(), 1, "must have authorizations");
|
||||
|
||||
// Delete the `HttpRoute` and ensure that the update reverts to the
|
||||
// default.
|
||||
kube::Api::<R>::namespaced(client.clone(), &ns)
|
||||
.delete("metrics-route", &kube::api::DeleteParams::default())
|
||||
.await
|
||||
.expect("HttpRoute must be deleted");
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an updated config");
|
||||
tracing::trace!(?config);
|
||||
assert_eq!(
|
||||
config.protocol,
|
||||
Some(grpc::inbound::ProxyProtocol {
|
||||
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
|
||||
grpc::inbound::proxy_protocol::Http1::default()
|
||||
)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn mk_pause(ns: &str, name: &str) -> k8s::Pod {
|
||||
|
|
|
|||
Loading…
Reference in New Issue